@@ -20,6 +20,13 @@ def get_config(self, umo: str | None = None):
2020 return self ._config
2121
2222
23+ def _clear_cua_session_state (computer_client , session_id : str ) -> None :
24+ computer_client .session_booter .pop (session_id , None )
25+ state = getattr (computer_client , "cua_idle_state" , {}).pop (session_id , None )
26+ if state is not None and not state .task .done ():
27+ state .task .cancel ()
28+
29+
2330class FakeShell :
2431 def __init__ (self ):
2532 self .commands = []
@@ -267,6 +274,7 @@ def test_cua_default_config_matches_booter_defaults():
267274 assert sandbox_defaults ["cua_image" ] == CUA_DEFAULT_CONFIG ["image" ]
268275 assert sandbox_defaults ["cua_os_type" ] == CUA_DEFAULT_CONFIG ["os_type" ]
269276 assert sandbox_defaults ["cua_ttl" ] == CUA_DEFAULT_CONFIG ["ttl" ]
277+ assert sandbox_defaults ["cua_idle_timeout" ] == 0
270278 assert (
271279 sandbox_defaults ["cua_telemetry_enabled" ]
272280 == CUA_DEFAULT_CONFIG ["telemetry_enabled" ]
@@ -367,6 +375,189 @@ async def fail_sync(booter):
367375 assert "cua-sync-fail" not in computer_client .session_booter
368376
369377
378+ @pytest .mark .asyncio
379+ async def test_cua_idle_timeout_shuts_down_session_proactively (monkeypatch ):
380+ from astrbot .core .computer import computer_client
381+
382+ shutdowns = []
383+
384+ class FakeCuaBooter :
385+ def __init__ (self , ** kwargs ):
386+ self .kwargs = kwargs
387+
388+ async def boot (self , session_id : str ):
389+ self .session_id = session_id
390+
391+ async def available (self ):
392+ return True
393+
394+ async def shutdown (self ):
395+ shutdowns .append (self .session_id )
396+
397+ monkeypatch .setattr (
398+ computer_client , "_sync_skills_to_sandbox" , lambda booter : asyncio .sleep (0 )
399+ )
400+ monkeypatch .setattr (
401+ "astrbot.core.computer.booters.cua.CuaBooter" ,
402+ FakeCuaBooter ,
403+ raising = False ,
404+ )
405+ _clear_cua_session_state (computer_client , "cua-idle-expire" )
406+
407+ ctx = FakeContext (
408+ {
409+ "provider_settings" : {
410+ "computer_use_runtime" : "sandbox" ,
411+ "sandbox" : {
412+ "booter" : "cua" ,
413+ "cua_idle_timeout" : 0.1 ,
414+ },
415+ }
416+ }
417+ )
418+
419+ booter = await computer_client .get_booter (ctx , "cua-idle-expire" )
420+ await asyncio .sleep (0.2 )
421+
422+ assert shutdowns == [booter .session_id ]
423+ assert "cua-idle-expire" not in computer_client .session_booter
424+
425+
426+ @pytest .mark .asyncio
427+ async def test_cua_idle_timeout_refreshes_on_reuse (monkeypatch ):
428+ from astrbot .core .computer import computer_client
429+
430+ shutdowns = []
431+
432+ class FakeCuaBooter :
433+ def __init__ (self , ** kwargs ):
434+ self .kwargs = kwargs
435+
436+ async def boot (self , session_id : str ):
437+ self .session_id = session_id
438+
439+ async def available (self ):
440+ return True
441+
442+ async def shutdown (self ):
443+ shutdowns .append (self .session_id )
444+
445+ monkeypatch .setattr (
446+ computer_client , "_sync_skills_to_sandbox" , lambda booter : asyncio .sleep (0 )
447+ )
448+ monkeypatch .setattr (
449+ "astrbot.core.computer.booters.cua.CuaBooter" ,
450+ FakeCuaBooter ,
451+ raising = False ,
452+ )
453+ _clear_cua_session_state (computer_client , "cua-idle-refresh" )
454+
455+ ctx = FakeContext (
456+ {
457+ "provider_settings" : {
458+ "computer_use_runtime" : "sandbox" ,
459+ "sandbox" : {
460+ "booter" : "cua" ,
461+ "cua_idle_timeout" : 0.2 ,
462+ },
463+ }
464+ }
465+ )
466+
467+ booter1 = await computer_client .get_booter (ctx , "cua-idle-refresh" )
468+ await asyncio .sleep (0.05 )
469+ booter2 = await computer_client .get_booter (ctx , "cua-idle-refresh" )
470+ await asyncio .sleep (0.05 )
471+
472+ assert booter2 is booter1
473+ assert shutdowns == []
474+
475+ await asyncio .sleep (0.25 )
476+
477+ assert shutdowns == [booter1 .session_id ]
478+ assert "cua-idle-refresh" not in computer_client .session_booter
479+
480+
481+ @pytest .mark .asyncio
482+ async def test_cua_idle_timeout_zero_disables_proactive_shutdown (monkeypatch ):
483+ from astrbot .core .computer import computer_client
484+
485+ shutdowns = []
486+
487+ class FakeCuaBooter :
488+ def __init__ (self , ** kwargs ):
489+ self .kwargs = kwargs
490+
491+ async def boot (self , session_id : str ):
492+ self .session_id = session_id
493+
494+ async def available (self ):
495+ return True
496+
497+ async def shutdown (self ):
498+ shutdowns .append (self .session_id )
499+
500+ monkeypatch .setattr (
501+ computer_client , "_sync_skills_to_sandbox" , lambda booter : asyncio .sleep (0 )
502+ )
503+ monkeypatch .setattr (
504+ "astrbot.core.computer.booters.cua.CuaBooter" ,
505+ FakeCuaBooter ,
506+ raising = False ,
507+ )
508+ _clear_cua_session_state (computer_client , "cua-idle-disabled" )
509+
510+ ctx = FakeContext (
511+ {
512+ "provider_settings" : {
513+ "computer_use_runtime" : "sandbox" ,
514+ "sandbox" : {
515+ "booter" : "cua" ,
516+ "cua_idle_timeout" : 0 ,
517+ },
518+ }
519+ }
520+ )
521+
522+ await computer_client .get_booter (ctx , "cua-idle-disabled" )
523+ await asyncio .sleep (0.05 )
524+
525+ assert shutdowns == []
526+ assert "cua-idle-disabled" in computer_client .session_booter
527+ assert "cua-idle-disabled" not in computer_client .cua_idle_state
528+
529+
530+ @pytest .mark .asyncio
531+ async def test_non_cua_booter_does_not_schedule_idle_cleanup (monkeypatch ):
532+ from astrbot .core .computer import computer_client
533+
534+ class FakeShipyardBooter :
535+ async def available (self ):
536+ return True
537+
538+ _clear_cua_session_state (computer_client , "shipyard-session" )
539+ computer_client .session_booter ["shipyard-session" ] = FakeShipyardBooter ()
540+
541+ ctx = FakeContext (
542+ {
543+ "provider_settings" : {
544+ "computer_use_runtime" : "sandbox" ,
545+ "sandbox" : {
546+ "booter" : "shipyard" ,
547+ "shipyard_endpoint" : "http://localhost:8080" ,
548+ "shipyard_access_token" : "token" ,
549+ "cua_idle_timeout" : 0.01 ,
550+ },
551+ }
552+ }
553+ )
554+
555+ booter = await computer_client .get_booter (ctx , "shipyard-session" )
556+
557+ assert isinstance (booter , FakeShipyardBooter )
558+ assert "shipyard-session" not in computer_client .cua_idle_state
559+
560+
370561@pytest .mark .asyncio
371562async def test_cua_components_map_sdk_results (tmp_path ):
372563 from astrbot .core .computer .booters .cua import (
0 commit comments