test(changelog): the cite rule's own cases, both callers asserted

Refs #262
This commit is contained in:
cndgrr 2026-08-04 12:11:09 +00:00
parent ef818001d0
commit f4cb970097
3 changed files with 169 additions and 0 deletions

View file

@ -298,6 +298,38 @@ check "fragment mode over-bound refusal names the bound and the split fix" 1 \
"the bound is 300: split it into multiple '- ' entries in this same fragment" \
in_tree fragments-dev-over-bound
# The terminal cite (#262) reds the PR that writes the fragment, through the
# same shared predicate — which is the whole point of the rule living there
# rather than in prose a reviewer has to remember.
fragment_tree fragments-dev-uncited 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
printf '%s\n' "- An entry that never learned to cite its issue." \
>"$TMP/fragments-dev-uncited/changelog.d/115.md"
check "fragment mode refuses an uncited entry, fragment named" 1 \
"115.md' has an entry with no issue citation" \
in_tree fragments-dev-uncited
check "fragment mode uncited refusal names the shape to write" 1 \
"end it with the issue it comes from: '(#N).'" \
in_tree fragments-dev-uncited
fragment_tree fragments-dev-misplaced-cite 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
printf '%s\n' "- The citation trails the period. (#115)" \
>"$TMP/fragments-dev-misplaced-cite/changelog.d/115.md"
check "fragment mode refuses a non-terminal citation, fragment named" 1 \
"115.md' has an entry whose issue citation is not terminal" \
in_tree fragments-dev-misplaced-cite
fragment_tree fragments-dev-grouped 1.2.4-dev <<'EOF'
# Changelog

View file

