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:
cndgrr 2026-08-04 10:01:39 +00:00
parent 0760d0c21f
commit 5677710b2c
3 changed files with 240 additions and 5 deletions

View file

@ -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

View file

@ -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
View 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