Files
ersatztv/scripts/tests/test_build_catalog.py
T
timothy 75139bbf31 fix(603): close four defects found in adversarial review of the stale-after fields
1. `date.fromisoformat` is not a YYYY-MM-DD validator. On Python >= 3.11 it also
   accepts ISO basic format ("20270101") and week dates ("2027-W01-1"), so a
   malformed-looking value passed the blocking check — and which forms parse
   depends on the interpreter, meaning the same corpus could validate differently
   on a dev machine and on the runner (pr-checks.yml pins only python-version
   '3.x'). Knock-on: the catalog's Review-due section sorts on the raw STRING, so
   an accepted "20270101" sorted AFTER "2027-01-15" ('-' < '0'), contradicting the
   section's own "sorted soonest-first" text. Gate on ^\d{4}-\d{2}-\d{2}$ first,
   which fixes both — a fixed-width zero-padded form makes string sort == date sort.

2. A present-but-empty `stale-after:` was collapsed to None by `or None` in the
   parser and then skipped by a truthiness guard in the validator, so it passed as
   "absent" — a field that silently never fires, which is the exact failure mode
   the blocking check exists to prevent. Keep "" distinct from None and test with
   `is not None`.

3. `test_catalog_is_date_independent` was partly vacuous: with no date in either
   render, both sides were trivially equal after the .replace(). It did still catch
   an injected clock-derived marker, but it passed with the feature deleted. Assert
   the dates are present.

4. The malformed-date check ran only over the active set, exempting archive
   records. Staleness is moot there, but a typo is still a typo — check both wings.

Adds regression tests for each, plus a Review-due row for a topic-file record
(pinning the `../decisions.md` vs bare-filename link forms).
2026-07-25 15:22:50 +02:00

109 lines
3.8 KiB
Python

from pathlib import Path
from typing import Any
import scripts.build_decisions_catalog as bc
import scripts.decisions_lib as dl
def test_catalog_lists_only_active_sorted_by_key():
recs = [
dl.Record(
heading="H2",
source=Path("x"),
lineno=1,
key="z.a",
status="active",
rule="Zeta rule",
),
dl.Record(
heading="H1",
source=Path("x"),
lineno=1,
key="a.b",
status="active",
rule="Alpha rule",
),
dl.Record(
heading="Old",
source=Path("x"),
lineno=1,
key="a.b",
status="superseded",
rule="old",
),
]
out = bc.render_catalog(recs)
assert "a.b" in out and "z.a" in out
assert out.index("a.b") < out.index("z.a") # sorted
assert "Alpha rule" in out and "Zeta rule" in out
assert "old" not in out # superseded excluded
assert "GENERATED" in out # do-not-edit banner
def test_anchor_matches_gitea_double_hyphen_slug():
# Ground truth: Gitea does NOT collapse hyphen runs. " — " (space, em dash, space) becomes
# "--" in the anchor (one hyphen per space/dash char), never collapsed to a single "-".
a = bc._anchor(
"2026-07-19 — CI `test` job reports a sampled true peak-anon, not cache-inflated `memory.peak` (#412)"
)
assert a == ("2026-07-19--ci-test-job-reports-a-sampled-true-peak-anon-not-cache-inflated-memorypeak-412")
b = bc._anchor("2026-07-16 — Optional advertised IPTV base URL (`iptv.base_url`)…")
assert "iptvbase_url" in b
def _r(key: str, **kw: Any) -> dl.Record:
base: dict[str, Any] = dict(
heading=f"H {key}",
source=Path("decisions.md"),
lineno=1,
key=key,
status="active",
rule="r",
)
base.update(kw)
return dl.Record(**base)
def test_review_due_section_lists_dated_records_soonest_first():
out = bc.render_catalog(
[
_r("b.later", stale_after="2027-03-01"),
_r("a.sooner", stale_after="2026-09-01"),
_r("c.undated"),
]
)
assert "## Review due" in out
assert out.index("2026-09-01") < out.index("2027-03-01")
assert "c.undated" not in out.split("## Review due")[1]
def test_review_due_section_omitted_when_no_record_is_dated():
assert "## Review due" not in bc.render_catalog([_r("a.b")])
def test_catalog_is_date_independent():
"""The generated file must never bake in 'today' — CI checks it with --check, so a
time-dependent render would drift red on a calendar boundary with no commit touching it."""
past = bc.render_catalog([_r("a.b", stale_after="2020-01-01")])
future = bc.render_catalog([_r("a.b", stale_after="2099-01-01")])
# Guard against vacuity: without this the assertion below passes trivially on a renderer that
# emits no dates at all (i.e. with the whole feature deleted).
assert "2020-01-01" in past and "2099-01-01" in future
# A long-past and a far-future date must produce byte-identical output apart from the date
# itself: no stale/fresh verdict, no marker, nothing derived from the clock.
assert past.replace("2020-01-01", "D") == future.replace("2099-01-01", "D")
def test_review_due_row_links_correctly_from_a_topic_file():
"""Review-due rows reuse _rel(), so a topic-file record must not get decisions.md's `../` form."""
out = bc.render_catalog(
[
_r("t.opic", source=Path("docs/decisions/workflow-process.md"), stale_after="2027-01-01"),
_r("m.ain", source=Path("docs/decisions.md"), stale_after="2027-01-02"),
]
)
due = out.split("## Review due")[1]
assert "(workflow-process.md#" in due
assert "(../decisions.md#" in due