Files
holocron/tests/test-check-rtk-prefix.sh
Defame1297 ed8c99efbd fix(git): stop prefixing rtk where it rewrites the output skills parse
The #113 sweep rested on CLAUDE.md's premise that rtk either filters or passes through unchanged,
so prefixing is always safe. Measured against rtk 0.42.4, that premise is false for several of the
commands the sweep prefixed, and two skills were left giving wrong answers silently.

Why:
- `rtk git worktree list --porcelain -z` discards both flags and renders its own format. The
  `locked`/`lock_reason` fields git-worktrees Step 2 must emit are absent entirely, and paths under
  $HOME are abbreviated to `~/`.
- `rtk git branch --list <name>` prints a phantom `* ` line even when nothing matches, so
  git-branches' stated ambiguity test — "output from both means the name is ambiguous" — reported
  every name as ambiguous. `tag --list` is a clean passthrough, so only one half broke.
- `rtk git diff --name-only`/`--name-status` append a `Changes:` trailer to output documented as
  "one per line"; `--word-diff` emits none of the `[-removed-] {+added+}` markers its table
  describes; `rtk git log -L` truncates each line at ~72 chars, on the one command whose purpose is
  showing line content.
- `rtk git stash pop` prints only `FAILED: git stash pop`, swallowing the conflict diagnostic and
  retained-entry message the surrounding prose tells the agent to rely on.

Implementation notes:
- Eleven sites reverted to bare `git`, each carrying its reason inline so the next sweep does not
  undo it. `mergetool` and `rebase -i` are reverted on clause 3's interactive limb only: the TTY
  defect does not reproduce — rtk filters exactly twelve subcommands and execs the rest — and
  ADR-0023 records that measurement rather than a convenient one.
- ADR-0023 states the rule repo-wide with a third clause: a command whose output the skill parses,
  or which is interactive, stays bare. `plugins/git/README.md` is reduced to a pointer; its claim
  that gitea skills "contain no git/rtk mentions at all" was false, and its citation of
  `hard-rules.md` pointed at a file containing no occurrence of "rtk".
- Eight gitea sites swept, all verified byte-identical passthroughs first.
- `scripts/check-rtk-prefix.sh` gates clause 1. Run against main's pre-sweep corpus it reports 99
  findings including every gitea site, so it would have caught the drift #113 was filed about.

Impact: the gate covers clause 1 only, in shell-tagged fences and the opening span of Run cells.
Clause 2 is not gateable — "Run `git switch`" and "`git switch` refuses" are the same tokens — and
prose bullets are invisible to it. Both limits are recorded in gates.md rather than left implied.

Refs: #113
ADR: 0023
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeH8SCbcrCAQrtymkNuhKP
2026-09-09 05:14:54 +00:00

225 lines
7.4 KiB
Bash

#!/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 on `main` 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.
#
# 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 on main trips the gate ---"
# Derived from git, not hardcoded: the point is "the drift this gate exists for",
# and a hardcoded path list goes stale the moment a file is renamed.
if ! git -C "$REPO_ROOT" rev-parse --verify -q main >/dev/null; then
echo "SKIP: no local 'main' ref — 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 main -- '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 on main — the historical case checked nothing"
else
for f in ${PRE_FILES[@]+"${PRE_FILES[@]}"}; do
mkdir -p "$PRE/$(dirname "$f")"
git -C "$REPO_ROOT" show "main:$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 <branch>` — 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 <b> <p> <r>/<b>` — always correct. `git worktree add <p> <b>` expands to exactly this |' \
"a referential bare mention AFTER the instructed command in a Run cell"
expect clean '| Operation | Run |
|---|---|
| **Never** `git worktree add <p> <r>/<b>` | 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 ]]