from __future__ import annotations import asyncio from pathlib import Path from typing import Any, cast from pyro_mcp.api import Pyro from pyro_mcp.vm_manager import VmManager from pyro_mcp.vm_network import TapNetworkManager def test_pyro_run_in_vm_delegates_to_manager(tmp_path: Path) -> None: pyro = Pyro( manager=VmManager( backend_name="mock", base_dir=tmp_path / "vms", network_manager=TapNetworkManager(enabled=False), ) ) result = pyro.run_in_vm( environment="debian:12-base", command="printf 'ok\\n'", vcpu_count=1, mem_mib=512, timeout_seconds=30, ttl_seconds=600, network=False, allow_host_compat=True, ) assert int(result["exit_code"]) == 0 assert str(result["stdout"]) == "ok\n" def test_pyro_create_server_registers_vm_run(tmp_path: Path) -> None: pyro = Pyro( manager=VmManager( backend_name="mock", base_dir=tmp_path / "vms", network_manager=TapNetworkManager(enabled=False), ) ) async def _run() -> list[str]: server = pyro.create_server() tools = await server.list_tools() return sorted(tool.name for tool in tools) tool_names = asyncio.run(_run()) assert "vm_run" in tool_names assert "vm_create" in tool_names assert "workspace_create" in tool_names assert "workspace_diff" in tool_names assert "workspace_sync_push" in tool_names assert "workspace_export" in tool_names assert "shell_open" in tool_names assert "shell_read" in tool_names assert "shell_write" in tool_names assert "shell_signal" in tool_names assert "shell_close" in tool_names assert "service_start" in tool_names assert "service_list" in tool_names assert "service_status" in tool_names assert "service_logs" in tool_names assert "service_stop" in tool_names def test_pyro_vm_run_tool_executes(tmp_path: Path) -> None: pyro = Pyro( manager=VmManager( backend_name="mock", base_dir=tmp_path / "vms", network_manager=TapNetworkManager(enabled=False), ) ) def _extract_structured(raw_result: object) -> dict[str, Any]: if not isinstance(raw_result, tuple) or len(raw_result) != 2: raise TypeError("unexpected call_tool result shape") _, structured = raw_result if not isinstance(structured, dict): raise TypeError("expected structured dictionary result") return cast(dict[str, Any], structured) async def _run() -> dict[str, Any]: server = pyro.create_server() return _extract_structured( await server.call_tool( "vm_run", { "environment": "debian:12-base", "command": "printf 'ok\\n'", "network": False, "allow_host_compat": True, }, ) ) result = asyncio.run(_run()) assert int(result["exit_code"]) == 0 def test_pyro_create_vm_defaults_sizing_and_host_compat(tmp_path: Path) -> None: pyro = Pyro( manager=VmManager( backend_name="mock", base_dir=tmp_path / "vms", network_manager=TapNetworkManager(enabled=False), ) ) created = pyro.create_vm( environment="debian:12-base", allow_host_compat=True, ) assert created["vcpu_count"] == 1 assert created["mem_mib"] == 1024 assert created["allow_host_compat"] is True def test_pyro_workspace_methods_delegate_to_manager(tmp_path: Path) -> None: pyro = Pyro( manager=VmManager( backend_name="mock", base_dir=tmp_path / "vms", network_manager=TapNetworkManager(enabled=False), ) ) source_dir = tmp_path / "seed" source_dir.mkdir() (source_dir / "note.txt").write_text("ok\n", encoding="utf-8") created = pyro.create_workspace( environment="debian:12-base", allow_host_compat=True, seed_path=source_dir, ) 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_workspace_sync(workspace_id, updated_dir, dest="subdir") executed = pyro.exec_workspace(workspace_id, command="cat note.txt") diff_payload = pyro.diff_workspace(workspace_id) export_path = tmp_path / "exported-note.txt" exported = pyro.export_workspace(workspace_id, "note.txt", output_path=export_path) service = pyro.start_service( workspace_id, "app", command="sh -lc 'touch .ready; while true; do sleep 60; done'", readiness={"type": "file", "path": ".ready"}, ) services = pyro.list_services(workspace_id) service_status = pyro.status_service(workspace_id, "app") service_logs = pyro.logs_service(workspace_id, "app", all=True) service_stopped = pyro.stop_service(workspace_id, "app") 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" assert synced["workspace_sync"]["destination"] == "/workspace/subdir" assert diff_payload["changed"] is True assert exported["output_path"] == str(export_path) assert export_path.read_text(encoding="utf-8") == "ok\n" assert service["state"] == "running" assert services["count"] == 1 assert service_status["state"] == "running" assert service_logs["tail_lines"] is None assert service_stopped["state"] == "stopped" assert status["command_count"] == 1 assert status["service_count"] == 1 assert logs["count"] == 1 assert deleted["deleted"] is True