forked from heavy-duty/ceremony
test(guards): the manifest guard drives the whole test plan
One CI step beside the self-ref pin, and test/vendored.test.sh covering both directions: the manifest -> tree scan (missing, symlink, directory, empty, ../ escape, absolute, untracked) and the closed-world root rule (neither list, vendored, exempted, prose is not an input, no recursion below the root), plus the real tree unmodified and the RELEASES.md regression both ways. The one-off `grep -Fx RELEASES.md` row at test/docs-sync.test.sh is deleted (#251 D4): two spellings of "the manifest is right" is the drift the manifest exists to prevent. Its intent is now a guard case, which the next doctrine file inherits for free. Refs #251
This commit is contained in:
parent
0760d0c21f
commit
5677710b2c
3 changed files with 240 additions and 5 deletions
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
|
@ -35,6 +35,11 @@ jobs:
|
|||
# Five stale markers survived the tags that shipped their machinery
|
||||
# (#221); #238 makes the release candidate reject that drift.
|
||||
run: bash .github/scripts/marker-check.sh
|
||||
- name: Vendored manifest
|
||||
# The manifest rules (issue #251; #248's near-miss): a doctrine file
|
||||
# at the root that nobody added to docs/VENDORED.txt is invisible to
|
||||
# every consumer's docs-sync, so it fails CI here instead.
|
||||
run: bash .github/scripts/vendored-check.sh
|
||||
- name: Tests
|
||||
env:
|
||||
# The npm-backed version_write case may skip locally when npm is
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ SCRIPT="$ROOT/actions/docs-sync/docs-sync.sh"
|
|||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# RELEASES.md's consumer-availability promise is true only when the real
|
||||
# manifest carries it (#248's review round). The fixture cases below prove
|
||||
# manifest-driven behavior; this row binds that behavior to the promised file.
|
||||
check "real manifest includes the release doctrine" 0 "RELEASES.md" \
|
||||
grep -Fx RELEASES.md "$ROOT/docs/VENDORED.txt"
|
||||
# The real manifest is asserted by test/vendored.test.sh, not here (#251 D4).
|
||||
# A `grep -Fx RELEASES.md` row lived at this spot from #248's review round,
|
||||
# binding the promise to the one file that had nearly been missed. It was the
|
||||
# hardcoded list the manifest exists to abolish, one layer down: two spellings
|
||||
# of "the manifest is right" is exactly the drift it prevents. Its intent —
|
||||
# every root doctrine file is declared, RELEASES.md included — is now a
|
||||
# closed-world guard case, which the next file inherits for free.
|
||||
|
||||
# --- fixture builders --------------------------------------------------------
|
||||
|
||||
|
|
|
|||
228
test/vendored.test.sh
Normal file
228
test/vendored.test.sh
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#!/usr/bin/env bash
|
||||
# Contract tests for .github/scripts/vendored-check.sh (issue #251) — the
|
||||
# self-guard that makes docs/VENDORED.txt authoritative over ceremony's OWN
|
||||
# tree. Driven against constructed fixture trees plus the real one; the CI
|
||||
# step runs the same script against the real tree.
|
||||
#
|
||||
# The fixture doc set is deliberately NOT ceremony's real six: a guard that
|
||||
# hardcodes the vendored list instead of reading the manifest fails these
|
||||
# rows, which is the failure the whole issue is about.
|
||||
#
|
||||
# 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"
|
||||
|
||||
CHECK="$ROOT/.github/scripts/vendored-check.sh"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# --- fixture builders --------------------------------------------------------
|
||||
|
||||
# tree <name> <manifest-entry...> — a fixture tree carrying only the manifest.
|
||||
tree() {
|
||||
local dir="$TMP/$1"
|
||||
shift
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$dir/docs"
|
||||
printf '%s\n' "$@" >"$dir/docs/VENDORED.txt"
|
||||
}
|
||||
|
||||
# doc <tree> <path> [content] — a regular file in a fixture tree.
|
||||
doc() {
|
||||
local path="$TMP/$1/$2"
|
||||
mkdir -p "$(dirname "$path")"
|
||||
printf '%s\n' "${3:-# a doc}" >"$path"
|
||||
}
|
||||
|
||||
# real_copy <name> — the real tree reduced to what the guard reads: the
|
||||
# manifest, every path the manifest names, and every root *.md. Cases mutate
|
||||
# the copy, so the guard's verdict on ceremony's actual doc set is proven
|
||||
# without touching the working tree.
|
||||
real_copy() {
|
||||
local dir="$TMP/$1" entry
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$dir/docs"
|
||||
cp "$ROOT/docs/VENDORED.txt" "$dir/docs/VENDORED.txt"
|
||||
cp "$ROOT"/*.md "$dir/"
|
||||
while IFS= read -r entry; do
|
||||
[ -n "$entry" ] || continue
|
||||
mkdir -p "$dir/$(dirname "$entry")"
|
||||
cp "$ROOT/$entry" "$dir/$entry"
|
||||
done <"$ROOT/docs/VENDORED.txt"
|
||||
}
|
||||
|
||||
run_check() {
|
||||
bash "$CHECK" "$TMP/$1"
|
||||
}
|
||||
|
||||
# --- the happy tree ----------------------------------------------------------
|
||||
|
||||
# One manifest entry lives in a subdirectory: the manifest is PATHS, not
|
||||
# filenames (docs-sync's fixtures prove the same), and the closed-world rule
|
||||
# over the root must not regress that to root-only.
|
||||
tree ok AGENTS.md RULES.md guide/DEEP.md
|
||||
doc ok AGENTS.md
|
||||
doc ok RULES.md
|
||||
doc ok guide/DEEP.md
|
||||
check "a tree whose root docs are all declared passes" 0 "3 manifest entries resolve" \
|
||||
run_check ok
|
||||
|
||||
tree blanks AGENTS.md '' RULES.md
|
||||
doc blanks AGENTS.md
|
||||
doc blanks RULES.md
|
||||
check "blank manifest lines are skipped, as docs-sync skips them" 0 "2 manifest entries" \
|
||||
run_check blanks
|
||||
|
||||
# --- the closed world: a root doc in neither list ----------------------------
|
||||
|
||||
# The #248 near-miss, replayed as a test: a new doctrine file lands at the
|
||||
# root and nobody adds it to the manifest.
|
||||
tree newdoc AGENTS.md
|
||||
doc newdoc AGENTS.md
|
||||
doc newdoc NEWDOC.md
|
||||
check "a root doc in neither list reds" 1 "'NEWDOC.md' is a root doc in NEITHER list" \
|
||||
run_check newdoc
|
||||
check "...and the refusal names the manifest fix" 1 "add 'NEWDOC.md' to docs/VENDORED.txt" \
|
||||
run_check newdoc
|
||||
check "...and the refusal names the exemption fix" 1 "add 'NEWDOC.md' to the exemption list" \
|
||||
run_check newdoc
|
||||
|
||||
# The decision the guard forces, taken each way: vendor it…
|
||||
tree newdoc-vendored AGENTS.md NEWDOC.md
|
||||
doc newdoc-vendored AGENTS.md
|
||||
doc newdoc-vendored NEWDOC.md
|
||||
check "a root doc added to the manifest passes" 0 "2 manifest entries" \
|
||||
run_check newdoc-vendored
|
||||
|
||||
# …or exempt it. The exemption list is in the script and carries a reason;
|
||||
# README.md is one of the four ceremony-only root docs it names.
|
||||
tree exempted AGENTS.md
|
||||
doc exempted AGENTS.md
|
||||
doc exempted README.md
|
||||
check "a root doc on the exemption list passes, with its reason" 0 "exempt: README.md" \
|
||||
run_check exempted
|
||||
|
||||
# The exemption list is NEVER read from prose. CONTRIBUTING.md's vendored-set
|
||||
# sentence is documentation; two declarations of the same set is the drift
|
||||
# the manifest exists to prevent (#251 D2's second must-fail).
|
||||
tree prose AGENTS.md
|
||||
doc prose AGENTS.md
|
||||
doc prose EXTRA.md
|
||||
doc prose CONTRIBUTING.md "The vendored set is AGENTS.md and EXTRA.md."
|
||||
check "a doc declared only in prose still reds" 1 "'EXTRA.md' is a root doc in NEITHER list" \
|
||||
run_check prose
|
||||
|
||||
# --- the rule is ROOT-level only ---------------------------------------------
|
||||
|
||||
# A guard that walked the tree would need an exemption list long enough that
|
||||
# nobody reads it — the exact failure the root-only rule is shaped against.
|
||||
# So an undeclared *.md under docs/, actions/ or drills/ must stay GREEN.
|
||||
tree subdirs AGENTS.md
|
||||
doc subdirs AGENTS.md
|
||||
doc subdirs docs/CONSUMERS.md
|
||||
doc subdirs actions/thing/README.md
|
||||
doc subdirs drills/2026-07-01.md
|
||||
check "undeclared *.md below the root stays green (no recursion)" 0 "1 manifest entries" \
|
||||
run_check subdirs
|
||||
|
||||
# --- manifest → tree: the scan -----------------------------------------------
|
||||
|
||||
tree missing AGENTS.md GONE.md
|
||||
doc missing AGENTS.md
|
||||
check "a manifest entry with no file reds, naming it" 1 "names 'GONE.md' but the tree has no such file" \
|
||||
run_check missing
|
||||
|
||||
tree symlinked AGENTS.md LINK.md
|
||||
doc symlinked AGENTS.md
|
||||
ln -s AGENTS.md "$TMP/symlinked/LINK.md"
|
||||
check "a manifest entry pointing at a symlink reds" 1 "names 'LINK.md', which is a SYMLINK" \
|
||||
run_check symlinked
|
||||
|
||||
tree dir-entry AGENTS.md guide
|
||||
doc dir-entry AGENTS.md
|
||||
mkdir -p "$TMP/dir-entry/guide"
|
||||
check "a manifest entry pointing at a directory reds" 1 "names 'guide', which is a DIRECTORY" \
|
||||
run_check dir-entry
|
||||
|
||||
tree empty-entry AGENTS.md HOLLOW.md
|
||||
doc empty-entry AGENTS.md
|
||||
: >"$TMP/empty-entry/HOLLOW.md"
|
||||
check "a manifest entry pointing at an empty file reds" 1 "names 'HOLLOW.md', which is EMPTY" \
|
||||
run_check empty-entry
|
||||
|
||||
# The escape case exists as a docs-sync fixture; ceremony's own manifest must
|
||||
# not be the one place it goes unchecked.
|
||||
tree escape AGENTS.md ../outside.md
|
||||
doc escape AGENTS.md
|
||||
check "a manifest entry escaping with ../ reds" 1 "names '../outside.md'" \
|
||||
run_check escape
|
||||
|
||||
tree absolute AGENTS.md /etc/hosts
|
||||
doc absolute AGENTS.md
|
||||
check "an absolute manifest entry reds" 1 "names '/etc/hosts'" \
|
||||
run_check absolute
|
||||
|
||||
# --- the manifest itself -----------------------------------------------------
|
||||
|
||||
rm -rf "$TMP/no-manifest"
|
||||
mkdir -p "$TMP/no-manifest"
|
||||
check "a tree with no manifest reds" 1 "no docs/VENDORED.txt under" \
|
||||
run_check no-manifest
|
||||
|
||||
rm -rf "$TMP/empty-manifest"
|
||||
mkdir -p "$TMP/empty-manifest/docs"
|
||||
: >"$TMP/empty-manifest/docs/VENDORED.txt"
|
||||
check "an empty manifest reds" 1 "is empty" run_check empty-manifest
|
||||
|
||||
# --- tracked-ness ------------------------------------------------------------
|
||||
|
||||
# A file present on this machine but absent from the tag's tree cannot be
|
||||
# fetched by a consumer syncing at that tag. The assertion binds only where
|
||||
# it can: when the tree IS a git work tree root.
|
||||
tree tracked AGENTS.md RULES.md
|
||||
doc tracked AGENTS.md
|
||||
doc tracked RULES.md
|
||||
git init -q "$TMP/tracked"
|
||||
git -C "$TMP/tracked" add docs/VENDORED.txt AGENTS.md RULES.md
|
||||
check "a git tree whose manifest entries are all tracked passes" 0 "2 manifest entries" \
|
||||
run_check tracked
|
||||
|
||||
tree untracked AGENTS.md RULES.md
|
||||
doc untracked AGENTS.md
|
||||
doc untracked RULES.md
|
||||
git init -q "$TMP/untracked"
|
||||
git -C "$TMP/untracked" add docs/VENDORED.txt AGENTS.md
|
||||
check "a git tree with an untracked manifest entry reds" 1 "names 'RULES.md', which is not TRACKED" \
|
||||
run_check untracked
|
||||
|
||||
# --- the real tree -----------------------------------------------------------
|
||||
|
||||
check "this tree, unmodified, is green" 0 "manifest entries resolve" bash "$CHECK" "$ROOT"
|
||||
|
||||
# The #248 near-miss on the REAL doc set: a scratch root doc nobody declared.
|
||||
real_copy scratch
|
||||
doc scratch SCRATCHDOC.md
|
||||
check "a scratch root doc on the real tree reds, naming it" 1 "'SCRATCHDOC.md' is a root doc in NEITHER list" \
|
||||
run_check scratch
|
||||
|
||||
# RELEASES.md stays listed — the regression criterion #248's review round
|
||||
# bought, now asserted BY THE GUARD rather than by a hardcoded `grep -Fx` row
|
||||
# in test/docs-sync.test.sh (#251 D1, D4). The closed world holds in both
|
||||
# directions: dropping it from the manifest alone reds…
|
||||
real_copy releases-dropped
|
||||
grep -v '^RELEASES\.md$' "$ROOT/docs/VENDORED.txt" >"$TMP/releases-dropped/docs/VENDORED.txt"
|
||||
check "dropping RELEASES.md from the manifest alone reds" 1 "'RELEASES.md' is a root doc in NEITHER list" \
|
||||
run_check releases-dropped
|
||||
|
||||
# …and it is green only when the file leaves the root in the same breath.
|
||||
real_copy releases-gone
|
||||
grep -v '^RELEASES\.md$' "$ROOT/docs/VENDORED.txt" >"$TMP/releases-gone/docs/VENDORED.txt"
|
||||
rm -f "$TMP/releases-gone/RELEASES.md"
|
||||
check "dropping RELEASES.md from the manifest AND the root is green" 0 "manifest entries resolve" \
|
||||
run_check releases-gone
|
||||
|
||||
summary
|
||||
Loading…
Reference in a new issue