From 6272d2997802cd50fa8e9081bc905924d58f54c9 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 12 Sep 2026 18:55:34 +0000 Subject: [PATCH] no-mistakes(review): Fail closed on empty report-only exclusion list --- scripts/disk-gc-plan.py | 5 +++++ tests/test_disk_gc_report_only.py | 24 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/scripts/disk-gc-plan.py b/scripts/disk-gc-plan.py index b3fa42a..ca1e867 100755 --- a/scripts/disk-gc-plan.py +++ b/scripts/disk-gc-plan.py @@ -59,6 +59,11 @@ def load_report_only_guests(contract_path: pathlib.Path) -> list[dict]: guests = data.get("report_only_guests") or [] if not isinstance(guests, list): raise SystemExit("report_only_guests must be a list") + if not guests: + raise SystemExit( + "report_only_guests is empty or missing - refusing to plan GC " + "without the report-only gate" + ) for guest in guests: if not isinstance(guest, dict) or not _keys(guest): raise SystemExit( diff --git a/tests/test_disk_gc_report_only.py b/tests/test_disk_gc_report_only.py index f3b4e48..cd19d82 100644 --- a/tests/test_disk_gc_report_only.py +++ b/tests/test_disk_gc_report_only.py @@ -124,6 +124,22 @@ def test_exclusion_entry_without_identity_fails_closed(tmp_path): assert "identity" in proc.stderr.lower(), proc.stderr +def test_empty_exclusion_block_fails_closed(tmp_path): + """An emptied report_only_guests list must break the run, not disable the gate.""" + contract = tmp_path / "empty.prose.md" + contract.write_text("```yaml\nreport_only_guests: []\n```\n") + scan_file = tmp_path / "scan.json" + scan_file.write_text(json.dumps([{"ct": 111, "usage_pct": 97}])) + 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.stdout + assert "gc-executor" not in proc.stdout + assert "report-only gate" in proc.stderr.lower(), proc.stderr + + def _plan_with_contract(scan, contract_text, tmp_path, name): contract = tmp_path / name contract.write_text(contract_text) @@ -152,7 +168,13 @@ def test_gate_is_read_from_the_contract_block(tmp_path): " reason: \"fixture reason\"\n" "```\n" ) - without_gate = "```yaml\nreport_only_guests: []\n```\n" + without_gate = ( + "```yaml\n" + "report_only_guests:\n" + " - guest: 999\n" + " reason: \"fixture excludes a different guest\"\n" + "```\n" + ) gated = _plan_with_contract(scan, with_gate, tmp_path, "gated.prose.md") assert gated[0]["action"] == "report-only", gated