pyro-mcp/tests/test_public_contract.py
Thales Maciel dc86d84e96 Add workspace review summaries
Add workspace summary across the CLI, SDK, and MCP, and include it in the workspace-core profile so chat hosts can review one concise view of the current session.

Persist lightweight review events for syncs, file edits, patch applies, exports, service lifecycle, and snapshot activity, then synthesize them with command history, current services, snapshot state, and current diff data since the last reset.

Update the walkthroughs, use-case docs, public contract, changelog, and roadmap for 4.3.0, and make dist-check invoke the CLI module directly so local package reinstall quirks do not break the packaging gate.

Validation: uv lock; ./.venv/bin/pytest --no-cov tests/test_vm_manager.py tests/test_cli.py tests/test_api.py tests/test_server.py tests/test_public_contract.py tests/test_workspace_use_case_smokes.py; UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make check; UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make dist-check; real guest-backed workspace create -> patch apply -> workspace summary --json -> delete smoke.
2026-03-13 19:21:11 -03:00

378 lines
16 KiB
Python

from __future__ import annotations
import argparse
import asyncio
import io
import tomllib
from contextlib import redirect_stdout
from pathlib import Path
from typing import Any, cast
import pytest
from pyro_mcp import Pyro, __version__
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_HOST_CONNECT_FLAGS,
PUBLIC_CLI_HOST_DOCTOR_FLAGS,
PUBLIC_CLI_HOST_PRINT_CONFIG_FLAGS,
PUBLIC_CLI_HOST_REPAIR_FLAGS,
PUBLIC_CLI_HOST_SUBCOMMANDS,
PUBLIC_CLI_MCP_SERVE_FLAGS,
PUBLIC_CLI_MCP_SUBCOMMANDS,
PUBLIC_CLI_RUN_FLAGS,
PUBLIC_CLI_WORKSPACE_CREATE_FLAGS,
PUBLIC_CLI_WORKSPACE_DIFF_FLAGS,
PUBLIC_CLI_WORKSPACE_DISK_EXPORT_FLAGS,
PUBLIC_CLI_WORKSPACE_DISK_LIST_FLAGS,
PUBLIC_CLI_WORKSPACE_DISK_READ_FLAGS,
PUBLIC_CLI_WORKSPACE_EXEC_FLAGS,
PUBLIC_CLI_WORKSPACE_EXPORT_FLAGS,
PUBLIC_CLI_WORKSPACE_FILE_LIST_FLAGS,
PUBLIC_CLI_WORKSPACE_FILE_READ_FLAGS,
PUBLIC_CLI_WORKSPACE_FILE_SUBCOMMANDS,
PUBLIC_CLI_WORKSPACE_FILE_WRITE_FLAGS,
PUBLIC_CLI_WORKSPACE_LIST_FLAGS,
PUBLIC_CLI_WORKSPACE_PATCH_APPLY_FLAGS,
PUBLIC_CLI_WORKSPACE_PATCH_SUBCOMMANDS,
PUBLIC_CLI_WORKSPACE_RESET_FLAGS,
PUBLIC_CLI_WORKSPACE_SERVICE_LIST_FLAGS,
PUBLIC_CLI_WORKSPACE_SERVICE_LOGS_FLAGS,
PUBLIC_CLI_WORKSPACE_SERVICE_START_FLAGS,
PUBLIC_CLI_WORKSPACE_SERVICE_STATUS_FLAGS,
PUBLIC_CLI_WORKSPACE_SERVICE_STOP_FLAGS,
PUBLIC_CLI_WORKSPACE_SERVICE_SUBCOMMANDS,
PUBLIC_CLI_WORKSPACE_SHELL_CLOSE_FLAGS,
PUBLIC_CLI_WORKSPACE_SHELL_OPEN_FLAGS,
PUBLIC_CLI_WORKSPACE_SHELL_READ_FLAGS,
PUBLIC_CLI_WORKSPACE_SHELL_SIGNAL_FLAGS,
PUBLIC_CLI_WORKSPACE_SHELL_SUBCOMMANDS,
PUBLIC_CLI_WORKSPACE_SHELL_WRITE_FLAGS,
PUBLIC_CLI_WORKSPACE_SNAPSHOT_CREATE_FLAGS,
PUBLIC_CLI_WORKSPACE_SNAPSHOT_DELETE_FLAGS,
PUBLIC_CLI_WORKSPACE_SNAPSHOT_LIST_FLAGS,
PUBLIC_CLI_WORKSPACE_SNAPSHOT_SUBCOMMANDS,
PUBLIC_CLI_WORKSPACE_START_FLAGS,
PUBLIC_CLI_WORKSPACE_STOP_FLAGS,
PUBLIC_CLI_WORKSPACE_SUBCOMMANDS,
PUBLIC_CLI_WORKSPACE_SUMMARY_FLAGS,
PUBLIC_CLI_WORKSPACE_SYNC_PUSH_FLAGS,
PUBLIC_CLI_WORKSPACE_SYNC_SUBCOMMANDS,
PUBLIC_CLI_WORKSPACE_UPDATE_FLAGS,
PUBLIC_MCP_PROFILES,
PUBLIC_MCP_WORKSPACE_CORE_PROFILE_TOOLS,
PUBLIC_SDK_METHODS,
)
from pyro_mcp.vm_manager import VmManager
from pyro_mcp.vm_network import TapNetworkManager
def _subparser_choice(parser: argparse.ArgumentParser, name: str) -> argparse.ArgumentParser:
subparsers = getattr(parser, "_subparsers", None)
if subparsers is None:
raise AssertionError("parser does not define subparsers")
group_actions = cast(list[Any], subparsers._group_actions) # noqa: SLF001
if not group_actions:
raise AssertionError("parser subparsers are empty")
choices = cast(dict[str, argparse.ArgumentParser], group_actions[0].choices)
return choices[name]
def test_public_sdk_methods_exist() -> None:
assert tuple(sorted(PUBLIC_SDK_METHODS)) == PUBLIC_SDK_METHODS
for method_name in PUBLIC_SDK_METHODS:
assert hasattr(Pyro, method_name), method_name
def test_public_cli_help_lists_commands_and_run_flags() -> None:
parser = _build_parser()
help_text = parser.format_help()
assert "--version" in help_text
for command_name in PUBLIC_CLI_COMMANDS:
assert command_name in help_text
run_parser = _build_parser()
run_help = run_parser.parse_args(["run", "debian:12-base", "--", "true"])
assert run_help.command == "run"
assert run_help.environment == "debian:12-base"
assert run_help.vcpu_count == 1
assert run_help.mem_mib == 1024
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
host_help_text = _subparser_choice(parser, "host").format_help()
for subcommand_name in PUBLIC_CLI_HOST_SUBCOMMANDS:
assert subcommand_name in host_help_text
host_connect_help_text = _subparser_choice(
_subparser_choice(parser, "host"), "connect"
).format_help()
for flag in PUBLIC_CLI_HOST_CONNECT_FLAGS:
assert flag in host_connect_help_text
host_doctor_help_text = _subparser_choice(
_subparser_choice(parser, "host"), "doctor"
).format_help()
for flag in PUBLIC_CLI_HOST_DOCTOR_FLAGS:
assert flag in host_doctor_help_text
host_print_config_help_text = _subparser_choice(
_subparser_choice(parser, "host"), "print-config"
).format_help()
for flag in PUBLIC_CLI_HOST_PRINT_CONFIG_FLAGS:
assert flag in host_print_config_help_text
host_repair_help_text = _subparser_choice(
_subparser_choice(parser, "host"), "repair"
).format_help()
for flag in PUBLIC_CLI_HOST_REPAIR_FLAGS:
assert flag in host_repair_help_text
mcp_help_text = _subparser_choice(parser, "mcp").format_help()
for subcommand_name in PUBLIC_CLI_MCP_SUBCOMMANDS:
assert subcommand_name in mcp_help_text
mcp_serve_help_text = _subparser_choice(_subparser_choice(parser, "mcp"), "serve").format_help()
for flag in PUBLIC_CLI_MCP_SERVE_FLAGS:
assert flag in mcp_serve_help_text
for profile_name in PUBLIC_MCP_PROFILES:
assert profile_name in mcp_serve_help_text
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_WORKSPACE_CREATE_FLAGS:
assert flag in workspace_create_help_text
workspace_exec_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "exec"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_EXEC_FLAGS:
assert flag in workspace_exec_help_text
workspace_sync_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"),
"sync",
).format_help()
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
workspace_export_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "export"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_EXPORT_FLAGS:
assert flag in workspace_export_help_text
workspace_list_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "list"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_LIST_FLAGS:
assert flag in workspace_list_help_text
workspace_update_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "update"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_UPDATE_FLAGS:
assert flag in workspace_update_help_text
workspace_file_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "file"
).format_help()
for subcommand_name in PUBLIC_CLI_WORKSPACE_FILE_SUBCOMMANDS:
assert subcommand_name in workspace_file_help_text
workspace_file_list_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "file"), "list"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_FILE_LIST_FLAGS:
assert flag in workspace_file_list_help_text
workspace_file_read_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "file"), "read"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_FILE_READ_FLAGS:
assert flag in workspace_file_read_help_text
workspace_file_write_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "file"), "write"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_FILE_WRITE_FLAGS:
assert flag in workspace_file_write_help_text
workspace_patch_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "patch"
).format_help()
for subcommand_name in PUBLIC_CLI_WORKSPACE_PATCH_SUBCOMMANDS:
assert subcommand_name in workspace_patch_help_text
workspace_patch_apply_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "patch"), "apply"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_PATCH_APPLY_FLAGS:
assert flag in workspace_patch_apply_help_text
workspace_disk_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "disk"
).format_help()
for subcommand_name in ("export", "list", "read"):
assert subcommand_name in workspace_disk_help_text
workspace_disk_export_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "disk"), "export"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_DISK_EXPORT_FLAGS:
assert flag in workspace_disk_export_help_text
workspace_disk_list_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "disk"), "list"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_DISK_LIST_FLAGS:
assert flag in workspace_disk_list_help_text
workspace_disk_read_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "disk"), "read"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_DISK_READ_FLAGS:
assert flag in workspace_disk_read_help_text
workspace_diff_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "diff"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_DIFF_FLAGS:
assert flag in workspace_diff_help_text
workspace_snapshot_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"),
"snapshot",
).format_help()
for subcommand_name in PUBLIC_CLI_WORKSPACE_SNAPSHOT_SUBCOMMANDS:
assert subcommand_name in workspace_snapshot_help_text
workspace_snapshot_create_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "snapshot"),
"create",
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SNAPSHOT_CREATE_FLAGS:
assert flag in workspace_snapshot_create_help_text
workspace_snapshot_list_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "snapshot"),
"list",
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SNAPSHOT_LIST_FLAGS:
assert flag in workspace_snapshot_list_help_text
workspace_snapshot_delete_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "snapshot"),
"delete",
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SNAPSHOT_DELETE_FLAGS:
assert flag in workspace_snapshot_delete_help_text
workspace_reset_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "reset"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_RESET_FLAGS:
assert flag in workspace_reset_help_text
workspace_start_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "start"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_START_FLAGS:
assert flag in workspace_start_help_text
workspace_stop_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "stop"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_STOP_FLAGS:
assert flag in workspace_stop_help_text
workspace_summary_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"), "summary"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SUMMARY_FLAGS:
assert flag in workspace_summary_help_text
workspace_shell_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"),
"shell",
).format_help()
for subcommand_name in PUBLIC_CLI_WORKSPACE_SHELL_SUBCOMMANDS:
assert subcommand_name in workspace_shell_help_text
workspace_shell_open_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "shell"), "open"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SHELL_OPEN_FLAGS:
assert flag in workspace_shell_open_help_text
workspace_shell_read_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "shell"), "read"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SHELL_READ_FLAGS:
assert flag in workspace_shell_read_help_text
workspace_shell_write_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "shell"), "write"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SHELL_WRITE_FLAGS:
assert flag in workspace_shell_write_help_text
workspace_shell_signal_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "shell"), "signal"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SHELL_SIGNAL_FLAGS:
assert flag in workspace_shell_signal_help_text
workspace_shell_close_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "shell"), "close"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SHELL_CLOSE_FLAGS:
assert flag in workspace_shell_close_help_text
workspace_service_help_text = _subparser_choice(
_subparser_choice(parser, "workspace"),
"service",
).format_help()
for subcommand_name in PUBLIC_CLI_WORKSPACE_SERVICE_SUBCOMMANDS:
assert subcommand_name in workspace_service_help_text
workspace_service_start_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "service"), "start"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SERVICE_START_FLAGS:
assert flag in workspace_service_start_help_text
workspace_service_list_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "service"), "list"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SERVICE_LIST_FLAGS:
assert flag in workspace_service_list_help_text
workspace_service_status_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "service"), "status"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SERVICE_STATUS_FLAGS:
assert flag in workspace_service_status_help_text
workspace_service_logs_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "service"), "logs"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SERVICE_LOGS_FLAGS:
assert flag in workspace_service_logs_help_text
workspace_service_stop_help_text = _subparser_choice(
_subparser_choice(_subparser_choice(parser, "workspace"), "service"), "stop"
).format_help()
for flag in PUBLIC_CLI_WORKSPACE_SERVICE_STOP_FLAGS:
assert flag in workspace_service_stop_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
def test_public_cli_version_matches_package_version() -> None:
parser = _build_parser()
stdout = io.StringIO()
with pytest.raises(SystemExit, match="0"), redirect_stdout(stdout):
parser.parse_args(["--version"])
assert stdout.getvalue().strip().endswith(f" {__version__}")
def test_public_mcp_tools_match_contract(tmp_path: Path) -> None:
pyro = Pyro(
manager=VmManager(
backend_name="mock",
base_dir=tmp_path / "vms",
network_manager=TapNetworkManager(enabled=False),
)
)
async def _run() -> tuple[str, ...]:
server = pyro.create_server()
tools = await server.list_tools()
return tuple(sorted(tool.name for tool in tools))
tool_names = asyncio.run(_run())
assert tool_names == tuple(sorted(PUBLIC_MCP_WORKSPACE_CORE_PROFILE_TOOLS))
def test_pyproject_exposes_single_public_cli_script() -> None:
pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
scripts = pyproject["project"]["scripts"]
assert scripts == {"pyro": "pyro_mcp.cli:main"}