#!/usr/bin/env bash set -euo pipefail # Tests for scripts/check-rtk-prefix.sh — ADR-0023 clause 1. # # Case 1 is the one that earns the rest: it runs the gate against the corpus as # it stood BEFORE the #113 sweep, and asserts it fails there. A gate that only # passes on the already-fixed tree proves nothing about whether it would have # caught the drift it was written for. # # Pinned to a commit SHA, not the `main` ref: `main` is the moving integration # branch, and the #113 fix (ed8c99e) landed back on it, so querying live # `main` for the "pre-sweep" corpus stopped reproducing the drift the moment # that fix merged — the case silently started asserting the opposite of what # it says. 598a7c3 is the last commit where gitea-issues/SKILL.md still had # the bare `git remote get-url origin` call ed8c99e fixed. # # Everything after that is synthetic. The false-NEGATIVE cases (a bare command # the gate must catch) and the false-POSITIVE cases (correct content the gate # must leave alone) carry equal weight: this hook's failure mode is not missing # a violation, it is firing on deliberately-bare clause-2 and clause-3 content # until someone adds it to SKIP. REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SCRIPT="$REPO_ROOT/scripts/check-rtk-prefix.sh" PASS=0 FAIL=0 pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } if ! command -v python3 >/dev/null 2>&1; then echo "SKIP: python3 is not installed — the script under test fails closed on it, so every case here would only re-assert the missing-dependency guard" exit 77 fi RUN_TMP="$(mktemp -d)" trap 'rm -rf "$RUN_TMP"' EXIT # Writes $2 to a .md file and asserts the gate's verdict. $1 is "clean" or # "dirty"; $3 is the case description. expect() { local want="$1" body="$2" desc="$3" local f="$RUN_TMP/case.md" printf '%s\n' "$body" > "$f" local rc=0 bash "$SCRIPT" "$f" > "$RUN_TMP/out" 2>&1 || rc=$? if [[ "$want" == "dirty" && $rc -eq 0 ]]; then fail "$desc — expected a violation, exited 0" elif [[ "$want" == "clean" && $rc -ne 0 ]]; then fail "$desc — expected no violation, exited $rc" sed 's/^/ /' "$RUN_TMP/out" else pass "$desc" fi } echo "--- the pre-#113 corpus trips the gate ---" # The commit set is derived from git, not hardcoded, at this fixed SHA: the # point is "the drift this gate exists for", and a hardcoded path list goes # stale the moment a file is renamed. The SHA itself is deliberately hardcoded # — see the header comment for why it must NOT be a moving ref like `main`. PRE_SWEEP_REF="598a7c326a4bbfa4a39007eb95d944c4150686e8" if ! git -C "$REPO_ROOT" rev-parse --verify -q "$PRE_SWEEP_REF" >/dev/null; then echo "SKIP: pre-sweep commit $PRE_SWEEP_REF not reachable locally (shallow clone?) — the historical-corpus case cannot be reconstructed" exit 77 fi PRE="$RUN_TMP/pre" # A read loop, not the bash-4 array builtin: tests/test-vale-wrap.sh scans every # script under tests/ for bash-4-only constructs, because these run on macOS's # bash 3.2. Same reason for the `[@]+` guards on every array expansion below. PRE_FILES=() PRE_PATHS=() while IFS= read -r PRE_REL; do PRE_FILES+=("$PRE_REL") PRE_PATHS+=("$PRE/$PRE_REL") done < <( git -C "$REPO_ROOT" ls-tree -r --name-only "$PRE_SWEEP_REF" -- 'plugins' \ | grep -E '^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$' \ | grep -v '/README\.md$' ) if [[ ${#PRE_FILES[@]} -eq 0 ]]; then fail "no plugin skill/agent files found at $PRE_SWEEP_REF — the historical case checked nothing" else for f in ${PRE_FILES[@]+"${PRE_FILES[@]}"}; do mkdir -p "$PRE/$(dirname "$f")" git -C "$REPO_ROOT" show "$PRE_SWEEP_REF:$f" > "$PRE/$f" done if bash "$SCRIPT" ${PRE_PATHS[@]+"${PRE_PATHS[@]}"} > "$RUN_TMP/pre.out" 2>&1; then fail "the pre-sweep corpus passed — the gate would not have caught the #113 drift" else hits="$(grep -c '^ADR-0023: ' "$RUN_TMP/pre.out" || true)" if [[ "$hits" -lt 20 ]]; then fail "the pre-sweep corpus produced only $hits findings — too few to be the known drift" elif ! grep -q 'gitea-issues/SKILL.md.*git remote get-url origin' "$RUN_TMP/pre.out"; then fail "the pre-sweep corpus failed, but not on the known gitea drift" sed 's/^/ /' "$RUN_TMP/pre.out" | head -5 else pass "the pre-sweep corpus fails with $hits findings, including the gitea sweep gap" fi fi fi echo "" echo "--- shell fences: what must fail ---" expect dirty '```bash git commit -m "x" ```' "a bare git command in a bash fence" expect dirty '```sh rtk git add -u && git commit -m "x" ```' "a bare git after && on a line that starts with rtk git" expect dirty '```bash SKIP=check-yaml git commit -m "x" ```' "a bare git behind an environment-variable prefix" expect dirty '```bash url=$(git config remote.origin.url) ```' "a bare git inside a command substitution" expect dirty '```console $ git status ```' "a bare git behind a copied shell prompt" echo "" echo "--- shell fences: what must NOT fail ---" expect clean '```bash rtk git commit -m "x" rtk git push ```' "prefixed commands" expect clean '```bash git stash list # bare per ADR-0023: rtk prints "No stashes" where git prints nothing ```' "a bare command carrying the ADR-0023 opt-out marker" expect clean '```text git worktree list --porcelain -z ```' "a non-shell fence (text) is out of scope" expect clean '```yaml entry: git ```' "a yaml fence is out of scope" expect clean '```bash # never reach for git commit --no-verify here rtk git commit ```' "a bare git inside a shell comment" expect clean 'Run `git switch ` — no wait, this is prose, not a fence.' \ "a bare git in a prose line outside any fence" echo "" echo "--- dispatch tables ---" expect dirty '| Operation | Run | |---|---| | List | `git worktree list -v` |' "a bare command opening a Run cell" expect clean '| Operation | Run | |---|---| | List | `rtk git worktree list -v` |' "a prefixed command in a Run cell" expect clean '| Operation | Run | |---|---| | List | `git worktree list -v` — bare per ADR-0023 |' \ "a bare Run cell carrying the opt-out marker" # The clause-2 shapes that made an every-span check unusable. Both are real rows # from git-worktrees/SKILL.md. expect clean '| Operation | Run | |---|---| | Track | `rtk git worktree add --track -b

