Make the local chat-host loop explicit and cheap so users can warm the machine once instead of rediscovering environment and guest setup on every session. Add cache-backed daily-loop manifests plus the new `pyro prepare` flow, extend `pyro doctor --environment` with warm/cold/stale readiness reporting, and add `make smoke-daily-loop` to prove the warmed repro-fix reset path end to end. Also fix `python -m pyro_mcp.cli` to invoke `main()` so the new smoke and `dist-check` actually exercise the CLI module, and update the docs/roadmap to present `doctor -> prepare -> connect host -> reset` as the recommended daily path. Validation: `uv lock`, `UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make check`, `UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make dist-check`, and `UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make smoke-daily-loop`.
138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
import pyro_mcp.daily_loop_smoke as smoke_module
|
|
|
|
|
|
class _FakePyro:
|
|
def __init__(self) -> None:
|
|
self.workspace_id = "ws-1"
|
|
self.message = "broken\n"
|
|
self.deleted = False
|
|
|
|
def create_workspace(
|
|
self,
|
|
*,
|
|
environment: str,
|
|
seed_path: Path,
|
|
name: str | None = None,
|
|
labels: dict[str, str] | None = None,
|
|
) -> dict[str, object]:
|
|
assert environment == "debian:12"
|
|
assert seed_path.is_dir()
|
|
assert name == "daily-loop"
|
|
assert labels == {"suite": "daily-loop-smoke"}
|
|
return {"workspace_id": self.workspace_id}
|
|
|
|
def exec_workspace(self, workspace_id: str, *, command: str) -> dict[str, object]:
|
|
assert workspace_id == self.workspace_id
|
|
if command != "sh check.sh":
|
|
raise AssertionError(f"unexpected command: {command}")
|
|
if self.message == "fixed\n":
|
|
return {"exit_code": 0, "stdout": "fixed\n"}
|
|
return {"exit_code": 1, "stderr": "expected fixed got broken\n"}
|
|
|
|
def apply_workspace_patch(self, workspace_id: str, *, patch: str) -> dict[str, object]:
|
|
assert workspace_id == self.workspace_id
|
|
assert "+fixed" in patch
|
|
self.message = "fixed\n"
|
|
return {"changed": True}
|
|
|
|
def export_workspace(
|
|
self,
|
|
workspace_id: str,
|
|
path: str,
|
|
*,
|
|
output_path: Path,
|
|
) -> dict[str, object]:
|
|
assert workspace_id == self.workspace_id
|
|
assert path == "message.txt"
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(self.message, encoding="utf-8")
|
|
return {"artifact_type": "file"}
|
|
|
|
def reset_workspace(self, workspace_id: str) -> dict[str, object]:
|
|
assert workspace_id == self.workspace_id
|
|
self.message = "broken\n"
|
|
return {"reset_count": 1}
|
|
|
|
def read_workspace_file(self, workspace_id: str, path: str) -> dict[str, object]:
|
|
assert workspace_id == self.workspace_id
|
|
assert path == "message.txt"
|
|
return {"content": self.message}
|
|
|
|
def delete_workspace(self, workspace_id: str) -> dict[str, object]:
|
|
assert workspace_id == self.workspace_id
|
|
self.deleted = True
|
|
return {"workspace_id": workspace_id, "deleted": True}
|
|
|
|
|
|
def test_run_prepare_parses_json(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
subprocess,
|
|
"run",
|
|
lambda *args, **kwargs: SimpleNamespace(
|
|
returncode=0,
|
|
stdout=json.dumps({"prepared": True}),
|
|
stderr="",
|
|
),
|
|
)
|
|
payload = smoke_module._run_prepare("debian:12")
|
|
assert payload == {"prepared": True}
|
|
|
|
|
|
def test_run_prepare_raises_on_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
subprocess,
|
|
"run",
|
|
lambda *args, **kwargs: SimpleNamespace(
|
|
returncode=1,
|
|
stdout="",
|
|
stderr="prepare failed",
|
|
),
|
|
)
|
|
with pytest.raises(RuntimeError, match="prepare failed"):
|
|
smoke_module._run_prepare("debian:12")
|
|
|
|
|
|
def test_run_daily_loop_smoke_happy_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
prepare_calls: list[str] = []
|
|
fake_pyro = _FakePyro()
|
|
|
|
def fake_run_prepare(environment: str) -> dict[str, object]:
|
|
prepare_calls.append(environment)
|
|
return {"prepared": True, "reused": len(prepare_calls) > 1}
|
|
|
|
monkeypatch.setattr(smoke_module, "_run_prepare", fake_run_prepare)
|
|
monkeypatch.setattr(smoke_module, "Pyro", lambda: fake_pyro)
|
|
|
|
smoke_module.run_daily_loop_smoke(environment="debian:12")
|
|
|
|
assert prepare_calls == ["debian:12", "debian:12"]
|
|
assert fake_pyro.deleted is True
|
|
|
|
|
|
def test_main_runs_selected_environment(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
observed: list[str] = []
|
|
monkeypatch.setattr(
|
|
smoke_module,
|
|
"run_daily_loop_smoke",
|
|
lambda *, environment: observed.append(environment),
|
|
)
|
|
monkeypatch.setattr(
|
|
smoke_module,
|
|
"build_arg_parser",
|
|
lambda: SimpleNamespace(
|
|
parse_args=lambda: SimpleNamespace(environment="debian:12-build")
|
|
),
|
|
)
|
|
smoke_module.main()
|
|
assert observed == ["debian:12-build"]
|