@ -204,6 +204,38 @@ check "preamble-only write is exact" 0 "" \
assert_file "$TMP/preamble-only/CHANGELOG.md" \
$'# Changelog\n\nOnly preamble so far.\n\n## 0.1.0 — 2026-07-24\n\n- The first entry ever (#1).'
# --- the fragment predicate at release time (#262) ---------------------------
# The cite rule joins changelog_fragment_problem, so it binds both callers:
# the arming guard at PR time and this assembler at release time. Asserted
# rather than assumed — a release that publishes an uncited entry is the
# failure the PR-time guard exists to have already caught.
tree uncited-release <<EOF
$BASE_CHANGELOG
EOF
frag uncited-release 60.md <<'EOF'
- An entry that never learned to cite its issue.
EOF
check "release time: an uncited fragment refuses the release, fragment named" 1 \
"changelog.d/60.md' has an entry with no issue citation" \
in_tree uncited-release 0.2.0 2026-07-24
check "release time: the uncited refusal survives --check too" 1 \
"has an entry with no issue citation" \
in_tree uncited-release 0.2.0 2026-07-24 --check
check "release time: the refused release wrote nothing" 0 "" \
test -e "$TMP/uncited-release/changelog.d/60.md"
tree misplaced-release <<EOF
$BASE_CHANGELOG
EOF
frag misplaced-release 61.md <<'EOF'
- The citation trails the period. (#61)
EOF
check "release time: a non-terminal citation refuses the release" 1 \
"changelog.d/61.md' has an entry whose issue citation is not terminal" \
in_tree misplaced-release 0.2.0 2026-07-24
# --- --check is provably read-only -------------------------------------------
tree check-readonly <<EOF

View file

@ -331,6 +331,111 @@ check "length bound: published sections stay unvalidated — 0.3.0's over-bound
check "length bound: published sections stay unvalidated — 0.2.0 reds nothing either" 0 "" \
changelog_section_problem "$ROOT/CHANGELOG.md" 0.2.0
# --- the terminal issue cite (#262) ------------------------------------------
# cite_case <number> <entry-line...> — a fragment holding exactly the given
# lines, so a case reads as the entry it is about.
cite_case() {
local num="$1"
shift
printf '%s\n' "$@" >"$PF/$num.md"
}
cite_case 40 '- Local (#262).'
check "cite: the canonical '(#N).' passes" 0 "" \
changelog_fragment_problem "$PF/40.md"
cite_case 41 '- Sibling repo (crew#309).'
check "cite: a sibling-repo reference passes" 0 "" \
changelog_fragment_problem "$PF/41.md"
cite_case 42 '- Fully qualified (heavy-duty/crew#309).'
check "cite: an owner/repo reference passes" 0 "" \
changelog_fragment_problem "$PF/42.md"
cite_case 43 '- Two issues, one entry (#236, #250).'
check "cite: one group carrying two references passes" 0 "" \
changelog_fragment_problem "$PF/43.md"
# The cite is measured on the normalized entry, so a citation that lands on
# a continuation line still closes the entry — the #167 lesson, repeated:
# wrapping alone must never red a compliant entry.
cite_case 44 '- An entry whose prose wraps onto a' ' continuation line, cite and all (#262).'
check "cite: a citation on a continuation line passes — the entry is normalized first" 0 "" \
changelog_fragment_problem "$PF/44.md"
cite_case 45 '### Added' '' '- Added one (#101).' '- Added two (#102).' '' \
'### Changed' '' '- Changed one (#103).' '' '### Fixed' '' '- Fixed one (#104).'
check "cite: a grouped fragment, three headings, every entry compliant, passes" 0 "" \
changelog_fragment_problem "$PF/45.md"
# The two diagnoses are distinct by construction (D5): a builder who reads
# one must not be told the other's fix.
cite_case 50 '- No cite here.'
check "cite: an entry with no reference at all is refused" 1 \
"50.md' has an entry with no issue citation" \
changelog_fragment_problem "$PF/50.md"
check "cite: the uncited refusal names the shape to write" 1 \
"end it with the issue it comes from: '(#N).'" \
changelog_fragment_problem "$PF/50.md"
cite_case 51 '- Cite before the period. (#262)'
check "cite: a citation trailing the period is refused — the 248.md shape" 1 \
"51.md' has an entry whose issue citation is not terminal" \
changelog_fragment_problem "$PF/51.md"
check "cite: the misplaced refusal names the shape to write" 1 \
"exactly one '(#N)' group ends the entry, the final '.' after it" \
changelog_fragment_problem "$PF/51.md"
cite_case 52 '- Trailing prose (#262) and then more.'
check "cite: a citation with prose after it is refused" 1 \
"has an entry whose issue citation is not terminal" \
changelog_fragment_problem "$PF/52.md"
cite_case 53 '- Two groups (#236) and (#250).'
check "cite: two citation groups are refused — one terminal group, or none (D2)" 1 \
"has an entry whose issue citation is not terminal" \
changelog_fragment_problem "$PF/53.md"
cite_case 54 '- Bad token (#abc).'
check "cite: a reference with no digits is no reference" 1 \
"has an entry with no issue citation" \
changelog_fragment_problem "$PF/54.md"
cite_case 55 '- Bad token (#).'
check "cite: an empty reference is no reference" 1 \
"has an entry with no issue citation" \
changelog_fragment_problem "$PF/55.md"
# The citation need not name the file's own issue (D3): the filename already
# carries the authorizing one, so an entry may cite the incident beside it.
cite_case 56 '- Cites another issue entirely (#101).'
check "cite: the reference need not match the filename" 0 "" \
changelog_fragment_problem "$PF/56.md"
# Ordering: the bound outranks the cite, so a fragment that reds today draws
# the diagnosis it drew before this rule existed.
{
printf -- '- %s\n' "$(mkchars 301)"
printf -- '- Uncited too.\n'
} >"$PF/57.md"
check "cite: an over-bound entry still reports the bound, not the cite" 1 \
"57.md' has a 301-character entry" \
changelog_fragment_problem "$PF/57.md"
# Published sections keep their pre-rule prose (D4): reddening history is a
# wall, not a guard. Every shipped section predates the cite.
check "cite: a published section with uncited entries still reds nothing" 0 "" \
changelog_section_problem "$ROOT/CHANGELOG.md" 0.3.0
# The fragments this repo carries right now are the rule's own first
# constituency — the guard is worth nothing if the tree it ships in fails it.
assert_tree_fragments() {
local f
while IFS= read -r f; do
[ -n "$f" ] || continue
changelog_fragment_problem "$f" || return 1
done <<<"$(changelog_fragments "$ROOT/changelog.d")"
}
check "cite: every fragment in this tree passes the rule it ships" 0 "" \
assert_tree_fragments
# --- the assembler (#114) ----------------------------------------------------
assert_assemble() {