""" Tests for `RulesFileWriter` in scripts/calibrate_helpers.py. Covers tasks 1.1/1.2 of the calibrate-assessment-inventory change: the single canonical serialization path for `.dochygiene-rules.json` — - canonical ordering: rules grouped delete-once-served -> temporary -> keep, glob-sorted within each group; nominations after rules; consults before rejected, each glob-sorted - idempotent round-trip (byte-identical rewrite of an already-canonical file) - hand-edit re-canonicalization (out-of-order input is rewritten canonical) - unknown-field warn-and-round-trip, for both rules and nomination entries sys.path is patched here (no shared conftest), matching the existing tests/test_calibrate_helpers.py pattern. """ from __future__ import annotations import json import sys from pathlib import Path _SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" if str(_SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPTS_DIR)) from calibrate_helpers import RulesFileWriter # noqa: E402 def _rule(glob, lifetime, **extra): d = {"glob": glob, "lifetime": lifetime, "confirmed_by": "human", "confirmed_on": "2026-07-15"} d.update(extra) return d class TestCanonicalOrdering: def test_rules_grouped_by_tier_in_order_and_glob_sorted(self): data = { "schema_version": 1, "rules": [ _rule("z/keep.md", "keep"), _rule("a/keep.md", "keep"), _rule("b/temp/*.md", "temporary"), _rule("a/temp/*.md", "temporary"), _rule("z/served.md", "delete-once-served"), _rule("a/served.md", "delete-once-served"), ], } writer = RulesFileWriter() canonical, _warnings = writer.canonicalize(data) globs = [r["glob"] for r in canonical["rules"]] assert globs == [ "a/served.md", "z/served.md", "a/temp/*.md", "b/temp/*.md", "a/keep.md", "z/keep.md", ] def test_nominations_after_rules_consults_before_rejected_sorted(self): data = { "schema_version": 1, "rules": [_rule("a.md", "keep")], "nominations": { "rejected": [ { "glob": "z/reject.md", "lifetime": "temporary", "why": "why z", "rejected_by": "judge", "judged_on": "2026-07-15", }, { "glob": "a/reject.md", "lifetime": "temporary", "why": "why a", "rejected_by": "judge", "judged_on": "2026-07-15", }, ], "consults": [ { "glob": "z/consult.md", "question": "q z", "evidence": "e", "cluster_key": "k", "asked_on": "2026-07-15", }, { "glob": "a/consult.md", "question": "q a", "evidence": "e", "cluster_key": "k", "asked_on": "2026-07-15", }, ], }, } writer = RulesFileWriter() canonical, _warnings = writer.canonicalize(data) keys = list(canonical.keys()) assert keys.index("rules") < keys.index("nominations") nomination_keys = list(canonical["nominations"].keys()) assert nomination_keys.index("consults") < nomination_keys.index("rejected") assert [c["glob"] for c in canonical["nominations"]["consults"]] == [ "a/consult.md", "z/consult.md", ] assert [r["glob"] for r in canonical["nominations"]["rejected"]] == [ "a/reject.md", "z/reject.md", ] def test_nominations_omitted_when_absent(self): data = {"schema_version": 1, "rules": [_rule("a.md", "keep")]} writer = RulesFileWriter() canonical, _warnings = writer.canonicalize(data) assert "nominations" not in canonical def test_nominations_omitted_when_present_but_empty(self): data = { "schema_version": 1, "rules": [_rule("a.md", "keep")], "nominations": {"consults": [], "rejected": []}, } writer = RulesFileWriter() canonical, _warnings = writer.canonicalize(data) assert "nominations" not in canonical class TestIdempotentRoundTrip: def test_writing_an_already_canonical_file_is_byte_identical(self, tmp_path): data = { "schema_version": 1, "rules": [ _rule("a/served.md", "delete-once-served"), _rule("a/temp/*.md", "temporary"), _rule("a/keep.md", "keep"), ], "nominations": { "consults": [ { "glob": "a/consult.md", "question": "q", "evidence": "e", "cluster_key": "k", "asked_on": "2026-07-15", } ], "rejected": [ { "glob": "a/reject.md", "lifetime": "temporary", "why": "why", "rejected_by": "judge", "judged_on": "2026-07-15", } ], }, } writer = RulesFileWriter() path = tmp_path / ".dochygiene-rules.json" writer.write(path, data) first_bytes = path.read_bytes() loaded, _warnings = writer.load(path) writer.write(path, loaded) second_bytes = path.read_bytes() assert first_bytes == second_bytes class TestHandEditRecanonicalization: def test_hand_edited_out_of_order_file_recanonicalizes_on_next_write(self, tmp_path): # Hand-authored file: keep rule listed before temporary/served rules, # rejected before consults -- out of canonical order. hand_edited = { "schema_version": 1, "rules": [ _rule("a/keep.md", "keep"), _rule("a/served.md", "delete-once-served"), _rule("a/temp/*.md", "temporary"), ], "nominations": { "rejected": [ { "glob": "a/reject.md", "lifetime": "temporary", "why": "why", "rejected_by": "judge", "judged_on": "2026-07-15", } ], "consults": [ { "glob": "a/consult.md", "question": "q", "evidence": "e", "cluster_key": "k", "asked_on": "2026-07-15", } ], }, } path = tmp_path / ".dochygiene-rules.json" path.write_text(json.dumps(hand_edited, indent=2), encoding="utf-8") writer = RulesFileWriter() loaded, _warnings = writer.load(path) writer.write(path, loaded) on_disk = json.loads(path.read_text(encoding="utf-8")) assert [r["glob"] for r in on_disk["rules"]] == [ "a/served.md", "a/temp/*.md", "a/keep.md", ] nomination_keys = list(on_disk["nominations"].keys()) assert nomination_keys.index("consults") < nomination_keys.index("rejected") class TestUnknownFieldsWarnAndRoundtrip: def test_unknown_rule_field_warns_and_is_preserved(self): data = { "schema_version": 1, "rules": [_rule("a.md", "keep", mystery_field="surprise")], } writer = RulesFileWriter() canonical, warnings = writer.canonicalize(data) assert canonical["rules"][0]["mystery_field"] == "surprise" assert any("mystery_field" in w for w in warnings) def test_unknown_consult_field_warns_and_is_preserved(self): data = { "schema_version": 1, "rules": [], "nominations": { "consults": [ { "glob": "a/consult.md", "question": "q", "evidence": "e", "cluster_key": "k", "asked_on": "2026-07-15", "extra_field": "unexpected", } ], "rejected": [], }, } writer = RulesFileWriter() canonical, warnings = writer.canonicalize(data) assert canonical["nominations"]["consults"][0]["extra_field"] == "unexpected" assert any("extra_field" in w for w in warnings) def test_unknown_rejected_field_warns_and_is_preserved(self): data = { "schema_version": 1, "rules": [], "nominations": { "consults": [], "rejected": [ { "glob": "a/reject.md", "lifetime": "temporary", "why": "why", "rejected_by": "judge", "judged_on": "2026-07-15", "surprise_field": "oops", } ], }, } writer = RulesFileWriter() canonical, warnings = writer.canonicalize(data) assert canonical["nominations"]["rejected"][0]["surprise_field"] == "oops" assert any("surprise_field" in w for w in warnings) class TestLoad: def test_load_reads_file_and_returns_canonical_data(self, tmp_path): data = { "schema_version": 1, "rules": [_rule("a/keep.md", "keep")], } path = tmp_path / ".dochygiene-rules.json" path.write_text(json.dumps(data), encoding="utf-8") writer = RulesFileWriter() loaded, warnings = writer.load(path) assert loaded["rules"][0]["glob"] == "a/keep.md" assert warnings == [] def test_load_missing_file_returns_empty_envelope(self, tmp_path): path = tmp_path / "does-not-exist.json" writer = RulesFileWriter() loaded, warnings = writer.load(path) assert loaded["rules"] == [] assert "nominations" not in loaded assert warnings == []