Pivot persistent APIs to workspaces
Replace the public persistent-sandbox contract with workspace-first naming across CLI, SDK, MCP, payloads, and on-disk state. Rename the task surface to workspace equivalents, switch create-time seeding to `seed_path`, and store records under `workspaces/<workspace_id>/workspace.json` without carrying legacy task aliases or migrating old local task state. Keep `pyro run` and `vm_*` unchanged. Validation covered `uv lock`, focused public-contract/API/CLI/manager tests, `UV_CACHE_DIR=.uv-cache make check`, and `UV_CACHE_DIR=.uv-cache make dist-check`.
This commit is contained in:
parent
f57454bcb4
commit
48b82d8386
13 changed files with 743 additions and 618 deletions
|
|
@ -48,8 +48,8 @@ def test_pyro_create_server_registers_vm_run(tmp_path: Path) -> None:
|
|||
tool_names = asyncio.run(_run())
|
||||
assert "vm_run" in tool_names
|
||||
assert "vm_create" in tool_names
|
||||
assert "task_create" in tool_names
|
||||
assert "task_sync_push" in tool_names
|
||||
assert "workspace_create" in tool_names
|
||||
assert "workspace_sync_push" in tool_names
|
||||
|
||||
|
||||
def test_pyro_vm_run_tool_executes(tmp_path: Path) -> None:
|
||||
|
|
@ -106,7 +106,7 @@ def test_pyro_create_vm_defaults_sizing_and_host_compat(tmp_path: Path) -> None:
|
|||
assert created["allow_host_compat"] is True
|
||||
|
||||
|
||||
def test_pyro_task_methods_delegate_to_manager(tmp_path: Path) -> None:
|
||||
def test_pyro_workspace_methods_delegate_to_manager(tmp_path: Path) -> None:
|
||||
pyro = Pyro(
|
||||
manager=VmManager(
|
||||
backend_name="mock",
|
||||
|
|
@ -119,20 +119,20 @@ def test_pyro_task_methods_delegate_to_manager(tmp_path: Path) -> None:
|
|||
source_dir.mkdir()
|
||||
(source_dir / "note.txt").write_text("ok\n", encoding="utf-8")
|
||||
|
||||
created = pyro.create_task(
|
||||
created = pyro.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
source_path=source_dir,
|
||||
seed_path=source_dir,
|
||||
)
|
||||
task_id = str(created["task_id"])
|
||||
workspace_id = str(created["workspace_id"])
|
||||
updated_dir = tmp_path / "updated"
|
||||
updated_dir.mkdir()
|
||||
(updated_dir / "more.txt").write_text("more\n", encoding="utf-8")
|
||||
synced = pyro.push_task_sync(task_id, updated_dir, dest="subdir")
|
||||
executed = pyro.exec_task(task_id, command="cat note.txt")
|
||||
status = pyro.status_task(task_id)
|
||||
logs = pyro.logs_task(task_id)
|
||||
deleted = pyro.delete_task(task_id)
|
||||
synced = pyro.push_workspace_sync(workspace_id, updated_dir, dest="subdir")
|
||||
executed = pyro.exec_workspace(workspace_id, command="cat note.txt")
|
||||
status = pyro.status_workspace(workspace_id)
|
||||
logs = pyro.logs_workspace(workspace_id)
|
||||
deleted = pyro.delete_workspace(workspace_id)
|
||||
|
||||
assert executed["stdout"] == "ok\n"
|
||||
assert created["workspace_seed"]["mode"] == "directory"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ def test_cli_help_guides_first_run() -> None:
|
|||
assert "pyro env list" in help_text
|
||||
assert "pyro env pull debian:12" in help_text
|
||||
assert "pyro run debian:12 -- git --version" in help_text
|
||||
assert "pyro task sync push TASK_ID ./changes" in help_text
|
||||
assert "pyro workspace sync push WORKSPACE_ID ./changes" in help_text
|
||||
assert "Use `pyro mcp serve` only after the CLI validation path works." in help_text
|
||||
|
||||
|
||||
|
|
@ -60,28 +60,37 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None:
|
|||
assert "Expose pyro tools over stdio for an MCP client." in mcp_help
|
||||
assert "Use this from an MCP client config after the CLI evaluation path works." in mcp_help
|
||||
|
||||
task_help = _subparser_choice(parser, "task").format_help()
|
||||
assert "pyro task create debian:12 --source-path ./repo" in task_help
|
||||
assert "pyro task sync push TASK_ID ./repo --dest src" in task_help
|
||||
assert "pyro task exec TASK_ID" in task_help
|
||||
workspace_help = _subparser_choice(parser, "workspace").format_help()
|
||||
assert "pyro workspace create debian:12 --seed-path ./repo" in workspace_help
|
||||
assert "pyro workspace sync push WORKSPACE_ID ./repo --dest src" in workspace_help
|
||||
assert "pyro workspace exec WORKSPACE_ID" in workspace_help
|
||||
|
||||
task_create_help = _subparser_choice(_subparser_choice(parser, "task"), "create").format_help()
|
||||
assert "--source-path" in task_create_help
|
||||
assert "seed into `/workspace`" in task_create_help
|
||||
|
||||
task_exec_help = _subparser_choice(_subparser_choice(parser, "task"), "exec").format_help()
|
||||
assert "persistent `/workspace`" in task_exec_help
|
||||
assert "pyro task exec TASK_ID -- cat note.txt" in task_exec_help
|
||||
|
||||
task_sync_help = _subparser_choice(_subparser_choice(parser, "task"), "sync").format_help()
|
||||
assert "Sync is non-atomic." in task_sync_help
|
||||
assert "pyro task sync push TASK_ID ./repo" in task_sync_help
|
||||
|
||||
task_sync_push_help = _subparser_choice(
|
||||
_subparser_choice(_subparser_choice(parser, "task"), "sync"), "push"
|
||||
workspace_create_help = _subparser_choice(
|
||||
_subparser_choice(parser, "workspace"),
|
||||
"create",
|
||||
).format_help()
|
||||
assert "--dest" in task_sync_push_help
|
||||
assert "Import host content into `/workspace`" in task_sync_push_help
|
||||
assert "--seed-path" in workspace_create_help
|
||||
assert "seed into `/workspace`" in workspace_create_help
|
||||
|
||||
workspace_exec_help = _subparser_choice(
|
||||
_subparser_choice(parser, "workspace"),
|
||||
"exec",
|
||||
).format_help()
|
||||
assert "persistent `/workspace`" in workspace_exec_help
|
||||
assert "pyro workspace exec WORKSPACE_ID -- cat note.txt" in workspace_exec_help
|
||||
|
||||
workspace_sync_help = _subparser_choice(
|
||||
_subparser_choice(parser, "workspace"),
|
||||
"sync",
|
||||
).format_help()
|
||||
assert "Sync is non-atomic." in workspace_sync_help
|
||||
assert "pyro workspace sync push WORKSPACE_ID ./repo" in workspace_sync_help
|
||||
|
||||
workspace_sync_push_help = _subparser_choice(
|
||||
_subparser_choice(_subparser_choice(parser, "workspace"), "sync"), "push"
|
||||
).format_help()
|
||||
assert "--dest" in workspace_sync_push_help
|
||||
assert "Import host content into `/workspace`" in workspace_sync_push_help
|
||||
|
||||
|
||||
def test_cli_run_prints_json(
|
||||
|
|
@ -344,32 +353,32 @@ def test_cli_requires_run_command() -> None:
|
|||
|
||||
def test_cli_requires_command_preserves_shell_argument_boundaries() -> None:
|
||||
command = cli._require_command(
|
||||
["--", "sh", "-lc", 'printf "hello from task\\n" > note.txt']
|
||||
["--", "sh", "-lc", 'printf "hello from workspace\\n" > note.txt']
|
||||
)
|
||||
assert command == 'sh -lc \'printf "hello from task\\n" > note.txt\''
|
||||
assert command == 'sh -lc \'printf "hello from workspace\\n" > note.txt\''
|
||||
|
||||
|
||||
def test_cli_task_create_prints_json(
|
||||
def test_cli_workspace_create_prints_json(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def create_task(self, **kwargs: Any) -> dict[str, Any]:
|
||||
def create_workspace(self, **kwargs: Any) -> dict[str, Any]:
|
||||
assert kwargs["environment"] == "debian:12"
|
||||
assert kwargs["source_path"] == "./repo"
|
||||
return {"task_id": "task-123", "state": "started"}
|
||||
assert kwargs["seed_path"] == "./repo"
|
||||
return {"workspace_id": "workspace-123", "state": "started"}
|
||||
|
||||
class StubParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="create",
|
||||
command="workspace",
|
||||
workspace_command="create",
|
||||
environment="debian:12",
|
||||
vcpu_count=1,
|
||||
mem_mib=1024,
|
||||
ttl_seconds=600,
|
||||
network=False,
|
||||
allow_host_compat=False,
|
||||
source_path="./repo",
|
||||
seed_path="./repo",
|
||||
json=True,
|
||||
)
|
||||
|
||||
|
|
@ -377,23 +386,23 @@ def test_cli_task_create_prints_json(
|
|||
monkeypatch.setattr(cli, "Pyro", StubPyro)
|
||||
cli.main()
|
||||
output = json.loads(capsys.readouterr().out)
|
||||
assert output["task_id"] == "task-123"
|
||||
assert output["workspace_id"] == "workspace-123"
|
||||
|
||||
|
||||
def test_cli_task_create_prints_human(
|
||||
def test_cli_workspace_create_prints_human(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def create_task(self, **kwargs: Any) -> dict[str, Any]:
|
||||
def create_workspace(self, **kwargs: Any) -> dict[str, Any]:
|
||||
del kwargs
|
||||
return {
|
||||
"task_id": "task-123",
|
||||
"workspace_id": "workspace-123",
|
||||
"environment": "debian:12",
|
||||
"state": "started",
|
||||
"workspace_path": "/workspace",
|
||||
"workspace_seed": {
|
||||
"mode": "directory",
|
||||
"source_path": "/tmp/repo",
|
||||
"seed_path": "/tmp/repo",
|
||||
"destination": "/workspace",
|
||||
"entry_count": 1,
|
||||
"bytes_written": 6,
|
||||
|
|
@ -408,15 +417,15 @@ def test_cli_task_create_prints_human(
|
|||
class StubParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="create",
|
||||
command="workspace",
|
||||
workspace_command="create",
|
||||
environment="debian:12",
|
||||
vcpu_count=1,
|
||||
mem_mib=1024,
|
||||
ttl_seconds=600,
|
||||
network=False,
|
||||
allow_host_compat=False,
|
||||
source_path="/tmp/repo",
|
||||
seed_path="/tmp/repo",
|
||||
json=False,
|
||||
)
|
||||
|
||||
|
|
@ -424,22 +433,28 @@ def test_cli_task_create_prints_human(
|
|||
monkeypatch.setattr(cli, "Pyro", StubPyro)
|
||||
cli.main()
|
||||
output = capsys.readouterr().out
|
||||
assert "Task: task-123" in output
|
||||
assert "Workspace ID: workspace-123" in output
|
||||
assert "Workspace: /workspace" in output
|
||||
assert "Workspace seed: directory from /tmp/repo" in output
|
||||
|
||||
|
||||
def test_cli_task_exec_prints_human_output(
|
||||
def test_cli_workspace_exec_prints_human_output(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def exec_task(self, task_id: str, *, command: str, timeout_seconds: int) -> dict[str, Any]:
|
||||
assert task_id == "task-123"
|
||||
def exec_workspace(
|
||||
self,
|
||||
workspace_id: str,
|
||||
*,
|
||||
command: str,
|
||||
timeout_seconds: int,
|
||||
) -> dict[str, Any]:
|
||||
assert workspace_id == "workspace-123"
|
||||
assert command == "cat note.txt"
|
||||
assert timeout_seconds == 30
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"workspace_id": workspace_id,
|
||||
"sequence": 2,
|
||||
"cwd": "/workspace",
|
||||
"execution_mode": "guest_vsock",
|
||||
|
|
@ -452,9 +467,9 @@ def test_cli_task_exec_prints_human_output(
|
|||
class StubParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="exec",
|
||||
task_id="task-123",
|
||||
command="workspace",
|
||||
workspace_command="exec",
|
||||
workspace_id="workspace-123",
|
||||
timeout_seconds=30,
|
||||
json=False,
|
||||
command_args=["--", "cat", "note.txt"],
|
||||
|
|
@ -465,19 +480,28 @@ def test_cli_task_exec_prints_human_output(
|
|||
cli.main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
assert "[task-exec] task_id=task-123 sequence=2 cwd=/workspace" in captured.err
|
||||
assert (
|
||||
"[workspace-exec] workspace_id=workspace-123 sequence=2 cwd=/workspace"
|
||||
in captured.err
|
||||
)
|
||||
|
||||
|
||||
def test_cli_task_sync_push_prints_json(
|
||||
def test_cli_workspace_sync_push_prints_json(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def push_task_sync(self, task_id: str, source_path: str, *, dest: str) -> dict[str, Any]:
|
||||
assert task_id == "task-123"
|
||||
def push_workspace_sync(
|
||||
self,
|
||||
workspace_id: str,
|
||||
source_path: str,
|
||||
*,
|
||||
dest: str,
|
||||
) -> dict[str, Any]:
|
||||
assert workspace_id == "workspace-123"
|
||||
assert source_path == "./repo"
|
||||
assert dest == "src"
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"workspace_id": workspace_id,
|
||||
"execution_mode": "guest_vsock",
|
||||
"workspace_sync": {
|
||||
"mode": "directory",
|
||||
|
|
@ -491,10 +515,10 @@ def test_cli_task_sync_push_prints_json(
|
|||
class StubParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="sync",
|
||||
task_sync_command="push",
|
||||
task_id="task-123",
|
||||
command="workspace",
|
||||
workspace_command="sync",
|
||||
workspace_sync_command="push",
|
||||
workspace_id="workspace-123",
|
||||
source_path="./repo",
|
||||
dest="src",
|
||||
json=True,
|
||||
|
|
@ -507,17 +531,23 @@ def test_cli_task_sync_push_prints_json(
|
|||
assert output["workspace_sync"]["destination"] == "/workspace/src"
|
||||
|
||||
|
||||
def test_cli_task_sync_push_prints_human(
|
||||
def test_cli_workspace_sync_push_prints_human(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def push_task_sync(self, task_id: str, source_path: str, *, dest: str) -> dict[str, Any]:
|
||||
assert task_id == "task-123"
|
||||
def push_workspace_sync(
|
||||
self,
|
||||
workspace_id: str,
|
||||
source_path: str,
|
||||
*,
|
||||
dest: str,
|
||||
) -> dict[str, Any]:
|
||||
assert workspace_id == "workspace-123"
|
||||
assert source_path == "./repo"
|
||||
assert dest == "/workspace"
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"workspace_id": workspace_id,
|
||||
"execution_mode": "guest_vsock",
|
||||
"workspace_sync": {
|
||||
"mode": "directory",
|
||||
|
|
@ -531,10 +561,10 @@ def test_cli_task_sync_push_prints_human(
|
|||
class StubParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="sync",
|
||||
task_sync_command="push",
|
||||
task_id="task-123",
|
||||
command="workspace",
|
||||
workspace_command="sync",
|
||||
workspace_sync_command="push",
|
||||
workspace_id="workspace-123",
|
||||
source_path="./repo",
|
||||
dest="/workspace",
|
||||
json=False,
|
||||
|
|
@ -544,22 +574,22 @@ def test_cli_task_sync_push_prints_human(
|
|||
monkeypatch.setattr(cli, "Pyro", StubPyro)
|
||||
cli.main()
|
||||
output = capsys.readouterr().out
|
||||
assert "[task-sync] task_id=task-123 mode=directory source=/tmp/repo" in output
|
||||
assert "[workspace-sync] workspace_id=workspace-123 mode=directory source=/tmp/repo" in output
|
||||
assert (
|
||||
"destination=/workspace entry_count=2 bytes_written=12 "
|
||||
"execution_mode=guest_vsock"
|
||||
) in output
|
||||
|
||||
|
||||
def test_cli_task_logs_and_delete_print_human(
|
||||
def test_cli_workspace_logs_and_delete_print_human(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def logs_task(self, task_id: str) -> dict[str, Any]:
|
||||
assert task_id == "task-123"
|
||||
def logs_workspace(self, workspace_id: str) -> dict[str, Any]:
|
||||
assert workspace_id == "workspace-123"
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"workspace_id": workspace_id,
|
||||
"count": 1,
|
||||
"entries": [
|
||||
{
|
||||
|
|
@ -574,16 +604,16 @@ def test_cli_task_logs_and_delete_print_human(
|
|||
],
|
||||
}
|
||||
|
||||
def delete_task(self, task_id: str) -> dict[str, Any]:
|
||||
assert task_id == "task-123"
|
||||
return {"task_id": task_id, "deleted": True}
|
||||
def delete_workspace(self, workspace_id: str) -> dict[str, Any]:
|
||||
assert workspace_id == "workspace-123"
|
||||
return {"workspace_id": workspace_id, "deleted": True}
|
||||
|
||||
class LogsParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="logs",
|
||||
task_id="task-123",
|
||||
command="workspace",
|
||||
workspace_command="logs",
|
||||
workspace_id="workspace-123",
|
||||
json=False,
|
||||
)
|
||||
|
||||
|
|
@ -594,9 +624,9 @@ def test_cli_task_logs_and_delete_print_human(
|
|||
class DeleteParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="delete",
|
||||
task_id="task-123",
|
||||
command="workspace",
|
||||
workspace_command="delete",
|
||||
workspace_id="workspace-123",
|
||||
json=False,
|
||||
)
|
||||
|
||||
|
|
@ -605,28 +635,28 @@ def test_cli_task_logs_and_delete_print_human(
|
|||
|
||||
output = capsys.readouterr().out
|
||||
assert "#1 exit_code=0 duration_ms=2 cwd=/workspace" in output
|
||||
assert "Deleted task: task-123" in output
|
||||
assert "Deleted workspace: workspace-123" in output
|
||||
|
||||
|
||||
def test_cli_task_status_and_delete_print_json(
|
||||
def test_cli_workspace_status_and_delete_print_json(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def status_task(self, task_id: str) -> dict[str, Any]:
|
||||
assert task_id == "task-123"
|
||||
return {"task_id": task_id, "state": "started"}
|
||||
def status_workspace(self, workspace_id: str) -> dict[str, Any]:
|
||||
assert workspace_id == "workspace-123"
|
||||
return {"workspace_id": workspace_id, "state": "started"}
|
||||
|
||||
def delete_task(self, task_id: str) -> dict[str, Any]:
|
||||
assert task_id == "task-123"
|
||||
return {"task_id": task_id, "deleted": True}
|
||||
def delete_workspace(self, workspace_id: str) -> dict[str, Any]:
|
||||
assert workspace_id == "workspace-123"
|
||||
return {"workspace_id": workspace_id, "deleted": True}
|
||||
|
||||
class StatusParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="status",
|
||||
task_id="task-123",
|
||||
command="workspace",
|
||||
workspace_command="status",
|
||||
workspace_id="workspace-123",
|
||||
json=True,
|
||||
)
|
||||
|
||||
|
|
@ -639,9 +669,9 @@ def test_cli_task_status_and_delete_print_json(
|
|||
class DeleteParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="delete",
|
||||
task_id="task-123",
|
||||
command="workspace",
|
||||
workspace_command="delete",
|
||||
workspace_id="workspace-123",
|
||||
json=True,
|
||||
)
|
||||
|
||||
|
|
@ -651,20 +681,26 @@ def test_cli_task_status_and_delete_print_json(
|
|||
assert deleted["deleted"] is True
|
||||
|
||||
|
||||
def test_cli_task_exec_json_error_exits_nonzero(
|
||||
def test_cli_workspace_exec_json_error_exits_nonzero(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def exec_task(self, task_id: str, *, command: str, timeout_seconds: int) -> dict[str, Any]:
|
||||
del task_id, command, timeout_seconds
|
||||
raise RuntimeError("task is unavailable")
|
||||
def exec_workspace(
|
||||
self,
|
||||
workspace_id: str,
|
||||
*,
|
||||
command: str,
|
||||
timeout_seconds: int,
|
||||
) -> dict[str, Any]:
|
||||
del workspace_id, command, timeout_seconds
|
||||
raise RuntimeError("workspace is unavailable")
|
||||
|
||||
class StubParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="task",
|
||||
task_command="exec",
|
||||
task_id="task-123",
|
||||
command="workspace",
|
||||
workspace_command="exec",
|
||||
workspace_id="workspace-123",
|
||||
timeout_seconds=30,
|
||||
json=True,
|
||||
command_args=["--", "true"],
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ from pyro_mcp.contract import (
|
|||
PUBLIC_CLI_DEMO_SUBCOMMANDS,
|
||||
PUBLIC_CLI_ENV_SUBCOMMANDS,
|
||||
PUBLIC_CLI_RUN_FLAGS,
|
||||
PUBLIC_CLI_TASK_CREATE_FLAGS,
|
||||
PUBLIC_CLI_TASK_SUBCOMMANDS,
|
||||
PUBLIC_CLI_TASK_SYNC_PUSH_FLAGS,
|
||||
PUBLIC_CLI_TASK_SYNC_SUBCOMMANDS,
|
||||
PUBLIC_CLI_WORKSPACE_CREATE_FLAGS,
|
||||
PUBLIC_CLI_WORKSPACE_SUBCOMMANDS,
|
||||
PUBLIC_CLI_WORKSPACE_SYNC_PUSH_FLAGS,
|
||||
PUBLIC_CLI_WORKSPACE_SYNC_SUBCOMMANDS,
|
||||
PUBLIC_MCP_TOOLS,
|
||||
PUBLIC_SDK_METHODS,
|
||||
)
|
||||
|
|
@ -67,22 +67,25 @@ def test_public_cli_help_lists_commands_and_run_flags() -> None:
|
|||
for subcommand_name in PUBLIC_CLI_ENV_SUBCOMMANDS:
|
||||
assert subcommand_name in env_help_text
|
||||
|
||||
task_help_text = _subparser_choice(parser, "task").format_help()
|
||||
for subcommand_name in PUBLIC_CLI_TASK_SUBCOMMANDS:
|
||||
assert subcommand_name in task_help_text
|
||||
task_create_help_text = _subparser_choice(
|
||||
_subparser_choice(parser, "task"), "create"
|
||||
workspace_help_text = _subparser_choice(parser, "workspace").format_help()
|
||||
for subcommand_name in PUBLIC_CLI_WORKSPACE_SUBCOMMANDS:
|
||||
assert subcommand_name in workspace_help_text
|
||||
workspace_create_help_text = _subparser_choice(
|
||||
_subparser_choice(parser, "workspace"), "create"
|
||||
).format_help()
|
||||
for flag in PUBLIC_CLI_TASK_CREATE_FLAGS:
|
||||
assert flag in task_create_help_text
|
||||
task_sync_help_text = _subparser_choice(_subparser_choice(parser, "task"), "sync").format_help()
|
||||
for subcommand_name in PUBLIC_CLI_TASK_SYNC_SUBCOMMANDS:
|
||||
assert subcommand_name in task_sync_help_text
|
||||
task_sync_push_help_text = _subparser_choice(
|
||||
_subparser_choice(_subparser_choice(parser, "task"), "sync"), "push"
|
||||
for flag in PUBLIC_CLI_WORKSPACE_CREATE_FLAGS:
|
||||
assert flag in workspace_create_help_text
|
||||
workspace_sync_help_text = _subparser_choice(
|
||||
_subparser_choice(parser, "workspace"),
|
||||
"sync",
|
||||
).format_help()
|
||||
for flag in PUBLIC_CLI_TASK_SYNC_PUSH_FLAGS:
|
||||
assert flag in task_sync_push_help_text
|
||||
for subcommand_name in PUBLIC_CLI_WORKSPACE_SYNC_SUBCOMMANDS:
|
||||
assert subcommand_name in workspace_sync_help_text
|
||||
workspace_sync_push_help_text = _subparser_choice(
|
||||
_subparser_choice(_subparser_choice(parser, "workspace"), "sync"), "push"
|
||||
).format_help()
|
||||
for flag in PUBLIC_CLI_WORKSPACE_SYNC_PUSH_FLAGS:
|
||||
assert flag in workspace_sync_push_help_text
|
||||
|
||||
demo_help_text = _subparser_choice(parser, "demo").format_help()
|
||||
for subcommand_name in PUBLIC_CLI_DEMO_SUBCOMMANDS:
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ def test_create_server_registers_vm_tools(tmp_path: Path) -> None:
|
|||
assert "vm_network_info" in tool_names
|
||||
assert "vm_run" in tool_names
|
||||
assert "vm_status" in tool_names
|
||||
assert "task_create" in tool_names
|
||||
assert "task_logs" in tool_names
|
||||
assert "task_sync_push" in tool_names
|
||||
assert "workspace_create" in tool_names
|
||||
assert "workspace_logs" in tool_names
|
||||
assert "workspace_sync_push" in tool_names
|
||||
|
||||
|
||||
def test_vm_run_round_trip(tmp_path: Path) -> None:
|
||||
|
|
@ -166,7 +166,7 @@ def test_server_main_runs_stdio_transport(monkeypatch: pytest.MonkeyPatch) -> No
|
|||
assert called == {"transport": "stdio"}
|
||||
|
||||
|
||||
def test_task_tools_round_trip(tmp_path: Path) -> None:
|
||||
def test_workspace_tools_round_trip(tmp_path: Path) -> None:
|
||||
manager = VmManager(
|
||||
backend_name="mock",
|
||||
base_dir=tmp_path / "vms",
|
||||
|
|
@ -194,23 +194,23 @@ def test_task_tools_round_trip(tmp_path: Path) -> None:
|
|||
server = create_server(manager=manager)
|
||||
created = _extract_structured(
|
||||
await server.call_tool(
|
||||
"task_create",
|
||||
"workspace_create",
|
||||
{
|
||||
"environment": "debian:12-base",
|
||||
"allow_host_compat": True,
|
||||
"source_path": str(source_dir),
|
||||
"seed_path": str(source_dir),
|
||||
},
|
||||
)
|
||||
)
|
||||
task_id = str(created["task_id"])
|
||||
workspace_id = str(created["workspace_id"])
|
||||
update_dir = tmp_path / "update"
|
||||
update_dir.mkdir()
|
||||
(update_dir / "more.txt").write_text("more\n", encoding="utf-8")
|
||||
synced = _extract_structured(
|
||||
await server.call_tool(
|
||||
"task_sync_push",
|
||||
"workspace_sync_push",
|
||||
{
|
||||
"task_id": task_id,
|
||||
"workspace_id": workspace_id,
|
||||
"source_path": str(update_dir),
|
||||
"dest": "subdir",
|
||||
},
|
||||
|
|
@ -218,15 +218,19 @@ def test_task_tools_round_trip(tmp_path: Path) -> None:
|
|||
)
|
||||
executed = _extract_structured(
|
||||
await server.call_tool(
|
||||
"task_exec",
|
||||
"workspace_exec",
|
||||
{
|
||||
"task_id": task_id,
|
||||
"workspace_id": workspace_id,
|
||||
"command": "cat subdir/more.txt",
|
||||
},
|
||||
)
|
||||
)
|
||||
logs = _extract_structured(await server.call_tool("task_logs", {"task_id": task_id}))
|
||||
deleted = _extract_structured(await server.call_tool("task_delete", {"task_id": task_id}))
|
||||
logs = _extract_structured(
|
||||
await server.call_tool("workspace_logs", {"workspace_id": workspace_id})
|
||||
)
|
||||
deleted = _extract_structured(
|
||||
await server.call_tool("workspace_delete", {"workspace_id": workspace_id})
|
||||
)
|
||||
return created, synced, executed, logs, deleted
|
||||
|
||||
created, synced, executed, logs, deleted = asyncio.run(_run())
|
||||
|
|
|
|||
|
|
@ -267,48 +267,48 @@ def test_vm_manager_run_vm(tmp_path: Path) -> None:
|
|||
assert str(result["stdout"]) == "ok\n"
|
||||
|
||||
|
||||
def test_task_lifecycle_and_logs(tmp_path: Path) -> None:
|
||||
def test_workspace_lifecycle_and_logs(tmp_path: Path) -> None:
|
||||
manager = VmManager(
|
||||
backend_name="mock",
|
||||
base_dir=tmp_path / "vms",
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
|
||||
created = manager.create_task(
|
||||
created = manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
)
|
||||
task_id = str(created["task_id"])
|
||||
workspace_id = str(created["workspace_id"])
|
||||
assert created["state"] == "started"
|
||||
assert created["workspace_path"] == "/workspace"
|
||||
|
||||
first = manager.exec_task(
|
||||
task_id,
|
||||
first = manager.exec_workspace(
|
||||
workspace_id,
|
||||
command="printf 'hello\\n' > note.txt",
|
||||
timeout_seconds=30,
|
||||
)
|
||||
second = manager.exec_task(task_id, command="cat note.txt", timeout_seconds=30)
|
||||
second = manager.exec_workspace(workspace_id, command="cat note.txt", timeout_seconds=30)
|
||||
|
||||
assert first["exit_code"] == 0
|
||||
assert second["stdout"] == "hello\n"
|
||||
|
||||
status = manager.status_task(task_id)
|
||||
status = manager.status_workspace(workspace_id)
|
||||
assert status["command_count"] == 2
|
||||
assert status["last_command"] is not None
|
||||
|
||||
logs = manager.logs_task(task_id)
|
||||
logs = manager.logs_workspace(workspace_id)
|
||||
assert logs["count"] == 2
|
||||
entries = logs["entries"]
|
||||
assert isinstance(entries, list)
|
||||
assert entries[1]["stdout"] == "hello\n"
|
||||
|
||||
deleted = manager.delete_task(task_id)
|
||||
deleted = manager.delete_workspace(workspace_id)
|
||||
assert deleted["deleted"] is True
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
manager.status_task(task_id)
|
||||
manager.status_workspace(workspace_id)
|
||||
|
||||
|
||||
def test_task_create_seeds_directory_source_into_workspace(tmp_path: Path) -> None:
|
||||
def test_workspace_create_seeds_directory_source_into_workspace(tmp_path: Path) -> None:
|
||||
source_dir = tmp_path / "seed"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "note.txt").write_text("hello\n", encoding="utf-8")
|
||||
|
|
@ -319,25 +319,25 @@ def test_task_create_seeds_directory_source_into_workspace(tmp_path: Path) -> No
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
|
||||
created = manager.create_task(
|
||||
created = manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
source_path=source_dir,
|
||||
seed_path=source_dir,
|
||||
)
|
||||
task_id = str(created["task_id"])
|
||||
workspace_id = str(created["workspace_id"])
|
||||
|
||||
workspace_seed = created["workspace_seed"]
|
||||
assert workspace_seed["mode"] == "directory"
|
||||
assert workspace_seed["source_path"] == str(source_dir.resolve())
|
||||
executed = manager.exec_task(task_id, command="cat note.txt", timeout_seconds=30)
|
||||
assert workspace_seed["seed_path"] == str(source_dir.resolve())
|
||||
executed = manager.exec_workspace(workspace_id, command="cat note.txt", timeout_seconds=30)
|
||||
assert executed["stdout"] == "hello\n"
|
||||
|
||||
status = manager.status_task(task_id)
|
||||
status = manager.status_workspace(workspace_id)
|
||||
assert status["workspace_seed"]["mode"] == "directory"
|
||||
assert status["workspace_seed"]["source_path"] == str(source_dir.resolve())
|
||||
assert status["workspace_seed"]["seed_path"] == str(source_dir.resolve())
|
||||
|
||||
|
||||
def test_task_create_seeds_tar_archive_into_workspace(tmp_path: Path) -> None:
|
||||
def test_workspace_create_seeds_tar_archive_into_workspace(tmp_path: Path) -> None:
|
||||
archive_path = tmp_path / "seed.tgz"
|
||||
nested_dir = tmp_path / "src"
|
||||
nested_dir.mkdir()
|
||||
|
|
@ -351,19 +351,19 @@ def test_task_create_seeds_tar_archive_into_workspace(tmp_path: Path) -> None:
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
|
||||
created = manager.create_task(
|
||||
created = manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
source_path=archive_path,
|
||||
seed_path=archive_path,
|
||||
)
|
||||
task_id = str(created["task_id"])
|
||||
workspace_id = str(created["workspace_id"])
|
||||
|
||||
assert created["workspace_seed"]["mode"] == "tar_archive"
|
||||
executed = manager.exec_task(task_id, command="cat note.txt", timeout_seconds=30)
|
||||
executed = manager.exec_workspace(workspace_id, command="cat note.txt", timeout_seconds=30)
|
||||
assert executed["stdout"] == "archive\n"
|
||||
|
||||
|
||||
def test_task_sync_push_updates_started_workspace(tmp_path: Path) -> None:
|
||||
def test_workspace_sync_push_updates_started_workspace(tmp_path: Path) -> None:
|
||||
source_dir = tmp_path / "seed"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "note.txt").write_text("hello\n", encoding="utf-8")
|
||||
|
|
@ -377,26 +377,30 @@ def test_task_sync_push_updates_started_workspace(tmp_path: Path) -> None:
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
|
||||
created = manager.create_task(
|
||||
created = manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
source_path=source_dir,
|
||||
seed_path=source_dir,
|
||||
)
|
||||
task_id = str(created["task_id"])
|
||||
synced = manager.push_task_sync(task_id, source_path=update_dir, dest="subdir")
|
||||
workspace_id = str(created["workspace_id"])
|
||||
synced = manager.push_workspace_sync(workspace_id, source_path=update_dir, dest="subdir")
|
||||
|
||||
assert synced["workspace_sync"]["mode"] == "directory"
|
||||
assert synced["workspace_sync"]["destination"] == "/workspace/subdir"
|
||||
|
||||
executed = manager.exec_task(task_id, command="cat subdir/more.txt", timeout_seconds=30)
|
||||
executed = manager.exec_workspace(
|
||||
workspace_id,
|
||||
command="cat subdir/more.txt",
|
||||
timeout_seconds=30,
|
||||
)
|
||||
assert executed["stdout"] == "more\n"
|
||||
|
||||
status = manager.status_task(task_id)
|
||||
status = manager.status_workspace(workspace_id)
|
||||
assert status["command_count"] == 1
|
||||
assert status["workspace_seed"]["mode"] == "directory"
|
||||
|
||||
|
||||
def test_task_sync_push_requires_started_task(tmp_path: Path) -> None:
|
||||
def test_workspace_sync_push_requires_started_workspace(tmp_path: Path) -> None:
|
||||
source_dir = tmp_path / "seed"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "note.txt").write_text("hello\n", encoding="utf-8")
|
||||
|
|
@ -410,22 +414,25 @@ def test_task_sync_push_requires_started_task(tmp_path: Path) -> None:
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
|
||||
created = manager.create_task(
|
||||
created = manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
source_path=source_dir,
|
||||
seed_path=source_dir,
|
||||
)
|
||||
task_id = str(created["task_id"])
|
||||
task_path = tmp_path / "vms" / "tasks" / task_id / "task.json"
|
||||
payload = json.loads(task_path.read_text(encoding="utf-8"))
|
||||
workspace_id = str(created["workspace_id"])
|
||||
workspace_path = tmp_path / "vms" / "workspaces" / workspace_id / "workspace.json"
|
||||
payload = json.loads(workspace_path.read_text(encoding="utf-8"))
|
||||
payload["state"] = "stopped"
|
||||
task_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
workspace_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with pytest.raises(RuntimeError, match="must be in 'started' state before task_sync_push"):
|
||||
manager.push_task_sync(task_id, source_path=update_dir)
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="must be in 'started' state before workspace_sync_push",
|
||||
):
|
||||
manager.push_workspace_sync(workspace_id, source_path=update_dir)
|
||||
|
||||
|
||||
def test_task_sync_push_rejects_destination_outside_workspace(tmp_path: Path) -> None:
|
||||
def test_workspace_sync_push_rejects_destination_outside_workspace(tmp_path: Path) -> None:
|
||||
source_dir = tmp_path / "seed"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "note.txt").write_text("hello\n", encoding="utf-8")
|
||||
|
|
@ -436,18 +443,18 @@ def test_task_sync_push_rejects_destination_outside_workspace(tmp_path: Path) ->
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
|
||||
task_id = str(
|
||||
manager.create_task(
|
||||
workspace_id = str(
|
||||
manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
)["task_id"]
|
||||
)["workspace_id"]
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="workspace destination must stay inside /workspace"):
|
||||
manager.push_task_sync(task_id, source_path=source_dir, dest="../escape")
|
||||
manager.push_workspace_sync(workspace_id, source_path=source_dir, dest="../escape")
|
||||
|
||||
|
||||
def test_task_create_rejects_unsafe_seed_archive(tmp_path: Path) -> None:
|
||||
def test_workspace_create_rejects_unsafe_seed_archive(tmp_path: Path) -> None:
|
||||
archive_path = tmp_path / "bad.tgz"
|
||||
with tarfile.open(archive_path, "w:gz") as archive:
|
||||
payload = b"bad\n"
|
||||
|
|
@ -462,15 +469,15 @@ def test_task_create_rejects_unsafe_seed_archive(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="unsafe archive member path"):
|
||||
manager.create_task(
|
||||
manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
source_path=archive_path,
|
||||
seed_path=archive_path,
|
||||
)
|
||||
assert list((tmp_path / "vms" / "tasks").iterdir()) == []
|
||||
assert list((tmp_path / "vms" / "workspaces").iterdir()) == []
|
||||
|
||||
|
||||
def test_task_create_rejects_archive_that_writes_through_symlink(tmp_path: Path) -> None:
|
||||
def test_workspace_create_rejects_archive_that_writes_through_symlink(tmp_path: Path) -> None:
|
||||
archive_path = tmp_path / "bad-symlink.tgz"
|
||||
with tarfile.open(archive_path, "w:gz") as archive:
|
||||
symlink_info = tarfile.TarInfo(name="linked")
|
||||
|
|
@ -490,14 +497,14 @@ def test_task_create_rejects_archive_that_writes_through_symlink(tmp_path: Path)
|
|||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="traverse through a symlinked path"):
|
||||
manager.create_task(
|
||||
manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
source_path=archive_path,
|
||||
seed_path=archive_path,
|
||||
)
|
||||
|
||||
|
||||
def test_task_create_cleans_up_on_seed_failure(
|
||||
def test_workspace_create_cleans_up_on_seed_failure(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
source_dir = tmp_path / "seed"
|
||||
|
|
@ -517,27 +524,27 @@ def test_task_create_cleans_up_on_seed_failure(
|
|||
monkeypatch.setattr(manager._backend, "import_archive", _boom) # noqa: SLF001
|
||||
|
||||
with pytest.raises(RuntimeError, match="seed import failed"):
|
||||
manager.create_task(
|
||||
manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
source_path=source_dir,
|
||||
seed_path=source_dir,
|
||||
)
|
||||
|
||||
assert list((tmp_path / "vms" / "tasks").iterdir()) == []
|
||||
assert list((tmp_path / "vms" / "workspaces").iterdir()) == []
|
||||
|
||||
|
||||
def test_task_rehydrates_across_manager_processes(tmp_path: Path) -> None:
|
||||
def test_workspace_rehydrates_across_manager_processes(tmp_path: Path) -> None:
|
||||
base_dir = tmp_path / "vms"
|
||||
manager = VmManager(
|
||||
backend_name="mock",
|
||||
base_dir=base_dir,
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
task_id = str(
|
||||
manager.create_task(
|
||||
workspace_id = str(
|
||||
manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
)["task_id"]
|
||||
)["workspace_id"]
|
||||
)
|
||||
|
||||
other = VmManager(
|
||||
|
|
@ -545,33 +552,33 @@ def test_task_rehydrates_across_manager_processes(tmp_path: Path) -> None:
|
|||
base_dir=base_dir,
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
executed = other.exec_task(task_id, command="printf 'ok\\n'", timeout_seconds=30)
|
||||
executed = other.exec_workspace(workspace_id, command="printf 'ok\\n'", timeout_seconds=30)
|
||||
assert executed["exit_code"] == 0
|
||||
assert executed["stdout"] == "ok\n"
|
||||
|
||||
logs = other.logs_task(task_id)
|
||||
logs = other.logs_workspace(workspace_id)
|
||||
assert logs["count"] == 1
|
||||
|
||||
|
||||
def test_task_requires_started_state(tmp_path: Path) -> None:
|
||||
def test_workspace_requires_started_state(tmp_path: Path) -> None:
|
||||
manager = VmManager(
|
||||
backend_name="mock",
|
||||
base_dir=tmp_path / "vms",
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
task_id = str(
|
||||
manager.create_task(
|
||||
workspace_id = str(
|
||||
manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
)["task_id"]
|
||||
)["workspace_id"]
|
||||
)
|
||||
task_dir = tmp_path / "vms" / "tasks" / task_id / "task.json"
|
||||
payload = json.loads(task_dir.read_text(encoding="utf-8"))
|
||||
workspace_path = tmp_path / "vms" / "workspaces" / workspace_id / "workspace.json"
|
||||
payload = json.loads(workspace_path.read_text(encoding="utf-8"))
|
||||
payload["state"] = "stopped"
|
||||
task_dir.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
workspace_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with pytest.raises(RuntimeError, match="must be in 'started' state"):
|
||||
manager.exec_task(task_id, command="true", timeout_seconds=30)
|
||||
manager.exec_workspace(workspace_id, command="true", timeout_seconds=30)
|
||||
|
||||
|
||||
def test_vm_manager_firecracker_backend_path(
|
||||
|
|
@ -708,7 +715,7 @@ def test_copy_rootfs_falls_back_to_copy2(
|
|||
assert dest.read_text(encoding="utf-8") == "payload"
|
||||
|
||||
|
||||
def test_task_create_cleans_up_on_start_failure(
|
||||
def test_workspace_create_cleans_up_on_start_failure(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
manager = VmManager(
|
||||
|
|
@ -724,9 +731,9 @@ def test_task_create_cleans_up_on_start_failure(
|
|||
monkeypatch.setattr(manager._backend, "start", _boom) # noqa: SLF001
|
||||
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
manager.create_task(environment="debian:12-base", allow_host_compat=True)
|
||||
manager.create_workspace(environment="debian:12-base", allow_host_compat=True)
|
||||
|
||||
assert list((tmp_path / "vms" / "tasks").iterdir()) == []
|
||||
assert list((tmp_path / "vms" / "workspaces").iterdir()) == []
|
||||
|
||||
|
||||
def test_exec_instance_wraps_guest_workspace_command(tmp_path: Path) -> None:
|
||||
|
|
@ -786,53 +793,53 @@ def test_exec_instance_wraps_guest_workspace_command(tmp_path: Path) -> None:
|
|||
assert captured["workdir"] is None
|
||||
|
||||
|
||||
def test_status_task_marks_dead_backing_process_stopped(tmp_path: Path) -> None:
|
||||
def test_status_workspace_marks_dead_backing_process_stopped(tmp_path: Path) -> None:
|
||||
manager = VmManager(
|
||||
backend_name="mock",
|
||||
base_dir=tmp_path / "vms",
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
task_id = str(
|
||||
manager.create_task(
|
||||
workspace_id = str(
|
||||
manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
)["task_id"]
|
||||
)["workspace_id"]
|
||||
)
|
||||
task_path = tmp_path / "vms" / "tasks" / task_id / "task.json"
|
||||
payload = json.loads(task_path.read_text(encoding="utf-8"))
|
||||
workspace_path = tmp_path / "vms" / "workspaces" / workspace_id / "workspace.json"
|
||||
payload = json.loads(workspace_path.read_text(encoding="utf-8"))
|
||||
payload["metadata"]["execution_mode"] = "guest_vsock"
|
||||
payload["firecracker_pid"] = 999999
|
||||
task_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
workspace_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
|
||||
status = manager.status_task(task_id)
|
||||
status = manager.status_workspace(workspace_id)
|
||||
assert status["state"] == "stopped"
|
||||
updated_payload = json.loads(task_path.read_text(encoding="utf-8"))
|
||||
updated_payload = json.loads(workspace_path.read_text(encoding="utf-8"))
|
||||
assert "backing guest process" in str(updated_payload.get("last_error", ""))
|
||||
|
||||
|
||||
def test_reap_expired_tasks_removes_invalid_and_expired_records(tmp_path: Path) -> None:
|
||||
def test_reap_expired_workspaces_removes_invalid_and_expired_records(tmp_path: Path) -> None:
|
||||
manager = VmManager(
|
||||
backend_name="mock",
|
||||
base_dir=tmp_path / "vms",
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
invalid_dir = tmp_path / "vms" / "tasks" / "invalid"
|
||||
invalid_dir = tmp_path / "vms" / "workspaces" / "invalid"
|
||||
invalid_dir.mkdir(parents=True)
|
||||
(invalid_dir / "task.json").write_text("[]", encoding="utf-8")
|
||||
(invalid_dir / "workspace.json").write_text("[]", encoding="utf-8")
|
||||
|
||||
task_id = str(
|
||||
manager.create_task(
|
||||
workspace_id = str(
|
||||
manager.create_workspace(
|
||||
environment="debian:12-base",
|
||||
allow_host_compat=True,
|
||||
)["task_id"]
|
||||
)["workspace_id"]
|
||||
)
|
||||
task_path = tmp_path / "vms" / "tasks" / task_id / "task.json"
|
||||
payload = json.loads(task_path.read_text(encoding="utf-8"))
|
||||
workspace_path = tmp_path / "vms" / "workspaces" / workspace_id / "workspace.json"
|
||||
payload = json.loads(workspace_path.read_text(encoding="utf-8"))
|
||||
payload["expires_at"] = 0.0
|
||||
task_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
workspace_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with manager._lock: # noqa: SLF001
|
||||
manager._reap_expired_tasks_locked(time.time()) # noqa: SLF001
|
||||
manager._reap_expired_workspaces_locked(time.time()) # noqa: SLF001
|
||||
|
||||
assert not invalid_dir.exists()
|
||||
assert not (tmp_path / "vms" / "tasks" / task_id).exists()
|
||||
assert not (tmp_path / "vms" / "workspaces" / workspace_id).exists()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue