PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 17s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m15s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 16m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m6s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
This is why `decisions lifecycle` went red, and it was NOT the known flake. I came close to dismissing it as one for the second time this session, because an earlier red on another branch genuinely was. The dual-format parser imported PyYAML to read frontmatter. `decisions-guard` does `setup-python` and installs NOTHING, so once the corpus was migrated every record became unparseable there: ModuleNotFoundError, job fails. The same would hit the Husky pre-commit hook and every contributor's machine. Installing PyYAML in CI is the wrong fix: READING happens everywhere -- CI, the hook, every dev -- while WRITING happens once, in a migration a human runs deliberately. So the read path is now dependency-free and only `migrate_decisions_split` (the writer) still imports yaml. A hand-rolled parser is only safe if it provably matches the library that WROTE the files, so `test_frontmatter_reader_matches_pyyaml_on_every_real_record` compares the two field-by-field across all 169 real records (importorskip, so it is skipped rather than failing where PyYAML is absent) with a >100-file guard against near-vacuity. It is narrow by construction: the frontmatter is machine-generated with default_flow_style=False and width=10**9, so every value is a single-line scalar, and the reader bails to None on anything nested. Verified by running all four affected entry points against a shim that makes `import yaml` raise: validate --base/--head, catalog --check, the kickoff guard, and the plain validate the pre-commit hook calls. All exit 0. refs #610
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import scripts.decisions_lib as dl
|
|
|
|
FIX = Path(__file__).parent / "fixtures" / "sample_decisions.md"
|
|
|
|
|
|
def test_parses_migrated_record():
|
|
recs = dl.parse_file(FIX)
|
|
migrated = [r for r in recs if r.key == "ci.runner-placement"]
|
|
assert len(migrated) == 1
|
|
r = migrated[0]
|
|
assert r.status == "active"
|
|
assert r.since == "2026-07-17"
|
|
assert r.supersedes == "none"
|
|
assert r.superseded_by == "none"
|
|
assert r.rule == "Every CI services container gets an explicit cap."
|
|
assert r.signals is not None
|
|
assert "issues: #390 #406" in r.signals
|
|
assert r.mechanics is not None
|
|
assert r.mechanics.startswith("docs/ci-cd.md")
|
|
|
|
|
|
def test_legacy_record_is_unmigrated():
|
|
recs = dl.parse_file(FIX)
|
|
legacy = [r for r in recs if r.heading.endswith("(#231)")]
|
|
assert len(legacy) == 1
|
|
assert legacy[0].key is None
|
|
assert legacy[0].status == "legacy-unmigrated"
|
|
|
|
|
|
def test_index_section_parses_as_heading():
|
|
recs = dl.parse_file(FIX)
|
|
assert any(r.heading == "Index" for r in recs)
|
|
|
|
|
|
def test_parses_optional_stale_after_and_sources():
|
|
recs = dl.parse_file(FIX)
|
|
r = next(r for r in recs if r.key == "ci.peak-anon-measurement")
|
|
assert r.stale_after == "2027-01-15"
|
|
assert r.sources is not None
|
|
assert "gitea run 4471" in r.sources
|
|
|
|
|
|
def test_optional_fields_default_to_none_when_absent():
|
|
recs = dl.parse_file(FIX)
|
|
r = next(r for r in recs if r.key == "ci.runner-placement")
|
|
assert r.stale_after is None
|
|
assert r.sources is None
|
|
|
|
|
|
def test_empty_stale_after_parses_as_empty_string_not_none():
|
|
"""Absent vs present-but-empty must stay distinguishable for the validator."""
|
|
text = (
|
|
"## H\n"
|
|
"`key: a.b` · `status: active` · `since: 2026-01-01` · `stale-after:` "
|
|
"· `supersedes: none` · `superseded-by: none`\n"
|
|
"**Rule:** r\n"
|
|
)
|
|
r = dl.parse_text(text, Path("fake.md"))[0]
|
|
assert r.stale_after == ""
|
|
|
|
|
|
def test_frontmatter_reader_matches_pyyaml_on_every_real_record():
|
|
"""The dependency-free reader must agree with PyYAML on the whole real corpus.
|
|
|
|
The read path cannot import PyYAML — it runs in CI's `decisions lifecycle` job, the Husky
|
|
pre-commit hook, and on every contributor's machine, none of which install it. (Requiring it
|
|
made the validator crash with ModuleNotFoundError once the corpus was migrated.) A hand parser
|
|
is only safe if it provably matches the library that WROTE the files, so this compares the two
|
|
across every record rather than on a sample.
|
|
"""
|
|
yaml = pytest.importorskip("yaml")
|
|
|
|
files = [p for p in dl.RECORDS_DIR.rglob("*.md")] + [p for p in dl.ARCHIVE_DIR.rglob("*.md")]
|
|
files = [f for f in files if dl.has_frontmatter(f.read_text(encoding="utf-8"))]
|
|
assert len(files) > 100, f"only {len(files)} frontmatter files found — test would be near-vacuous"
|
|
|
|
diffs = []
|
|
for f in files:
|
|
lines = f.read_text(encoding="utf-8").splitlines()
|
|
end = next(i for i, ln in enumerate(lines[1:], start=1) if ln.rstrip() == "---")
|
|
block = "\n".join(lines[1:end])
|
|
mine = dl._read_frontmatter(block)
|
|
theirs = yaml.safe_load(block) or {}
|
|
theirs = {k: ("" if v is None else str(v)) for k, v in theirs.items()}
|
|
if mine != theirs:
|
|
for k in set(mine or {}) | set(theirs):
|
|
if (mine or {}).get(k) != theirs.get(k):
|
|
diffs.append(f"{f.name}:{k}\n mine ={(mine or {}).get(k)!r}\n pyyaml={theirs.get(k)!r}")
|
|
assert not diffs, f"{len(diffs)} field(s) differ from PyYAML:\n" + "\n".join(diffs[:5])
|