Add daily-loop prepare and readiness checks

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`.
This commit is contained in:
Thales Maciel 2026-03-13 21:17:59 -03:00
parent d0cf6d8f21
commit 663241d5d2
26 changed files with 1592 additions and 199 deletions

View file

@ -6,7 +6,32 @@ from pathlib import Path
import pytest
from pyro_mcp.daily_loop import DailyLoopManifest, prepare_manifest_path, write_prepare_manifest
from pyro_mcp.runtime import doctor_report, resolve_runtime_paths, runtime_capabilities
from pyro_mcp.vm_environments import EnvironmentStore, get_environment
def _materialize_installed_environment(
environment_store: EnvironmentStore,
*,
name: str,
) -> None:
spec = get_environment(name, runtime_paths=environment_store._runtime_paths)
install_dir = environment_store._install_dir(spec)
install_dir.mkdir(parents=True, exist_ok=True)
(install_dir / "vmlinux").write_text("kernel\n", encoding="utf-8")
(install_dir / "rootfs.ext4").write_text("rootfs\n", encoding="utf-8")
(install_dir / "environment.json").write_text(
json.dumps(
{
"name": spec.name,
"version": spec.version,
"source": "test-cache",
"source_digest": spec.source_digest,
}
),
encoding="utf-8",
)
def test_resolve_runtime_paths_default_bundle() -> None:
@ -109,6 +134,7 @@ def test_doctor_report_has_runtime_fields() -> None:
assert "runtime_ok" in report
assert "kvm" in report
assert "networking" in report
assert "daily_loop" in report
if report["runtime_ok"]:
runtime = report.get("runtime")
assert isinstance(runtime, dict)
@ -122,6 +148,61 @@ def test_doctor_report_has_runtime_fields() -> None:
assert "tun_available" in networking
def test_doctor_report_daily_loop_statuses(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setenv("PYRO_ENVIRONMENT_CACHE_DIR", str(tmp_path))
cold_report = doctor_report(environment="debian:12")
cold_daily_loop = cold_report["daily_loop"]
assert cold_daily_loop["status"] == "cold"
assert cold_daily_loop["installed"] is False
paths = resolve_runtime_paths()
environment_store = EnvironmentStore(runtime_paths=paths, cache_dir=tmp_path)
_materialize_installed_environment(environment_store, name="debian:12")
installed_report = doctor_report(environment="debian:12")
installed_daily_loop = installed_report["daily_loop"]
assert installed_daily_loop["status"] == "cold"
assert installed_daily_loop["installed"] is True
manifest_path = prepare_manifest_path(
tmp_path,
platform="linux-x86_64",
environment="debian:12",
)
write_prepare_manifest(
manifest_path,
DailyLoopManifest(
environment="debian:12",
environment_version="1.0.0",
platform="linux-x86_64",
catalog_version=environment_store.catalog_version,
bundle_version=(
None
if paths.manifest.get("bundle_version") is None
else str(paths.manifest.get("bundle_version"))
),
prepared_at=123.0,
network_prepared=True,
last_prepare_duration_ms=456,
),
)
warm_report = doctor_report(environment="debian:12")
warm_daily_loop = warm_report["daily_loop"]
assert warm_daily_loop["status"] == "warm"
assert warm_daily_loop["network_prepared"] is True
stale_manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
stale_manifest["catalog_version"] = "0.0.0"
manifest_path.write_text(json.dumps(stale_manifest), encoding="utf-8")
stale_report = doctor_report(environment="debian:12")
stale_daily_loop = stale_report["daily_loop"]
assert stale_daily_loop["status"] == "stale"
assert "catalog version changed" in str(stale_daily_loop["reason"])
def test_runtime_capabilities_reports_real_bundle_flags() -> None:
paths = resolve_runtime_paths()
capabilities = runtime_capabilities(paths)