"""Equivalence harness for the #610 one-file-per-record migration. The migration is a MOVE: content relocates, nothing is rewritten. That makes correctness *provable* rather than reviewable — parse the corpus before, run the migration, parse it after, and assert the two Record sets are identical field-for-field with byte-identical rationale. **Runs against a checked-in FIXTURE, not the live corpus.** Migrating the real `docs/` tree would make this a one-shot: once the real migration landed, the tree is already split, the harness has nothing to move, and the tests fail for a reason that has nothing to do with the code under test. A fixture keeps these tests exercising the *script* rather than the repo's current state, so they stay meaningful and green after the migration ships. The one-off proof against the real corpus was run separately at migration time: 168 records before and after, zero lost, zero gained, zero field mismatches, zero rationale bodies differing. """ from __future__ import annotations import shutil from pathlib import Path import pytest import scripts.decisions_lib as dl import scripts.migrate_decisions_split as mig def _independent_strip(rec: dl.Record) -> str: """Strip the metadata block WITHOUT reusing the migration's own helper. Deliberately a second implementation. Computing the "before" baseline with `mig._rationale_body` made the byte-identity proof circular: a bug in that one function corrupts both sides equally and the assertion still passes. PROVED: collapsing interior blank lines there left all harness tests green. """ lines = rec.body.split("\n") while lines and not lines[0].strip(): lines.pop(0) if lines and lines[0].lstrip().startswith("`key:"): lines.pop(0) while lines and lines[0].strip(): lines.pop(0) return "\n".join(lines).strip() FIXTURE = Path(__file__).parent / "fixtures" / "premigration" COMPARED_FIELDS = ( "key", "status", "since", "supersedes", "superseded_by", "stale_after", "rule", "signals", "mechanics", "sources", ) # The fixture is static, so an exact count is a real completeness guard here rather than a number # that rots every time a record lands on main. EXPECTED_RECORDS = 4 @pytest.fixture(scope="module") def migrated(tmp_path_factory): """Copy the fixture corpus to tmp, point decisions_lib at it, run the real migration.""" tmp = tmp_path_factory.mktemp("corpus") shutil.copytree(FIXTURE, tmp / "docs") orig = (dl.REPO_ROOT, dl.DECISIONS_MD, dl.TOPIC_DIR, dl.ARCHIVE_DIR, dl.RECORDS_DIR) dl.REPO_ROOT = tmp dl.DECISIONS_MD = tmp / "docs" / "decisions.md" dl.TOPIC_DIR = tmp / "docs" / "decisions" dl.ARCHIVE_DIR = dl.TOPIC_DIR / "archive" dl.RECORDS_DIR = dl.TOPIC_DIR / "records" try: before = {} for f in [dl.DECISIONS_MD, *sorted(dl.TOPIC_DIR.glob("*.md")), *sorted(dl.ARCHIVE_DIR.rglob("*.md"))]: for r in dl.parse_file(f): if r.key and r.heading not in dl.SKIP_HEADINGS: before[r.key] = (r, _independent_strip(r)) mig.main([]) after = {} for f in [*dl.RECORDS_DIR.rglob("*.md"), *dl.ARCHIVE_DIR.rglob("*.md")]: for r in dl.parse_file(f): if r.key: after[r.key] = (r, r.body) yield before, after, tmp finally: dl.REPO_ROOT, dl.DECISIONS_MD, dl.TOPIC_DIR, dl.ARCHIVE_DIR, dl.RECORDS_DIR = orig def test_fixture_is_actually_pre_migration(migrated): """Guard against the harness silently testing nothing if the fixture ever gets migrated.""" before, _, _ = migrated assert len(before) == EXPECTED_RECORDS, f"fixture parsed {len(before)}, expected {EXPECTED_RECORDS}" def test_every_record_survives_with_the_same_key(migrated): before, after, _ = migrated assert set(before) == set(after), ( f"lost: {sorted(set(before) - set(after))} / gained: {sorted(set(after) - set(before))}" ) def test_every_metadata_field_round_trips(migrated): before, after, _ = migrated diffs = [] for key, (b, _) in before.items(): a, _ = after[key] for f in COMPARED_FIELDS: if getattr(b, f) != getattr(a, f): diffs.append(f"{key}.{f}: {getattr(b, f)!r} -> {getattr(a, f)!r}") assert not diffs, "\n".join(diffs) def test_title_round_trips_from_the_heading(migrated): before, after, _ = migrated diffs = [ f"{k}: {b.heading!r} -> {after[k][0].heading!r}" for k, (b, _) in before.items() if b.heading != after[k][0].heading ] assert not diffs, "\n".join(diffs) def test_rationale_bodies_are_byte_identical(migrated): """Including interior blank lines and backticks — the bytes are the payload.""" before, after, _ = migrated diffs = [k for k, (_, body) in before.items() if body != after[k][1]] assert not diffs, f"{len(diffs)} rationale bodies changed: {diffs}" def test_path_matches_key(migrated): _, after, _tmp = migrated bad = [] for key, (rec, _) in after.items(): area, _, topic = key.partition(".") if rec.source.parent.name != area or rec.source.stem != topic: bad.append(f"{key} -> {rec.source}") assert not bad, "\n".join(bad) def test_archived_records_land_in_the_archive_wing(migrated): _, after, _tmp = migrated misfiled = [ key for key, (rec, _) in after.items() if (rec.status in ("superseded", "retired")) != ("archive" in rec.source.parts) ] assert not misfiled, misfiled def test_source_files_are_kept_and_retain_their_narrative(migrated): """Preamble prose is often the only copy — it must survive, not be deleted with the records.""" _, _, tmp = migrated kept = tmp / "docs" / "decisions.md" assert kept.exists(), "decisions.md was deleted" assert "the only copy of it" in kept.read_text() topic = tmp / "docs" / "decisions" / "topic.md" assert topic.exists() and "the only copy of this narrative" in topic.read_text() def test_no_records_remain_in_the_legacy_files(migrated): _, _, tmp = migrated leftovers = {} for f in [tmp / "docs" / "decisions.md", *sorted((tmp / "docs" / "decisions").glob("*.md"))]: if f.name in dl._NON_DECISION_FILES: continue keyed = [r.key for r in dl.parse_file(f) if r.key] if keyed: leftovers[f.name] = keyed assert not leftovers, leftovers def test_same_file_anchor_bullets_are_dropped(migrated): """The split makes them WRONG, not merely stale — they point at headings that left the file.""" _, _, tmp = migrated assert "](#" not in (tmp / "docs" / "decisions.md").read_text(), "a same-file anchor now dangles" def test_every_generated_link_resolves_from_its_own_file(migrated): """Resolve each link relative to the file it LIVES IN — the base the reader's browser uses. Asserting existence from `docs/decisions/` regardless of where the link sits is the wrong base for `docs/decisions.md` (parent `docs/`) and for the archive files (parent `docs/decisions/archive/`). It encodes the very bug this is meant to catch: 105 links dangled while it stayed green. """ _, _, tmp = migrated import re as _re dangling = [] checked = 0 for f in tmp.rglob("*.md"): if mig._MOVED_HEADING not in f.read_text(): continue section = f.read_text().split(mig._MOVED_HEADING, 1)[1] for link in _re.findall(r"\]\(([^)#]+)\)", section): checked += 1 if not (f.parent / link).resolve().exists(): dangling.append(f"{f.relative_to(tmp)} -> {link}") assert checked, "no generated links found — test would pass vacuously" assert not dangling, f"{len(dangling)} dangling of {checked}:\n" + "\n".join(dangling[:10])