"""Regression tests for the disk-gc report-only gate (CT 111 / tdunna / .129). WHY THIS FILE EXISTS: `disk-gc-threat-response.prose.md` defined AMBER as "GC scheduled for next run" and its Execution loop called `gc-executor` for EVERY threat, with no guest-level exclusion. CT 111 (tdunna, 192.168.68.129) belongs to Theo and is report-only per the captain (2026-08-17, re-confirmed 2026-09-10) — so a single AMBER reading on that guest would have scheduled GC commands (apt clean, journal vacuum, log/tmp deletion, snap removal) against someone else's box. The only marker was frontmatter `report_only_agents`, which names an AGENT while the scan unit is a GUEST. These tests execute the real planner (`scripts/disk-gc-plan.py`) and assert observable behaviour: an excluded guest never produces a `gc-executor` action at any level, while our own guests still do. """ from __future__ import annotations import json import pathlib import subprocess import sys ROOT = pathlib.Path(__file__).resolve().parent.parent PLAN = ROOT / "scripts" / "disk-gc-plan.py" def _plan(scan, tmp_path): scan_file = tmp_path / "scan.json" scan_file.write_text(json.dumps(scan)) proc = subprocess.run( [sys.executable, str(PLAN), "--scan", str(scan_file), "--json"], capture_output=True, text=True, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def _actions_for(plan, target): return [row for row in plan if str(row["target"]) == str(target)] def test_excluded_guest_never_gets_gc_at_any_level(tmp_path): """CT 111 at AMBER, RED and CRITICAL — always report-only, never gc-executor.""" for pct, level in ((84, "AMBER"), (90, "RED"), (97, "CRITICAL")): plan = _plan([{"id": 111, "hostname": "tdunna", "ip": "192.168.68.129", "usage_pct": pct}], tmp_path) rows = _actions_for(plan, 111) assert rows, f"CT 111 must still be reported at {level}" assert rows[0]["level"] == level assert rows[0]["action"] == "report-only", rows assert not any(r["action"] == "gc-executor" for r in rows) def test_exclusion_matches_on_any_identity_key(tmp_path): """The gate is keyed on guest/host, so id, hostname or IP all match.""" for entry in ({"id": 111, "usage_pct": 95}, {"hostname": "tdunna", "usage_pct": 95}, {"ip": "192.168.68.129", "usage_pct": 95}): plan = _plan([entry], tmp_path) assert all(r["action"] == "report-only" for r in plan), (entry, plan) def test_our_own_guests_still_get_gc(tmp_path): """acerpve .9 and amdpve .15 are ours — they must still be acted on.""" plan = _plan([{"hostname": "acerpve", "ip": "192.168.68.9", "usage_pct": 77}, {"hostname": "amdpve", "ip": "192.168.68.15", "usage_pct": 76}], tmp_path) assert len(plan) == 2 assert all(r["action"] == "gc-executor" for r in plan), plan def test_below_threshold_emits_nothing(tmp_path): """GREEN guests produce no action at all.""" assert _plan([{"id": 111, "usage_pct": 40}], tmp_path) == [] def test_agent_name_alone_does_not_gate_a_guest(tmp_path): """An agent-name marker must not be the gate: an unrelated guest still gets GC.""" plan = _plan([{"id": 999, "hostname": "koby", "usage_pct": 95}], tmp_path) assert plan and plan[0]["action"] == "gc-executor" def _plan_with_contract(scan, contract_text, tmp_path, name): contract = tmp_path / name contract.write_text(contract_text) scan_file = tmp_path / f"scan-{name}.json" scan_file.write_text(json.dumps(scan)) proc = subprocess.run( [sys.executable, str(PLAN), "--scan", str(scan_file), "--contract", str(contract), "--json"], capture_output=True, text=True, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_gate_is_read_from_the_contract_block(tmp_path): """The gate is data-driven by the contract block: the planner excludes the guest when the block names it and acts on it when the block does not. Executes the real planner interface against both fixtures so the behaviour change is observable.""" scan = [{"id": 111, "hostname": "tdunna", "ip": "192.168.68.129", "usage_pct": 95}] with_gate = ( "```yaml\n" "report_only_guests:\n" " - guest: 111\n" " hostname: tdunna\n" " ip: 192.168.68.129\n" " reason: \"fixture reason\"\n" "```\n" ) without_gate = "```yaml\nreport_only_guests: []\n```\n" gated = _plan_with_contract(scan, with_gate, tmp_path, "gated.prose.md") assert gated[0]["action"] == "report-only", gated assert gated[0]["reason"] == "fixture reason", gated ungated = _plan_with_contract(scan, without_gate, tmp_path, "ungated.prose.md") assert ungated[0]["action"] == "gc-executor", ungated assert ungated[0]["action"] != gated[0]["action"]