/` — always correct. `git worktree add

` expands to exactly this |' \ "a referential bare mention AFTER the instructed command in a Run cell" expect clean '| Operation | Run | |---|---| | **Never** `git worktree add

/` | That ref resolves, so `git push` needs an explicit refspec |' \ "an anti-pattern row whose Run cell opens with prose" expect clean '| Flag | Meaning | |---|---| | `-L` | as in `git log -L` |' "a table with no Run column is out of scope" echo "" echo "--- degenerate inputs ---" expect clean '' "an empty file" expect clean '```bash rtk git status' "an unterminated fence does not crash the parser" if bash "$SCRIPT" > "$RUN_TMP/noargs.out" 2>&1; then pass "no filenames exits 0 rather than erroring" else fail "no filenames should exit 0 — pre-commit calls hooks with an empty file list" fi if bash "$SCRIPT" "$RUN_TMP/does-not-exist.md" > "$RUN_TMP/missing.out" 2>&1; then fail "an unreadable file exited 0 — unreadable must be an error, never a silent pass" elif ! grep -q 'could not read file' "$RUN_TMP/missing.out"; then fail "an unreadable file failed for the wrong reason" sed 's/^/ /' "$RUN_TMP/missing.out" else pass "an unreadable file exits 1 and says so" fi echo "" echo "--- the live corpus is clean ---" LIVE=() while IFS= read -r LIVE_REL; do LIVE+=("$LIVE_REL") done < <( git -C "$REPO_ROOT" ls-files -- 'plugins' \ | grep -E '^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$' \ | grep -v '/README\.md$' ) if [[ ${#LIVE[@]} -eq 0 ]]; then fail "no plugin skill/agent files matched the hook's files: pattern" elif (cd "$REPO_ROOT" && bash "$SCRIPT" ${LIVE[@]+"${LIVE[@]}"} > "$RUN_TMP/live.out" 2>&1); then pass "the ${#LIVE[@]} in-scope corpus files pass" else fail "the live corpus has ADR-0023 clause-1 violations" sed 's/^/ /' "$RUN_TMP/live.out" fi echo "" echo "Results: $PASS passed, $FAIL failed" [[ $FAIL -eq 0 ]]