Refactor public API around environments
This commit is contained in:
parent
57dae52cc2
commit
5d5243df23
41 changed files with 1301 additions and 459 deletions
|
|
@ -18,7 +18,7 @@ def test_pyro_run_in_vm_delegates_to_manager(tmp_path: Path) -> None:
|
|||
)
|
||||
)
|
||||
result = pyro.run_in_vm(
|
||||
profile="debian-base",
|
||||
environment="debian:12-base",
|
||||
command="printf 'ok\\n'",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
|
|
@ -72,7 +72,7 @@ def test_pyro_vm_run_tool_executes(tmp_path: Path) -> None:
|
|||
await server.call_tool(
|
||||
"vm_run",
|
||||
{
|
||||
"profile": "debian-base",
|
||||
"environment": "debian:12-base",
|
||||
"command": "printf 'ok\\n'",
|
||||
"vcpu_count": 1,
|
||||
"mem_mib": 512,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ def test_cli_run_prints_json(
|
|||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
command="run",
|
||||
profile="debian-git",
|
||||
environment="debian:12",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
timeout_seconds=30,
|
||||
|
|
@ -84,6 +84,24 @@ def test_cli_demo_ollama_prints_summary(
|
|||
assert "[summary] exit_code=0 fallback_used=False execution_mode=guest_vsock" in output
|
||||
|
||||
|
||||
def test_cli_env_list_prints_json(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
class StubPyro:
|
||||
def list_environments(self) -> list[dict[str, object]]:
|
||||
return [{"name": "debian:12", "installed": False}]
|
||||
|
||||
class StubParser:
|
||||
def parse_args(self) -> argparse.Namespace:
|
||||
return argparse.Namespace(command="env", env_command="list")
|
||||
|
||||
monkeypatch.setattr(cli, "_build_parser", lambda: StubParser())
|
||||
monkeypatch.setattr(cli, "Pyro", StubPyro)
|
||||
cli.main()
|
||||
output = json.loads(capsys.readouterr().out)
|
||||
assert output["environments"][0]["name"] == "debian:12"
|
||||
|
||||
|
||||
def test_cli_requires_run_command() -> None:
|
||||
with pytest.raises(ValueError, match="command is required"):
|
||||
cli._require_command([])
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ def test_run_demo_happy_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
def run_in_vm(
|
||||
self,
|
||||
*,
|
||||
profile: str,
|
||||
environment: str,
|
||||
command: str,
|
||||
vcpu_count: int,
|
||||
mem_mib: int,
|
||||
|
|
@ -30,7 +30,7 @@ def test_run_demo_happy_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
(
|
||||
"run_in_vm",
|
||||
{
|
||||
"profile": profile,
|
||||
"environment": environment,
|
||||
"command": command,
|
||||
"vcpu_count": vcpu_count,
|
||||
"mem_mib": mem_mib,
|
||||
|
|
@ -50,7 +50,7 @@ def test_run_demo_happy_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
(
|
||||
"run_in_vm",
|
||||
{
|
||||
"profile": "debian-git",
|
||||
"environment": "debian:12",
|
||||
"command": "git --version",
|
||||
"vcpu_count": 1,
|
||||
"mem_mib": 512,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ def test_langchain_example_delegates_to_pyro(monkeypatch: pytest.MonkeyPatch) ->
|
|||
)(),
|
||||
)
|
||||
result = module.run_vm_run_tool(
|
||||
profile="debian-git",
|
||||
environment="debian:12",
|
||||
command="git --version",
|
||||
vcpu_count=1,
|
||||
mem_mib=1024,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ def _stepwise_model_response(payload: dict[str, Any], step: int) -> dict[str, An
|
|||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [{"id": "1", "function": {"name": "vm_list_profiles"}}],
|
||||
"tool_calls": [{"id": "1", "function": {"name": "vm_list_environments"}}],
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -50,7 +50,7 @@ def _stepwise_model_response(payload: dict[str, Any], step: int) -> dict[str, An
|
|||
"name": "vm_run",
|
||||
"arguments": json.dumps(
|
||||
{
|
||||
"profile": "debian-git",
|
||||
"environment": "debian:12",
|
||||
"command": "printf 'true\\n'",
|
||||
"vcpu_count": 1,
|
||||
"mem_mib": 512,
|
||||
|
|
@ -117,7 +117,7 @@ def test_run_ollama_tool_demo_recovers_from_bad_vm_id(
|
|||
"name": "vm_exec",
|
||||
"arguments": json.dumps(
|
||||
{
|
||||
"vm_id": "vm_list_profiles",
|
||||
"vm_id": "vm_list_environments",
|
||||
"command": ollama_demo.NETWORK_PROOF_COMMAND,
|
||||
}
|
||||
),
|
||||
|
|
@ -157,15 +157,15 @@ def test_run_ollama_tool_demo_resolves_vm_id_placeholder(
|
|||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{"id": "1", "function": {"name": "vm_list_profiles"}},
|
||||
{"id": "2", "function": {"name": "vm_list_profiles"}},
|
||||
{"id": "1", "function": {"name": "vm_list_environments"}},
|
||||
{"id": "2", "function": {"name": "vm_list_environments"}},
|
||||
{
|
||||
"id": "3",
|
||||
"function": {
|
||||
"name": "vm_create",
|
||||
"arguments": json.dumps(
|
||||
{
|
||||
"profile": "debian-git",
|
||||
"environment": "debian:12",
|
||||
"vcpu_count": "2",
|
||||
"mem_mib": "2048",
|
||||
}
|
||||
|
|
@ -217,7 +217,12 @@ def test_run_ollama_tool_demo_resolves_vm_id_placeholder(
|
|||
|
||||
def test_dispatch_tool_call_vm_exec_autostarts_created_vm(tmp_path: Path) -> None:
|
||||
pyro = RealPyro(manager=RealVmManager(backend_name="mock", base_dir=tmp_path / "vms"))
|
||||
created = pyro.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=60)
|
||||
created = pyro.create_vm(
|
||||
environment="debian:12-base",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=60,
|
||||
)
|
||||
vm_id = str(created["vm_id"])
|
||||
|
||||
executed = ollama_demo._dispatch_tool_call(
|
||||
|
|
@ -291,7 +296,7 @@ def test_run_ollama_tool_demo_verbose_logs_values(monkeypatch: pytest.MonkeyPatc
|
|||
assert result["fallback_used"] is False
|
||||
assert str(result["exec_result"]["stdout"]).strip() == "true"
|
||||
assert any("[model] input user:" in line for line in logs)
|
||||
assert any("[model] tool_call vm_list_profiles args={}" in line for line in logs)
|
||||
assert any("[model] tool_call vm_list_environments args={}" in line for line in logs)
|
||||
assert any("[tool] result vm_run " in line for line in logs)
|
||||
|
||||
|
||||
|
|
@ -299,7 +304,7 @@ def test_run_ollama_tool_demo_verbose_logs_values(monkeypatch: pytest.MonkeyPatc
|
|||
("tool_call", "error"),
|
||||
[
|
||||
(1, "invalid tool call entry"),
|
||||
({"id": "", "function": {"name": "vm_list_profiles"}}, "valid call id"),
|
||||
({"id": "", "function": {"name": "vm_list_environments"}}, "valid call id"),
|
||||
({"id": "1"}, "function metadata"),
|
||||
({"id": "1", "function": {"name": 3}}, "name is invalid"),
|
||||
],
|
||||
|
|
@ -326,7 +331,7 @@ def test_run_ollama_tool_demo_max_rounds(monkeypatch: pytest.MonkeyPatch) -> Non
|
|||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"tool_calls": [{"id": "1", "function": {"name": "vm_list_profiles"}}],
|
||||
"tool_calls": [{"id": "1", "function": {"name": "vm_list_environments"}}],
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -384,13 +389,13 @@ def test_run_ollama_tool_demo_exec_result_validation(
|
|||
|
||||
def test_dispatch_tool_call_coverage(tmp_path: Path) -> None:
|
||||
pyro = RealPyro(manager=RealVmManager(backend_name="mock", base_dir=tmp_path / "vms"))
|
||||
profiles = ollama_demo._dispatch_tool_call(pyro, "vm_list_profiles", {})
|
||||
assert "profiles" in profiles
|
||||
environments = ollama_demo._dispatch_tool_call(pyro, "vm_list_environments", {})
|
||||
assert "environments" in environments
|
||||
created = ollama_demo._dispatch_tool_call(
|
||||
pyro,
|
||||
"vm_create",
|
||||
{
|
||||
"profile": "debian-base",
|
||||
"environment": "debian:12-base",
|
||||
"vcpu_count": "1",
|
||||
"mem_mib": "512",
|
||||
"ttl_seconds": "60",
|
||||
|
|
@ -412,7 +417,7 @@ def test_dispatch_tool_call_coverage(tmp_path: Path) -> None:
|
|||
pyro,
|
||||
"vm_run",
|
||||
{
|
||||
"profile": "debian-base",
|
||||
"environment": "debian:12-base",
|
||||
"command": "printf 'true\\n'",
|
||||
"vcpu_count": "1",
|
||||
"mem_mib": "512",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def test_openai_example_runs_function_call_loop(monkeypatch: pytest.MonkeyPatch)
|
|||
name="vm_run",
|
||||
call_id="call_123",
|
||||
arguments=(
|
||||
'{"profile":"debian-git","command":"git --version",'
|
||||
'{"environment":"debian:12","command":"git --version",'
|
||||
'"vcpu_count":1,"mem_mib":1024}'
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from pyro_mcp.cli import _build_parser
|
|||
from pyro_mcp.contract import (
|
||||
PUBLIC_CLI_COMMANDS,
|
||||
PUBLIC_CLI_DEMO_SUBCOMMANDS,
|
||||
PUBLIC_CLI_ENV_SUBCOMMANDS,
|
||||
PUBLIC_CLI_RUN_FLAGS,
|
||||
PUBLIC_MCP_TOOLS,
|
||||
PUBLIC_SDK_METHODS,
|
||||
|
|
@ -49,14 +50,19 @@ def test_public_cli_help_lists_commands_and_run_flags() -> None:
|
|||
|
||||
run_parser = _build_parser()
|
||||
run_help = run_parser.parse_args(
|
||||
["run", "--profile", "debian-base", "--vcpu-count", "1", "--mem-mib", "512", "--", "true"]
|
||||
["run", "debian:12-base", "--vcpu-count", "1", "--mem-mib", "512", "--", "true"]
|
||||
)
|
||||
assert run_help.command == "run"
|
||||
assert run_help.environment == "debian:12-base"
|
||||
|
||||
run_help_text = _subparser_choice(parser, "run").format_help()
|
||||
for flag in PUBLIC_CLI_RUN_FLAGS:
|
||||
assert flag in run_help_text
|
||||
|
||||
env_help_text = _subparser_choice(parser, "env").format_help()
|
||||
for subcommand_name in PUBLIC_CLI_ENV_SUBCOMMANDS:
|
||||
assert subcommand_name in env_help_text
|
||||
|
||||
demo_help_text = _subparser_choice(parser, "demo").format_help()
|
||||
for subcommand_name in PUBLIC_CLI_DEMO_SUBCOMMANDS:
|
||||
assert subcommand_name in demo_help_text
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ def test_doctor_report_has_runtime_fields() -> None:
|
|||
assert "firecracker_bin" in runtime
|
||||
assert "guest_agent_path" in runtime
|
||||
assert "component_versions" in runtime
|
||||
assert "environments" in runtime
|
||||
networking = report["networking"]
|
||||
assert isinstance(networking, dict)
|
||||
assert "tun_available" in networking
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def test_network_check_uses_network_enabled_manager(monkeypatch: pytest.MonkeyPa
|
|||
result = runtime_network_check.run_network_check()
|
||||
|
||||
assert observed["run_kwargs"] == {
|
||||
"profile": "debian-git",
|
||||
"environment": "debian:12",
|
||||
"command": runtime_network_check.NETWORK_CHECK_COMMAND,
|
||||
"vcpu_count": 1,
|
||||
"mem_mib": 1024,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def test_create_server_registers_vm_tools(tmp_path: Path) -> None:
|
|||
tool_names = asyncio.run(_run())
|
||||
assert "vm_create" in tool_names
|
||||
assert "vm_exec" in tool_names
|
||||
assert "vm_list_profiles" in tool_names
|
||||
assert "vm_list_environments" in tool_names
|
||||
assert "vm_network_info" in tool_names
|
||||
assert "vm_run" in tool_names
|
||||
assert "vm_status" in tool_names
|
||||
|
|
@ -54,7 +54,7 @@ def test_vm_run_round_trip(tmp_path: Path) -> None:
|
|||
await server.call_tool(
|
||||
"vm_run",
|
||||
{
|
||||
"profile": "debian-git",
|
||||
"environment": "debian:12",
|
||||
"command": "printf 'git version 2.0\\n'",
|
||||
"vcpu_count": 1,
|
||||
"mem_mib": 512,
|
||||
|
|
@ -95,19 +95,24 @@ def test_vm_tools_status_stop_delete_and_reap(tmp_path: Path) -> None:
|
|||
dict[str, Any],
|
||||
]:
|
||||
server = create_server(manager=manager)
|
||||
profiles_raw = await server.call_tool("vm_list_profiles", {})
|
||||
if not isinstance(profiles_raw, tuple) or len(profiles_raw) != 2:
|
||||
raise TypeError("unexpected profiles result")
|
||||
_, profiles_structured = profiles_raw
|
||||
if not isinstance(profiles_structured, dict):
|
||||
raise TypeError("profiles tool should return a dictionary")
|
||||
raw_profiles = profiles_structured.get("result")
|
||||
if not isinstance(raw_profiles, list):
|
||||
raise TypeError("profiles tool did not contain a result list")
|
||||
environments_raw = await server.call_tool("vm_list_environments", {})
|
||||
if not isinstance(environments_raw, tuple) or len(environments_raw) != 2:
|
||||
raise TypeError("unexpected environments result")
|
||||
_, environments_structured = environments_raw
|
||||
if not isinstance(environments_structured, dict):
|
||||
raise TypeError("environments tool should return a dictionary")
|
||||
raw_environments = environments_structured.get("result")
|
||||
if not isinstance(raw_environments, list):
|
||||
raise TypeError("environments tool did not contain a result list")
|
||||
created = _extract_structured(
|
||||
await server.call_tool(
|
||||
"vm_create",
|
||||
{"profile": "debian-base", "vcpu_count": 1, "mem_mib": 512, "ttl_seconds": 600},
|
||||
{
|
||||
"environment": "debian:12-base",
|
||||
"vcpu_count": 1,
|
||||
"mem_mib": 512,
|
||||
"ttl_seconds": 600,
|
||||
},
|
||||
)
|
||||
)
|
||||
vm_id = str(created["vm_id"])
|
||||
|
|
@ -120,7 +125,12 @@ def test_vm_tools_status_stop_delete_and_reap(tmp_path: Path) -> None:
|
|||
expiring = _extract_structured(
|
||||
await server.call_tool(
|
||||
"vm_create",
|
||||
{"profile": "debian-base", "vcpu_count": 1, "mem_mib": 512, "ttl_seconds": 1},
|
||||
{
|
||||
"environment": "debian:12-base",
|
||||
"vcpu_count": 1,
|
||||
"mem_mib": 512,
|
||||
"ttl_seconds": 1,
|
||||
},
|
||||
)
|
||||
)
|
||||
expiring_id = str(expiring["vm_id"])
|
||||
|
|
@ -131,16 +141,16 @@ def test_vm_tools_status_stop_delete_and_reap(tmp_path: Path) -> None:
|
|||
network,
|
||||
stopped,
|
||||
deleted,
|
||||
cast(list[dict[str, object]], raw_profiles),
|
||||
cast(list[dict[str, object]], raw_environments),
|
||||
reaped,
|
||||
)
|
||||
|
||||
status, network, stopped, deleted, profiles, reaped = asyncio.run(_run())
|
||||
status, network, stopped, deleted, environments, reaped = asyncio.run(_run())
|
||||
assert status["state"] == "started"
|
||||
assert network["network_enabled"] is False
|
||||
assert stopped["state"] == "stopped"
|
||||
assert bool(deleted["deleted"]) is True
|
||||
assert profiles[0]["name"] == "debian-base"
|
||||
assert environments[0]["name"] == "debian:12"
|
||||
assert int(reaped["count"]) == 1
|
||||
|
||||
|
||||
|
|
|
|||
153
tests/test_vm_environments.py
Normal file
153
tests/test_vm_environments.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pyro_mcp.runtime import resolve_runtime_paths
|
||||
from pyro_mcp.vm_environments import EnvironmentStore, get_environment, list_environments
|
||||
|
||||
|
||||
def test_list_environments_includes_expected_entries() -> None:
|
||||
environments = list_environments(runtime_paths=resolve_runtime_paths())
|
||||
names = {str(entry["name"]) for entry in environments}
|
||||
assert {"debian:12", "debian:12-base", "debian:12-build"} <= names
|
||||
|
||||
|
||||
def test_get_environment_rejects_unknown() -> None:
|
||||
with pytest.raises(ValueError, match="unknown environment"):
|
||||
get_environment("does-not-exist")
|
||||
|
||||
|
||||
def test_environment_store_installs_from_local_runtime_source(tmp_path: Path) -> None:
|
||||
store = EnvironmentStore(runtime_paths=resolve_runtime_paths(), cache_dir=tmp_path / "cache")
|
||||
installed = store.ensure_installed("debian:12")
|
||||
|
||||
assert installed.kernel_image.exists()
|
||||
assert installed.rootfs_image.exists()
|
||||
assert (installed.install_dir / "environment.json").exists()
|
||||
|
||||
|
||||
def test_environment_store_pull_and_cached_inspect(tmp_path: Path) -> None:
|
||||
store = EnvironmentStore(runtime_paths=resolve_runtime_paths(), cache_dir=tmp_path / "cache")
|
||||
|
||||
before = store.inspect_environment("debian:12")
|
||||
assert before["installed"] is False
|
||||
|
||||
pulled = store.pull_environment("debian:12")
|
||||
assert pulled["installed"] is True
|
||||
assert "install_manifest" in pulled
|
||||
|
||||
cached = store.ensure_installed("debian:12")
|
||||
assert cached.installed is True
|
||||
|
||||
after = store.inspect_environment("debian:12")
|
||||
assert after["installed"] is True
|
||||
assert "install_manifest" in after
|
||||
|
||||
|
||||
def test_environment_store_uses_env_override_for_default_cache_dir(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setenv("PYRO_ENVIRONMENT_CACHE_DIR", str(tmp_path / "override-cache"))
|
||||
store = EnvironmentStore(runtime_paths=resolve_runtime_paths())
|
||||
assert store.cache_dir == tmp_path / "override-cache"
|
||||
|
||||
|
||||
def test_environment_store_installs_from_archive_when_runtime_source_missing(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
runtime_paths = resolve_runtime_paths()
|
||||
source_environment = get_environment("debian:12-base", runtime_paths=runtime_paths)
|
||||
|
||||
archive_dir = tmp_path / "archive"
|
||||
archive_dir.mkdir(parents=True, exist_ok=True)
|
||||
(archive_dir / "vmlinux").write_text("kernel\n", encoding="utf-8")
|
||||
(archive_dir / "rootfs.ext4").write_text("rootfs\n", encoding="utf-8")
|
||||
archive_path = tmp_path / "environment.tgz"
|
||||
with tarfile.open(archive_path, "w:gz") as archive:
|
||||
archive.add(archive_dir / "vmlinux", arcname="vmlinux")
|
||||
archive.add(archive_dir / "rootfs.ext4", arcname="rootfs.ext4")
|
||||
|
||||
missing_bundle = tmp_path / "bundle"
|
||||
platform_root = missing_bundle / "linux-x86_64"
|
||||
platform_root.mkdir(parents=True, exist_ok=True)
|
||||
(missing_bundle / "NOTICE").write_text(
|
||||
runtime_paths.notice_path.read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(platform_root / "manifest.json").write_text(
|
||||
runtime_paths.manifest_path.read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(platform_root / "bin").mkdir(parents=True, exist_ok=True)
|
||||
(platform_root / "bin" / "firecracker").write_bytes(runtime_paths.firecracker_bin.read_bytes())
|
||||
(platform_root / "bin" / "jailer").write_bytes(runtime_paths.jailer_bin.read_bytes())
|
||||
guest_agent_path = runtime_paths.guest_agent_path
|
||||
if guest_agent_path is None:
|
||||
raise AssertionError("expected guest agent path")
|
||||
(platform_root / "guest").mkdir(parents=True, exist_ok=True)
|
||||
(platform_root / "guest" / "pyro_guest_agent.py").write_text(
|
||||
guest_agent_path.read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setenv("PYRO_RUNTIME_BUNDLE_DIR", str(missing_bundle))
|
||||
monkeypatch.setattr(
|
||||
"pyro_mcp.vm_environments.CATALOG",
|
||||
{
|
||||
"debian:12-base": source_environment.__class__(
|
||||
name=source_environment.name,
|
||||
version=source_environment.version,
|
||||
description=source_environment.description,
|
||||
default_packages=source_environment.default_packages,
|
||||
distribution=source_environment.distribution,
|
||||
distribution_version=source_environment.distribution_version,
|
||||
source_profile=source_environment.source_profile,
|
||||
platform=source_environment.platform,
|
||||
source_url=archive_path.resolve().as_uri(),
|
||||
source_digest=source_environment.source_digest,
|
||||
compatibility=source_environment.compatibility,
|
||||
)
|
||||
},
|
||||
)
|
||||
store = EnvironmentStore(
|
||||
runtime_paths=resolve_runtime_paths(verify_checksums=False),
|
||||
cache_dir=tmp_path / "cache",
|
||||
)
|
||||
installed = store.ensure_installed("debian:12-base")
|
||||
|
||||
assert installed.kernel_image.read_text(encoding="utf-8") == "kernel\n"
|
||||
assert installed.rootfs_image.read_text(encoding="utf-8") == "rootfs\n"
|
||||
|
||||
|
||||
def test_environment_store_prunes_stale_entries(tmp_path: Path) -> None:
|
||||
store = EnvironmentStore(runtime_paths=resolve_runtime_paths(), cache_dir=tmp_path / "cache")
|
||||
platform_dir = store.cache_dir / "linux-x86_64"
|
||||
platform_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
(platform_dir / ".partial-download").mkdir()
|
||||
(platform_dir / "missing-marker").mkdir()
|
||||
|
||||
invalid = platform_dir / "invalid"
|
||||
invalid.mkdir()
|
||||
(invalid / "environment.json").write_text('{"name": 1, "version": 2}', encoding="utf-8")
|
||||
|
||||
unknown = platform_dir / "unknown"
|
||||
unknown.mkdir()
|
||||
(unknown / "environment.json").write_text(
|
||||
'{"name": "unknown:1", "version": "1.0.0"}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
stale = platform_dir / "stale"
|
||||
stale.mkdir()
|
||||
(stale / "environment.json").write_text(
|
||||
'{"name": "debian:12", "version": "0.9.0"}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = store.prune_environments()
|
||||
|
||||
assert result["count"] == 5
|
||||
|
|
@ -17,7 +17,12 @@ def test_vm_manager_lifecycle_and_auto_cleanup(tmp_path: Path) -> None:
|
|||
base_dir=tmp_path / "vms",
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
created = manager.create_vm(profile="debian-git", vcpu_count=1, mem_mib=512, ttl_seconds=600)
|
||||
created = manager.create_vm(
|
||||
environment="debian:12",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=600,
|
||||
)
|
||||
vm_id = str(created["vm_id"])
|
||||
started = manager.start_vm(vm_id)
|
||||
assert started["state"] == "started"
|
||||
|
|
@ -37,9 +42,12 @@ def test_vm_manager_exec_timeout(tmp_path: Path) -> None:
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
vm_id = str(
|
||||
manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=600)[
|
||||
"vm_id"
|
||||
]
|
||||
manager.create_vm(
|
||||
environment="debian:12-base",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=600,
|
||||
)["vm_id"]
|
||||
)
|
||||
manager.start_vm(vm_id)
|
||||
result = manager.exec_vm(vm_id, command="sleep 2", timeout_seconds=1)
|
||||
|
|
@ -54,9 +62,12 @@ def test_vm_manager_stop_and_delete(tmp_path: Path) -> None:
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
vm_id = str(
|
||||
manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=600)[
|
||||
"vm_id"
|
||||
]
|
||||
manager.create_vm(
|
||||
environment="debian:12-base",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=600,
|
||||
)["vm_id"]
|
||||
)
|
||||
manager.start_vm(vm_id)
|
||||
stopped = manager.stop_vm(vm_id)
|
||||
|
|
@ -73,7 +84,12 @@ def test_vm_manager_reaps_expired(tmp_path: Path) -> None:
|
|||
)
|
||||
manager.MIN_TTL_SECONDS = 1
|
||||
vm_id = str(
|
||||
manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=1)["vm_id"]
|
||||
manager.create_vm(
|
||||
environment="debian:12-base",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=1,
|
||||
)["vm_id"]
|
||||
)
|
||||
instance = manager._instances[vm_id] # noqa: SLF001
|
||||
instance.expires_at = 0.0
|
||||
|
|
@ -91,7 +107,12 @@ def test_vm_manager_reaps_started_vm(tmp_path: Path) -> None:
|
|||
)
|
||||
manager.MIN_TTL_SECONDS = 1
|
||||
vm_id = str(
|
||||
manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=1)["vm_id"]
|
||||
manager.create_vm(
|
||||
environment="debian:12-base",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=1,
|
||||
)["vm_id"]
|
||||
)
|
||||
manager.start_vm(vm_id)
|
||||
manager._instances[vm_id].expires_at = 0.0 # noqa: SLF001
|
||||
|
|
@ -114,7 +135,7 @@ def test_vm_manager_validates_limits(tmp_path: Path, kwargs: dict[str, Any], msg
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
with pytest.raises(ValueError, match=msg):
|
||||
manager.create_vm(profile="debian-base", **kwargs)
|
||||
manager.create_vm(environment="debian:12-base", **kwargs)
|
||||
|
||||
|
||||
def test_vm_manager_max_active_limit(tmp_path: Path) -> None:
|
||||
|
|
@ -124,9 +145,9 @@ def test_vm_manager_max_active_limit(tmp_path: Path) -> None:
|
|||
max_active_vms=1,
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=600)
|
||||
manager.create_vm(environment="debian:12-base", vcpu_count=1, mem_mib=512, ttl_seconds=600)
|
||||
with pytest.raises(RuntimeError, match="max active VMs reached"):
|
||||
manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=600)
|
||||
manager.create_vm(environment="debian:12-base", vcpu_count=1, mem_mib=512, ttl_seconds=600)
|
||||
|
||||
|
||||
def test_vm_manager_state_validation(tmp_path: Path) -> None:
|
||||
|
|
@ -136,9 +157,12 @@ def test_vm_manager_state_validation(tmp_path: Path) -> None:
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
vm_id = str(
|
||||
manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=600)[
|
||||
"vm_id"
|
||||
]
|
||||
manager.create_vm(
|
||||
environment="debian:12-base",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=600,
|
||||
)["vm_id"]
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="must be in 'started' state"):
|
||||
manager.exec_vm(vm_id, command="echo hi", timeout_seconds=30)
|
||||
|
|
@ -157,7 +181,12 @@ def test_vm_manager_status_expired_raises(tmp_path: Path) -> None:
|
|||
)
|
||||
manager.MIN_TTL_SECONDS = 1
|
||||
vm_id = str(
|
||||
manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=1)["vm_id"]
|
||||
manager.create_vm(
|
||||
environment="debian:12-base",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=1,
|
||||
)["vm_id"]
|
||||
)
|
||||
manager._instances[vm_id].expires_at = 0.0 # noqa: SLF001
|
||||
with pytest.raises(RuntimeError, match="expired and was automatically deleted"):
|
||||
|
|
@ -179,7 +208,12 @@ def test_vm_manager_network_info(tmp_path: Path) -> None:
|
|||
base_dir=tmp_path / "vms",
|
||||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
created = manager.create_vm(profile="debian-base", vcpu_count=1, mem_mib=512, ttl_seconds=600)
|
||||
created = manager.create_vm(
|
||||
environment="debian:12-base",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
ttl_seconds=600,
|
||||
)
|
||||
vm_id = str(created["vm_id"])
|
||||
status = manager.status_vm(vm_id)
|
||||
info = manager.network_info_vm(vm_id)
|
||||
|
|
@ -195,7 +229,7 @@ def test_vm_manager_run_vm(tmp_path: Path) -> None:
|
|||
network_manager=TapNetworkManager(enabled=False),
|
||||
)
|
||||
result = manager.run_vm(
|
||||
profile="debian-base",
|
||||
environment="debian:12-base",
|
||||
command="printf 'ok\\n'",
|
||||
vcpu_count=1,
|
||||
mem_mib=512,
|
||||
|
|
@ -213,13 +247,13 @@ def test_vm_manager_firecracker_backend_path(
|
|||
class StubFirecrackerBackend:
|
||||
def __init__(
|
||||
self,
|
||||
artifacts_dir: Path,
|
||||
environment_store: Any,
|
||||
firecracker_bin: Path,
|
||||
jailer_bin: Path,
|
||||
runtime_capabilities: Any,
|
||||
network_manager: TapNetworkManager,
|
||||
) -> None:
|
||||
self.artifacts_dir = artifacts_dir
|
||||
self.environment_store = environment_store
|
||||
self.firecracker_bin = firecracker_bin
|
||||
self.jailer_bin = jailer_bin
|
||||
self.runtime_capabilities = runtime_capabilities
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pyro_mcp.vm_profiles import get_profile, list_profiles, resolve_artifacts
|
||||
|
||||
|
||||
def test_list_profiles_includes_expected_entries() -> None:
|
||||
profiles = list_profiles()
|
||||
names = {str(entry["name"]) for entry in profiles}
|
||||
assert {"debian-base", "debian-git", "debian-build"} <= names
|
||||
|
||||
|
||||
def test_get_profile_rejects_unknown() -> None:
|
||||
with pytest.raises(ValueError, match="unknown profile"):
|
||||
get_profile("does-not-exist")
|
||||
|
||||
|
||||
def test_resolve_artifacts() -> None:
|
||||
artifacts = resolve_artifacts(Path("/tmp/artifacts"), "debian-git")
|
||||
assert str(artifacts.kernel_image).endswith("/debian-git/vmlinux")
|
||||
assert str(artifacts.rootfs_image).endswith("/debian-git/rootfs.ext4")
|
||||
Loading…
Add table
Add a link
Reference in a new issue