diff --git a/src/mcqemu/tools/sandbox.py b/src/mcqemu/tools/sandbox.py index eda179e..017e3cb 100644 --- a/src/mcqemu/tools/sandbox.py +++ b/src/mcqemu/tools/sandbox.py @@ -25,11 +25,19 @@ async def _agent_responding(record) -> bool: return False -def _auto_name(taken: list[str]) -> str: - if "sandbox" not in taken: +def _auto_name(registry) -> str: + """First 'sandbox[-N]' name that is unregistered or belongs to a dead VM + (launch_vm reclaims dead names, so reusing them is safe and keeps names + predictable).""" + + def usable(candidate: str) -> bool: + record = registry.get(candidate) + return record is None or not registry.is_process_alive(record) + + if usable("sandbox"): return "sandbox" i = 2 - while f"sandbox-{i}" in taken: + while not usable(f"sandbox-{i}"): i += 1 return f"sandbox-{i}" @@ -57,7 +65,7 @@ async def sandbox_vm( raise ToolError(f"Bad base image: {e}") from e if name is None: - name = _auto_name(state.registry.names()) + name = _auto_name(state.registry) elif not valid_vm_name(name): raise ToolError( f"Invalid VM name {name!r}: use letters, digits, '.', '_', '-' (max 48 chars)." diff --git a/tests/test_sandbox.py b/tests/test_sandbox.py index 824209e..0dafb8b 100644 --- a/tests/test_sandbox.py +++ b/tests/test_sandbox.py @@ -99,13 +99,24 @@ async def test_sandbox_vm_creates_overlay_and_waits(dirs, base_image, fake_launc assert data["overlay"] == str(overlay) -async def test_sandbox_auto_names_avoid_collisions(dirs, base_image, fake_launch, agent_up): +async def test_sandbox_auto_names_avoid_live_collisions( + dirs, base_image, fake_launch, agent_up, all_pids_alive +): write_registry(dirs, seeded_record(dirs, "sandbox")) async with Client(mcp) as client: data = result_data(await client.call_tool("sandbox_vm", {"base_image": str(base_image)})) assert data["name"] == "sandbox-2" +async def test_sandbox_auto_name_reuses_dead( + dirs, base_image, fake_launch, agent_up, all_pids_dead +): + write_registry(dirs, seeded_record(dirs, "sandbox")) + async with Client(mcp) as client: + data = result_data(await client.call_tool("sandbox_vm", {"base_image": str(base_image)})) + assert data["name"] == "sandbox" + + async def test_sandbox_agent_timeout_reports_hint(dirs, base_image, fake_launch, monkeypatch): async def never(record): return False