@@ -72,17 +72,22 @@ def _hyperlight_integration_runtime_skip_reason() -> str | None:
7272 if (reason := _hyperlight_integration_static_skip_reason ()) is not None :
7373 return reason
7474
75+ sandbox : Any | None = None
7576 try :
7677 sandbox_cls = execute_code_module ._load_sandbox_class ()
77- sandbox = sandbox_cls (
78+ sandbox_instance = sandbox_cls (
7879 backend = execute_code_module .DEFAULT_HYPERLIGHT_BACKEND ,
7980 module = execute_code_module .DEFAULT_HYPERLIGHT_MODULE ,
8081 )
81- sandbox .run ("None" )
82+ sandbox = sandbox_instance
83+ sandbox_instance .run ("None" )
8284 except RuntimeError as exc :
8385 message = str (exc )
8486 if "no hypervisor was found for sandbox" in message .lower ():
8587 return "Hyperlight integration tests require a runner with a working Hyperlight hypervisor."
88+ finally :
89+ if sandbox is not None :
90+ _close_sandbox (sandbox )
8691
8792 return None
8893
@@ -146,6 +151,23 @@ def span_exporter(monkeypatch) -> Generator[InMemorySpanExporter]:
146151 exporter .clear ()
147152
148153
154+ def _close_sandbox (sandbox : Any ) -> None :
155+ close_hook = getattr (sandbox , "close" , None ) or getattr (sandbox , "shutdown" , None )
156+ if callable (close_hook ):
157+ with contextlib .suppress (Exception ):
158+ close_hook ()
159+
160+
161+ def _close_execute_code_registry (execute_code : HyperlightExecuteCodeTool ) -> None :
162+ close_hook = getattr (execute_code ._registry , "close" , None )
163+ if callable (close_hook ):
164+ close_hook ()
165+
166+
167+ def _close_provider_registry (provider : HyperlightCodeActProvider ) -> None :
168+ _close_execute_code_registry (provider ._execute_code_tool )
169+
170+
149171@pytest .fixture (scope = "module" )
150172def shared_sandbox ():
151173 """Long-lived sandbox with snapshot/restore for read-mostly tests.
@@ -163,7 +185,10 @@ def shared_sandbox():
163185 )
164186 sandbox .run ("None" )
165187 snapshot = sandbox .snapshot ()
166- yield sandbox , snapshot
188+ try :
189+ yield sandbox , snapshot
190+ finally :
191+ _close_sandbox (sandbox )
167192
168193
169194@pytest .fixture
@@ -190,7 +215,10 @@ def fresh_sandbox():
190215 module = execute_code_module .DEFAULT_HYPERLIGHT_MODULE ,
191216 temp_output = True ,
192217 )
193- yield sandbox
218+ try :
219+ yield sandbox
220+ finally :
221+ _close_sandbox (sandbox )
194222
195223
196224@tool (approval_mode = "never_require" )
@@ -1233,10 +1261,13 @@ async def test_agent_runs_hyperlight_codeact_end_to_end_with_fake_sandbox(monkey
12331261 provider = HyperlightCodeActProvider (tools = [compute ])
12341262 agent = Agent (client = client , context_providers = [provider ])
12351263
1236- response = await agent .run ("Use the sandbox to add 20 and 22." )
1264+ try :
1265+ response = await agent .run ("Use the sandbox to add 20 and 22." )
12371266
1238- assert response .text == "The sandbox returned 42."
1239- assert client .call_count == 2
1267+ assert response .text == "The sandbox returned 42."
1268+ assert client .call_count == 2
1269+ finally :
1270+ _close_provider_registry (provider )
12401271 assert len (_FakeSandbox .instances ) == 1
12411272 assert "compute" in _FakeSandbox .instances [0 ].registered_tools
12421273
@@ -1250,10 +1281,13 @@ async def test_agent_runs_hyperlight_codeact_end_to_end_with_real_sandbox() -> N
12501281 provider = HyperlightCodeActProvider (tools = [compute ])
12511282 agent = Agent (client = client , context_providers = [provider ])
12521283
1253- response = await agent .run ("Use the sandbox to add 20 and 22." )
1284+ try :
1285+ response = await agent .run ("Use the sandbox to add 20 and 22." )
12541286
1255- assert response .text == "The sandbox returned 42."
1256- assert client .call_count == 2
1287+ assert response .text == "The sandbox returned 42."
1288+ assert client .call_count == 2
1289+ finally :
1290+ _close_provider_registry (provider )
12571291
12581292
12591293@pytest .mark .integration
@@ -1272,41 +1306,44 @@ async def test_provider_run_tool_writes_files_with_real_sandbox(tmp_path: Path)
12721306 run_tool = context .tools [0 ][1 ][0 ]
12731307 assert isinstance (run_tool , HyperlightExecuteCodeTool )
12741308
1275- result = await run_tool .invoke (
1276- arguments = {
1277- "code" : (
1278- 'payload = "hello from sandbox"\n '
1279- "output_path = None\n "
1280- 'for candidate in ("/output/result.txt",):\n '
1281- " try:\n "
1282- ' with open(candidate, "w", encoding="utf-8") as f:\n '
1283- " f.write(payload)\n "
1284- " except OSError:\n "
1285- " continue\n "
1286- " output_path = candidate\n "
1287- " break\n "
1288- 'assert output_path is not None, "output path unavailable"\n '
1289- 'print("validated")\n '
1290- )
1291- }
1292- )
1293-
1294- outputs = result
1295- error_outputs = [
1296- f"{ item .message } : { item .error_details } "
1297- for item in outputs
1298- if item .type == "error" and item .error_details is not None
1299- ]
1300- assert not error_outputs , error_outputs
1301-
1302- text_output = next ((item for item in outputs if item .type == "text" and item .text is not None ), None )
1303- if text_output is not None :
1304- assert text_output .text == "validated\n "
1309+ try :
1310+ result = await run_tool .invoke (
1311+ arguments = {
1312+ "code" : (
1313+ 'payload = "hello from sandbox"\n '
1314+ "output_path = None\n "
1315+ 'for candidate in ("/output/result.txt",):\n '
1316+ " try:\n "
1317+ ' with open(candidate, "w", encoding="utf-8") as f:\n '
1318+ " f.write(payload)\n "
1319+ " except OSError:\n "
1320+ " continue\n "
1321+ " output_path = candidate\n "
1322+ " break\n "
1323+ 'assert output_path is not None, "output path unavailable"\n '
1324+ 'print("validated")\n '
1325+ )
1326+ }
1327+ )
13051328
1306- file_output = next ((item for item in outputs if item .type == "data" ), None )
1307- if file_output is not None :
1308- assert file_output .uri is not None and file_output .uri .startswith ("data:" )
1309- assert file_output .additional_properties ["path" ] in {"/output/result.txt" , "/output/output/result.txt" }
1329+ outputs = result
1330+ error_outputs = [
1331+ f"{ item .message } : { item .error_details } "
1332+ for item in outputs
1333+ if item .type == "error" and item .error_details is not None
1334+ ]
1335+ assert not error_outputs , error_outputs
1336+
1337+ text_output = next ((item for item in outputs if item .type == "text" and item .text is not None ), None )
1338+ if text_output is not None :
1339+ assert text_output .text == "validated\n "
1340+
1341+ file_output = next ((item for item in outputs if item .type == "data" ), None )
1342+ if file_output is not None :
1343+ assert file_output .uri is not None and file_output .uri .startswith ("data:" )
1344+ assert file_output .additional_properties ["path" ] in {"/output/result.txt" , "/output/output/result.txt" }
1345+ finally :
1346+ _close_execute_code_registry (run_tool )
13101347
13111348
13121349@pytest .mark .integration
@@ -1325,46 +1362,49 @@ async def test_provider_run_tool_pings_bing_with_real_sandbox() -> None:
13251362 run_tool = context .tools [0 ][1 ][0 ]
13261363 assert isinstance (run_tool , HyperlightExecuteCodeTool )
13271364
1328- result = await run_tool .invoke (
1329- arguments = {
1330- "code" : (
1331- "import _socket\n \n "
1332- 'addresses = _socket.getaddrinfo("bing.com", 80, _socket.AF_INET, _socket.SOCK_STREAM)\n '
1333- 'assert addresses, "bing.com did not resolve"\n '
1334- "last_error = None\n "
1335- "for family, socktype, proto, _, sockaddr in addresses:\n "
1336- " connection = None\n "
1337- " try:\n "
1338- " connection = _socket.socket(family, socktype, proto)\n "
1339- " connection.settimeout(10)\n "
1340- " connection.connect(sockaddr)\n "
1341- ' print("pinged bing.com")\n '
1342- " break\n "
1343- " except OSError as exc:\n "
1344- " last_error = exc\n "
1345- " finally:\n "
1346- " if connection is not None:\n "
1347- " try:\n "
1348- " connection.close()\n "
1349- " except OSError:\n "
1350- " pass\n "
1351- "else:\n "
1352- ' raise last_error or RuntimeError("unable to reach bing.com")\n '
1353- )
1354- }
1355- )
1356-
1357- outputs = result
1358- error_outputs = [
1359- f"{ item .message } : { item .error_details } "
1360- for item in outputs
1361- if item .type == "error" and item .error_details is not None
1362- ]
1363- assert not error_outputs , error_outputs
1365+ try :
1366+ result = await run_tool .invoke (
1367+ arguments = {
1368+ "code" : (
1369+ "import _socket\n \n "
1370+ 'addresses = _socket.getaddrinfo("bing.com", 80, _socket.AF_INET, _socket.SOCK_STREAM)\n '
1371+ 'assert addresses, "bing.com did not resolve"\n '
1372+ "last_error = None\n "
1373+ "for family, socktype, proto, _, sockaddr in addresses:\n "
1374+ " connection = None\n "
1375+ " try:\n "
1376+ " connection = _socket.socket(family, socktype, proto)\n "
1377+ " connection.settimeout(10)\n "
1378+ " connection.connect(sockaddr)\n "
1379+ ' print("pinged bing.com")\n '
1380+ " break\n "
1381+ " except OSError as exc:\n "
1382+ " last_error = exc\n "
1383+ " finally:\n "
1384+ " if connection is not None:\n "
1385+ " try:\n "
1386+ " connection.close()\n "
1387+ " except OSError:\n "
1388+ " pass\n "
1389+ "else:\n "
1390+ ' raise last_error or RuntimeError("unable to reach bing.com")\n '
1391+ )
1392+ }
1393+ )
13641394
1365- text_output = next ((item for item in outputs if item .type == "text" and item .text is not None ), None )
1366- if text_output is not None :
1367- assert text_output .text == "pinged bing.com\n "
1395+ outputs = result
1396+ error_outputs = [
1397+ f"{ item .message } : { item .error_details } "
1398+ for item in outputs
1399+ if item .type == "error" and item .error_details is not None
1400+ ]
1401+ assert not error_outputs , error_outputs
1402+
1403+ text_output = next ((item for item in outputs if item .type == "text" and item .text is not None ), None )
1404+ if text_output is not None :
1405+ assert text_output .text == "pinged bing.com\n "
1406+ finally :
1407+ _close_execute_code_registry (run_tool )
13681408
13691409
13701410# ---------------------------------------------------------------------------
@@ -1475,25 +1515,29 @@ async def test_output_dir_cleared_between_invocations() -> None:
14751515 run_tool = context .tools [0 ][1 ][0 ]
14761516 assert isinstance (run_tool , HyperlightExecuteCodeTool )
14771517
1478- # First invocation: write a file
1479- result1 = await run_tool .invoke (
1480- arguments = {"code" : ('with open("/output/stale.txt", "w") as f:\n f.write("first")\n print("wrote")\n ' )}
1481- )
1482- assert result1 [0 ].type == "text" or result1 [0 ].type == "data"
1483- outputs1 = result1
1484- assert any (
1485- item .type == "data" and "stale.txt" in (item .additional_properties or {}).get ("path" , "" ) for item in outputs1
1486- ), "First invocation should produce stale.txt"
1487-
1488- # Second invocation: no file writes
1489- result2 = await run_tool .invoke (arguments = {"code" : 'print("clean")\n ' })
1490- outputs2 = result2
1491- stale_files = [
1492- item
1493- for item in outputs2
1494- if item .type == "data" and "stale.txt" in (item .additional_properties or {}).get ("path" , "" )
1495- ]
1496- assert not stale_files , "Stale output file leaked into second invocation"
1518+ try :
1519+ # First invocation: write a file
1520+ result1 = await run_tool .invoke (
1521+ arguments = {"code" : ('with open("/output/stale.txt", "w") as f:\n f.write("first")\n print("wrote")\n ' )}
1522+ )
1523+ assert result1 [0 ].type == "text" or result1 [0 ].type == "data"
1524+ outputs1 = result1
1525+ assert any (
1526+ item .type == "data" and "stale.txt" in (item .additional_properties or {}).get ("path" , "" )
1527+ for item in outputs1
1528+ ), "First invocation should produce stale.txt"
1529+
1530+ # Second invocation: no file writes
1531+ result2 = await run_tool .invoke (arguments = {"code" : 'print("clean")\n ' })
1532+ outputs2 = result2
1533+ stale_files = [
1534+ item
1535+ for item in outputs2
1536+ if item .type == "data" and "stale.txt" in (item .additional_properties or {}).get ("path" , "" )
1537+ ]
1538+ assert not stale_files , "Stale output file leaked into second invocation"
1539+ finally :
1540+ _close_execute_code_registry (run_tool )
14971541
14981542
14991543@pytest .mark .integration
@@ -1532,12 +1576,16 @@ async def _concurrent_task():
15321576 concurrent_ran = True
15331577 release .set ()
15341578
1535- code_task = asyncio .create_task (run_tool .invoke (arguments = {"code" : 'print("done")\n ' }))
1536- await _concurrent_task ()
1537- result = await code_task
1579+ try :
1580+ code_task = asyncio .create_task (run_tool .invoke (arguments = {"code" : 'print("done")\n ' }))
1581+ await _concurrent_task ()
1582+ result = await code_task
15381583
1539- assert concurrent_ran , "Event loop was blocked during sandbox execution"
1540- assert result [0 ].type == "text"
1584+ assert concurrent_ran , "Event loop was blocked during sandbox execution"
1585+ assert result [0 ].type == "text"
1586+ finally :
1587+ release .set ()
1588+ _close_execute_code_registry (run_tool )
15411589
15421590
15431591class _ThreadAffinityFakeSandbox (_FakeSandbox ):
0 commit comments