ceremony/test/changelog-armed.test.sh
cndgrr ef818001d0 test(changelog): every fragment fixture carries a terminal cite
Scoped to the fixtures the new rule actually binds: a fragment whose
predicate complaint is already its name, a smuggled heading, a dangling
heading or the 300-character bound is left alone, so the diagnosis it
tests is still the one it draws. The section-predicate fixtures are
untouched (D4).

The computed-length fixtures keep their measured lengths: the cite is
seven characters, so an entry that must measure exactly 300 builds 293
of the run and lets the cite carry the rest. 33.md's cite lands on the
last continuation line, which is the wrapped-citation case.

Refs #262
2026-08-04 12:08:54 +00:00

551 lines
15 KiB
Bash

#!/usr/bin/env bash
# Contract tests for actions/changelog-armed (issue #5). Constructed fixture
# trees — a dir with a changelog plus a VERSION file or package.json, not
# git repos — the same discipline as the box suite this guard is ported
# from. set -u, not -e: failing commands are behavior for the harness to
# inspect.
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=test/harness.sh
. "$ROOT/test/harness.sh"
SCRIPT="$ROOT/actions/changelog-armed/changelog-armed.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
# The guard reads the consumer's tree at its working directory, so every
# case runs from inside a constructed fixture tree.
in_tree() {
local dir="$1"
shift
(cd "$TMP/$dir" && bash "$SCRIPT" "$@")
}
# tree <name> <version> — a fixture tree with a VERSION file; the changelog
# body arrives on stdin.
tree() {
mkdir -p "$TMP/$1"
printf '%s\n' "$2" >"$TMP/$1/VERSION"
cat >"$TMP/$1/CHANGELOG.md"
}
# pkg_tree <name> <version> — the same, package-json backend.
pkg_tree() {
mkdir -p "$TMP/$1"
printf '{ "name": "fixture", "version": "%s" }\n' "$2" >"$TMP/$1/package.json"
cat >"$TMP/$1/CHANGELOG.md"
}
# --- the -dev rows: top section MUST be '## Unreleased' ----------------------
tree dev-armed 1.2.4-dev <<'EOF'
# Changelog
## Unreleased
- Pending entry.
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
check "-dev + Unreleased on top passes" 0 "agrees" in_tree dev-armed
tree dev-seeded 1.2.4-dev <<'EOF'
# Changelog
## Unreleased
### Added
### Changed
### Fixed
## 1.2.3 — 2026-07-20
### Fixed
- The shipped entry.
EOF
check "-dev + seeded empty Unreleased headings passes" 0 "agrees" in_tree dev-seeded
tree dev-stamped 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
check "-dev + stamped top fails" 1 "development tree" in_tree dev-stamped
check "-dev failure names the file" 1 "CHANGELOG.md" in_tree dev-stamped
check "-dev failure teaches the re-arm fix" 1 "re-arm" in_tree dev-stamped
# --- the bare rows: both ceremony shapes legal, half-ceremonies refused -----
tree bare-armed 1.2.3 <<'EOF'
# Changelog
## Unreleased
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
check "bare + re-armed tree passes" 0 "agrees" in_tree bare-armed
tree bare-stamped 1.2.3 <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
## 1.2.2 — 2026-07-01
- Older entry.
EOF
check "bare + own stamped section on top passes" 0 "agrees" in_tree bare-stamped
tree bare-dangling-heading 1.2.3 <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
### Added
EOF
check "bare + dangling heading fails with the heading diagnosis" 1 \
"section '1.2.3' has no entries — a heading is not an entry" \
in_tree bare-dangling-heading
check "bare + dangling heading keeps the half-ceremony remedy" 1 \
"HALF-DONE ceremony" in_tree bare-dangling-heading
tree bare-partly-dangling 1.2.3 <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
### Added
### Fixed
- Fixed entry.
EOF
check "bare + one empty grouped heading names the first empty heading" 1 \
"section '1.2.3' has an empty heading: '### Added'" \
in_tree bare-partly-dangling
tree bare-empty-stamp 1.2.3 <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
## 1.2.2 — 2026-07-01
- Older entry.
EOF
check "bare + own stamped section but EMPTY fails" 1 "no non-empty" \
in_tree bare-empty-stamp
tree bare-wrong-stamp 1.2.3 <<'EOF'
# Changelog
## 9.9.9 — 2026-07-20
- An entry under the wrong number.
EOF
check "bare + top section naming another version fails" 1 "stamped the wrong number" \
in_tree bare-wrong-stamp
tree bare-half-ceremony 1.2.3 <<'EOF'
# Changelog
## Unreleased
- Pending entry that was never stamped.
## 1.2.2 — 2026-07-01
- Older entry.
EOF
check "bare + no section for the version anywhere fails" 1 "HALF-DONE ceremony" \
in_tree bare-half-ceremony
# Whole-version matching: 1.2.3 must not be satisfied by a 1.2.3-rc1 section.
tree bare-rc-only 1.2.3 <<'EOF'
# Changelog
## Unreleased
## 1.2.3-rc1 — 2026-07-15
- The candidate's entry.
EOF
check "bare: an rc section never satisfies the bare version" 1 "HALF-DONE ceremony" \
in_tree bare-rc-only
# An rc is a pre-release, not a dev tree (#3's version_is_dev): it keys on
# the bare rules, so a stamped rc section of its own is shippable.
tree rc-stamped 2.0.0-rc1 <<'EOF'
# Changelog
## Unreleased
## 2.0.0-rc1 — 2026-07-20
- The candidate's entry.
EOF
check "rc keys as bare, own stamped section passes" 0 "agrees" in_tree rc-stamped
# --- degenerate trees --------------------------------------------------------
tree no-sections 1.2.3-dev <<'EOF'
# Changelog
Only preamble prose, no sections.
EOF
check "changelog with no '## ' at all fails" 1 "nothing for a PR entry to land under" \
in_tree no-sections
mkdir -p "$TMP/no-changelog"
printf '1.2.3\n' >"$TMP/no-changelog/VERSION"
check "missing changelog fails" 1 "no such file" in_tree no-changelog
mkdir -p "$TMP/no-version"
printf '# Changelog\n\n## Unreleased\n' >"$TMP/no-version/CHANGELOG.md"
check "missing version source fails" 1 "cannot read the version" in_tree no-version
check "unknown version-source refused" 1 "unknown backend" \
in_tree dev-armed CHANGELOG.md carrier-pigeon
# --- the package-json backend ------------------------------------------------
pkg_tree pkg-dev-armed 0.2.0-dev <<'EOF'
# Changelog
## Unreleased
- Pending entry.
## 0.1.0 — 2026-07-20
- The shipped entry.
EOF
check "package-json: -dev + armed passes" 0 "agrees" \
in_tree pkg-dev-armed CHANGELOG.md package-json
pkg_tree pkg-bare-armed 0.1.0 <<'EOF'
# Changelog
## Unreleased
## 0.1.0 — 2026-07-20
- The shipped entry.
EOF
check "package-json: bare + armed passes" 0 "agrees" \
in_tree pkg-bare-armed CHANGELOG.md package-json
# --- fragment mode: changelog.d/ is the arming -------------------------------
fragment_tree() {
local name="$1" version="$2"
shift 2
tree "$name" "$version"
mkdir -p "$TMP/$name/changelog.d"
printf '%s\n' "# Changelog fragments" >"$TMP/$name/changelog.d/README.md"
}
fragment_tree fragments-dev-empty 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
check "fragment -dev + marker + no fragments passes" 0 "fragment mode" \
in_tree fragments-dev-empty
fragment_tree fragments-dev-flat 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
printf '%s\n' "- Added fragment mode (#115)." >"$TMP/fragments-dev-flat/changelog.d/115.md"
check "fragment -dev + well-formed flat fragment passes" 0 "fragment mode" \
in_tree fragments-dev-flat
# The entry length bound (#167) reds the PR that writes the fragment, with
# the shared changelog_fragment_problem diagnosis.
fragment_tree fragments-dev-over-bound 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
printf -- '- %s\n' \
"$(awk 'BEGIN { s = ""; while (length(s) < 301) s = s "a"; print s }')" \
>"$TMP/fragments-dev-over-bound/changelog.d/115.md"
check "fragment mode refuses an over-bound entry, fragment and length named" 1 \
"115.md' has a 301-character entry" \
in_tree fragments-dev-over-bound
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
fragment_tree fragments-dev-grouped 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
### Fixed
- The shipped entry.
EOF
cat >"$TMP/fragments-dev-grouped/changelog.d/115.md" <<'EOF'
### Changed
- Added fragment mode (#115).
EOF
check "fragment -dev + well-formed grouped fragment passes" 0 "fragment mode" \
in_tree fragments-dev-grouped
fragment_tree fragments-dev-mixed 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
printf '%s\n' "- Flat fragment (#115)." >"$TMP/fragments-dev-mixed/changelog.d/114.md"
cat >"$TMP/fragments-dev-mixed/changelog.d/115.md" <<'EOF'
### Fixed
- Grouped fragment (#115).
EOF
check "fragment mode refuses mixed shapes with the shared assembler diagnosis" 1 \
"fragment 'changelog.d/115.md' is grouped but fragment 'changelog.d/114.md' is not" \
in_tree fragments-dev-mixed
fragment_tree fragments-dev-all-grouped-over-flat 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
cat >"$TMP/fragments-dev-all-grouped-over-flat/changelog.d/115.md" <<'EOF'
### Fixed
- Grouped fragment (#115).
EOF
check "fragment mode refuses an all-grouped set over a flat published section" 1 \
"changelog.d/115.md' is grouped but newest published section '1.2.3'" \
in_tree fragments-dev-all-grouped-over-flat
fragment_tree fragments-dev-flat-over-grouped 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
### Fixed
- The shipped entry.
EOF
printf '%s\n' "- Flat fragment (#115)." >"$TMP/fragments-dev-flat-over-grouped/changelog.d/115.md"
check "fragment mode refuses a flat set over a grouped published section" 1 \
"changelog.d/115.md' is flat but newest published section '1.2.3'" \
in_tree fragments-dev-flat-over-grouped
# The declared anchor (#182): the flip tree — a grouped set under a
# 'grouped' sentinel over a flat published section — is green, where the
# same tree minus the sentinel is the all-grouped-over-flat red row above.
fragment_tree fragments-dev-flip 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
printf '%s\n' "grouped" >"$TMP/fragments-dev-flip/changelog.d/shape"
cat >"$TMP/fragments-dev-flip/changelog.d/115.md" <<'EOF'
### Fixed
- Grouped fragment (#115).
EOF
check "fragment mode: 'grouped' sentinel admits the flip tree over a flat published section" 0 \
"fragment mode" in_tree fragments-dev-flip
# Post-flip drift is refused on its own PR: a flat probe fragment atop the
# flip tree goes red — beside grouped fragments the mix rule names it first.
printf '%s\n' "- Flat probe (#116)." >"$TMP/fragments-dev-flip/changelog.d/116.md"
check "fragment mode: a flat probe atop the flip tree is refused" 1 \
"changelog.d/115.md' is grouped but fragment 'changelog.d/116.md' is not" \
in_tree fragments-dev-flip
rm "$TMP/fragments-dev-flip/changelog.d/116.md"
# And once the grouped fragments are consumed, the sentinel alone still
# holds the shape: an all-flat set under 'grouped' is refused, sentinel
# named — the published-section inference never gets a say.
rm "$TMP/fragments-dev-flip/changelog.d/115.md"
printf '%s\n' "- Flat probe (#116)." >"$TMP/fragments-dev-flip/changelog.d/116.md"
check "fragment mode: a flat set under the 'grouped' sentinel refused, sentinel named" 1 \
"changelog.d/116.md' is flat but 'changelog.d/shape' declares grouped" \
in_tree fragments-dev-flip
rm "$TMP/fragments-dev-flip/changelog.d/116.md"
printf '%s\n' "Grouped" >"$TMP/fragments-dev-flip/changelog.d/shape"
check "fragment mode: a malformed sentinel is refused, file named" 1 \
"'changelog.d/shape' declares neither shape" \
in_tree fragments-dev-flip
printf 'grouped\n\n' >"$TMP/fragments-dev-flip/changelog.d/shape"
check "fragment mode: a sentinel with a trailing blank line is refused, file named" 1 \
"'changelog.d/shape' declares neither shape" \
in_tree fragments-dev-flip
fragment_tree fragments-dev-no-published 1.2.4-dev <<'EOF'
# Changelog
Preamble only.
EOF
cat >"$TMP/fragments-dev-no-published/changelog.d/115.md" <<'EOF'
### Fixed
- Grouped fragment (#115).
EOF
check "fragment mode accepts a consistent set with no published section" 0 \
"fragment mode" in_tree fragments-dev-no-published
fragment_tree fragments-unreleased 1.2.4-dev <<'EOF'
# Changelog
## Unreleased
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
check "fragment mode refuses even an empty Unreleased section" 1 \
"Unreleased' section survived the adoption" in_tree fragments-unreleased
fragment_tree fragments-no-marker 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
rm "$TMP/fragments-no-marker/changelog.d/README.md"
check "fragment mode requires the generated marker" 1 "README.md" \
in_tree fragments-no-marker
fragment_tree fragments-bad-name 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
printf '%s\n' "- An entry." >"$TMP/fragments-bad-name/changelog.d/notes.md"
check "fragment mode quotes malformed-fragment diagnosis and file" 1 \
"fragment 'changelog.d/notes.md' is not named for its issue" \
in_tree fragments-bad-name
fragment_tree fragments-dangling-group 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
printf '%s\n' "### Changed" >"$TMP/fragments-dangling-group/changelog.d/115.md"
check "fragment mode refuses a dangling fragment heading" 1 \
"fragment 'changelog.d/115.md' has no entries" \
in_tree fragments-dangling-group
fragment_tree fragments-bare-stamped 1.2.3 <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
## 1.2.2 — 2026-07-01
- The older entry.
EOF
check "fragment bare + stamped section + consumed directory passes" 0 \
"fragment mode" in_tree fragments-bare-stamped
cp -R "$TMP/fragments-bare-stamped" "$TMP/fragments-bare-survivor"
printf '%s\n' "- This entry was not consumed (#115)." \
>"$TMP/fragments-bare-survivor/changelog.d/115.md"
check "fragment bare refuses and lists surviving fragments" 1 \
"these fragments were not consumed: changelog.d/115.md" \
in_tree fragments-bare-survivor
fragment_tree fragments-bare-wrong 1.2.3 <<'EOF'
# Changelog
## 9.9.9 — 2026-07-20
- The wrong release.
## 1.2.3 — 2026-07-19
- The right release was not stamped on top.
EOF
check "fragment bare refuses a stamp for another version" 1 \
"stamped the wrong number" in_tree fragments-bare-wrong
fragment_tree fragments-bare-missing 1.2.3 <<'EOF'
# Changelog
## 1.2.2 — 2026-07-01
- The older entry.
EOF
check "fragment bare refuses a missing stamp via section diagnosis" 1 \
"no section for '1.2.3'" in_tree fragments-bare-missing
fragment_tree fragments-cross-mode 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
check "same changelog passes in fragment mode" 0 "fragment mode" \
in_tree fragments-cross-mode
rm -rf "$TMP/fragments-cross-mode/changelog.d"
check "same changelog fails in legacy mode" 1 "development tree" \
in_tree fragments-cross-mode
# --- the action's wiring: inputs arrive as env vars --------------------------
mkdir -p "$TMP/env-tree"
printf '1.2.4-dev\n' >"$TMP/env-tree/VERSION"
printf '# Changelog\n\n## Unreleased\n\n- Pending.\n' >"$TMP/env-tree/NOTES.md"
# A non-default changelog name proves the env var is honored, not the default.
env_tree() {
(cd "$TMP/env-tree" && CHANGELOG=NOTES.md VERSION_SOURCE=file bash "$SCRIPT")
}
check "env vars drive the script the way action.yml does" 0 "agrees" env_tree
mkdir -p "$TMP/env-fragments/custom.d"
printf '1.2.4-dev\n' >"$TMP/env-fragments/VERSION"
printf '# Changelog\n\n## 1.2.3 — 2026-07-20\n\n- Shipped.\n' \
>"$TMP/env-fragments/CHANGELOG.md"
printf '%s\n' "# Changelog fragments" >"$TMP/env-fragments/custom.d/README.md"
env_fragments() {
(cd "$TMP/env-fragments" && FRAGMENTS_DIR=custom.d bash "$SCRIPT")
}
check "fragments-dir env var selects fragment mode" 0 "fragment mode" env_fragments
summary