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).
109 lines
3.8 KiB
Python
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
|