304 lines
11 KiB
Python
304 lines
11 KiB
Python
|
|
"""
|
||
|
|
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 == []
|