forked from heavy-duty/ceremony
229 lines
8.5 KiB
Bash
229 lines
8.5 KiB
Bash
|
|
#!/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
|