@@ -546,6 +546,92 @@ async def available(self):
546546 await computer_client .get_booter (ctx , "session-b" )
547547
548548
549+ @pytest .mark .asyncio
550+ async def test_get_booter_uses_selected_running_sandbox_instead_of_default (
551+ monkeypatch ,
552+ tmp_path ,
553+ ):
554+ from astrbot .core .computer import computer_client
555+ from astrbot .core .computer .cua_registry import CuaSandboxRegistry
556+
557+ class FakeBooter :
558+ def __init__ (self , sandbox_id : str ):
559+ self .sandbox_id = sandbox_id
560+
561+ async def available (self ):
562+ return True
563+
564+ registry = CuaSandboxRegistry (storage_path = tmp_path / "registry.json" )
565+ registry .upsert_sandbox (
566+ sandbox_id = "sb-default" ,
567+ sandbox_name = "default" ,
568+ booter_type = "cua" ,
569+ provider = "cua" ,
570+ managed = True ,
571+ created_by_astrbot = True ,
572+ owner_user_id = "session-a" ,
573+ owner_session_id = "session-a" ,
574+ connect_info = {},
575+ is_default = True ,
576+ )
577+ registry .upsert_sandbox (
578+ sandbox_id = "sb-selected" ,
579+ sandbox_name = "selected" ,
580+ booter_type = "cua" ,
581+ provider = "cua" ,
582+ managed = True ,
583+ created_by_astrbot = True ,
584+ owner_user_id = "session-a" ,
585+ owner_session_id = "session-a" ,
586+ connect_info = {},
587+ )
588+ registry .set_current_sandbox_id ("session-a" , "sb-selected" )
589+ monkeypatch .setattr (computer_client , "cua_registry" , registry )
590+ computer_client .session_booter .clear ()
591+ computer_client .session_booter ["sb-default" ] = FakeBooter ("sb-default" )
592+ computer_client .session_booter ["sb-selected" ] = FakeBooter ("sb-selected" )
593+
594+ booter = await computer_client .get_booter (
595+ FakeContext (
596+ {
597+ "provider_settings" : {
598+ "computer_use_runtime" : "sandbox" ,
599+ "sandbox" : {"booter" : "cua" },
600+ }
601+ }
602+ ),
603+ "session-a" ,
604+ )
605+
606+ assert booter .sandbox_id == "sb-selected"
607+
608+
609+ @pytest .mark .asyncio
610+ async def test_switch_current_rejects_non_running_sandbox (monkeypatch , tmp_path ):
611+ from astrbot .core .computer import computer_client
612+ from astrbot .core .computer .cua_registry import CuaSandboxRegistry
613+
614+ registry = CuaSandboxRegistry (storage_path = tmp_path / "registry.json" )
615+ registry .upsert_sandbox (
616+ sandbox_id = "sb-stale" ,
617+ sandbox_name = "stale" ,
618+ booter_type = "cua" ,
619+ provider = "cua" ,
620+ managed = True ,
621+ created_by_astrbot = True ,
622+ owner_user_id = "session-a" ,
623+ owner_session_id = "session-a" ,
624+ connect_info = {},
625+ )
626+ monkeypatch .setattr (computer_client , "cua_registry" , registry )
627+ computer_client .session_booter .clear ()
628+
629+ with pytest .raises (RuntimeError , match = "not running" ):
630+ computer_client .switch_current_cua_sandbox ("session-a" , "sb-stale" )
631+
632+ assert registry .get_current_sandbox_id ("session-a" ) is None
633+
634+
549635@pytest .mark .asyncio
550636async def test_busy_sandbox_screenshot_is_allowed_without_taking_control (
551637 monkeypatch , tmp_path
@@ -935,6 +1021,53 @@ async def available(self):
9351021 )
9361022
9371023
1024+ @pytest .mark .asyncio
1025+ async def test_get_booter_serializes_concurrent_default_boot (monkeypatch , tmp_path ):
1026+ from astrbot .core .computer import computer_client
1027+ from astrbot .core .computer .cua_registry import CuaSandboxRegistry
1028+
1029+ boot_started = asyncio .Event ()
1030+ release_boot = asyncio .Event ()
1031+ boots = []
1032+
1033+ class FakeBooter :
1034+ def __init__ (self , sandbox_id : str ):
1035+ self .sandbox_id = sandbox_id
1036+
1037+ async def available (self ):
1038+ return True
1039+
1040+ async def fake_boot_managed (ctx , session_id , sandbox_id , cua_kwargs ):
1041+ boots .append (sandbox_id )
1042+ boot_started .set ()
1043+ await release_boot .wait ()
1044+ return FakeBooter (sandbox_id )
1045+
1046+ registry = CuaSandboxRegistry (storage_path = tmp_path / "registry.json" )
1047+ monkeypatch .setattr (computer_client , "cua_registry" , registry )
1048+ monkeypatch .setattr (computer_client , "_boot_managed_cua_sandbox" , fake_boot_managed )
1049+ computer_client .session_booter .clear ()
1050+
1051+ ctx = FakeContext (
1052+ {
1053+ "provider_settings" : {
1054+ "computer_use_runtime" : "sandbox" ,
1055+ "sandbox" : {"booter" : "cua" },
1056+ }
1057+ }
1058+ )
1059+ task_one = asyncio .create_task (computer_client .get_booter (ctx , "session-a" ))
1060+ await boot_started .wait ()
1061+ task_two = asyncio .create_task (computer_client .get_booter (ctx , "session-a" ))
1062+ await asyncio .sleep (0 )
1063+ release_boot .set ()
1064+
1065+ booter_one , booter_two = await asyncio .gather (task_one , task_two )
1066+
1067+ assert booter_one is booter_two
1068+ assert len (boots ) == 1
1069+
1070+
9381071@pytest .mark .asyncio
9391072async def test_get_booter_shuts_down_client_when_skill_sync_fails (monkeypatch ):
9401073 from astrbot .core .computer import computer_client
@@ -1036,6 +1169,48 @@ async def shutdown(self):
10361169
10371170 assert shutdowns == [booter .session_id ]
10381171 assert booter .sandbox_id not in computer_client .session_booter
1172+ assert registry .get_sandbox (booter .sandbox_id ) is None
1173+
1174+
1175+ @pytest .mark .asyncio
1176+ async def test_cua_idle_timeout_marks_persistent_sandbox_stopped_after_lease_release (
1177+ monkeypatch ,
1178+ ):
1179+ from astrbot .core .computer import computer_client
1180+ from astrbot .core .computer .cua_registry import CuaSandboxRegistry
1181+
1182+ shutdowns = []
1183+
1184+ class FakeBooter :
1185+ async def shutdown (self ):
1186+ shutdowns .append ("sb-persistent" )
1187+
1188+ registry = CuaSandboxRegistry (storage_path = Path ("/tmp/cua-idle-persistent.json" ))
1189+ registry .upsert_sandbox (
1190+ sandbox_id = "sb-persistent" ,
1191+ sandbox_name = "persistent" ,
1192+ booter_type = "cua" ,
1193+ provider = "cua" ,
1194+ managed = True ,
1195+ created_by_astrbot = True ,
1196+ owner_user_id = "session-a" ,
1197+ owner_session_id = "session-a" ,
1198+ connect_info = {},
1199+ retention_policy = "persistent" ,
1200+ idle_timeout = 0.05 ,
1201+ )
1202+ monkeypatch .setattr (computer_client , "cua_registry" , registry )
1203+ computer_client .session_booter .clear ()
1204+ computer_client .session_booter ["sb-persistent" ] = FakeBooter ()
1205+
1206+ computer_client ._schedule_cua_idle_cleanup ("sb-persistent" , 0.05 )
1207+ await asyncio .sleep (0.1 )
1208+
1209+ record = registry .get_sandbox ("sb-persistent" )
1210+ assert shutdowns == ["sb-persistent" ]
1211+ assert record is not None
1212+ assert record ["status" ] == "stopped"
1213+ assert "sb-persistent" not in computer_client .session_booter
10391214
10401215
10411216@pytest .mark .asyncio
0 commit comments