From 5677710b2c806882a573c55f61495c6006c42ee5 Mon Sep 17 00:00:00 2001 From: cndgrr <59120057+cndgrr@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:01:39 +0000 Subject: [PATCH] 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 --- .github/workflows/ci.yml | 5 + test/docs-sync.test.sh | 12 ++- test/vendored.test.sh | 228 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 test/vendored.test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c17c424..e082bb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/test/docs-sync.test.sh b/test/docs-sync.test.sh index f5ecae9..ff0f5b0 100644 --- a/test/docs-sync.test.sh +++ b/test/docs-sync.test.sh @@ -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 -------------------------------------------------------- diff --git a/test/vendored.test.sh b/test/vendored.test.sh new file mode 100644 index 0000000..e8030a4 --- /dev/null +++ b/test/vendored.test.sh @@ -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 — 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 [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 — 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