voyage/tests/e2e-fix-round-4-5b2-a.sh

303 lines
11 KiB
Bash

#!/usr/bin/env bash
# tests/e2e-fix-round-4-5b2-a.sh
# End to end for fix round 4.5b2, part A (plan
# .claude/projects/2026-10-31-guard-4-5b2-part-a, End-to-end:, decisions 91,
# 107 and 123): the Bash hook (hooks/scripts/pre-bash-executor.mjs) reads
# commands the way a session sends them, and /bin/bash and /bin/zsh -f show,
# each in a temp directory with HOME set to a temp directory, that each block
# is real and each pass is text to the shell.
# 1. ESR: the three commands that newly pass (a here-string, a closed quoted
# heredoc, a closed quote before an escaped <) pass the hook, and both
# shells run the line after them as a command (a stub next on PATH
# touches ran-next) and create no PWNED file.
# 2. Blocks that are no false positive: SFX-X and SFX-dot print
# body EXPANDED under both shells; SIS1 and the eval and sh -c forms of a
# twice-escaped operator (X1, X1s) create PWNED under both shells; the
# [CNT] command expands under /bin/bash, while zsh stops with a parse
# error.
# 3. The shipped form still passes: the parser block of
# commands/trekreview.md (## Phase 1), filled with the repo root and the
# typed argument fix "<<" handling.
# A missing /bin/zsh is a FAIL, not a skip.
# Prints one "ok - <case>" or "FAIL - <case>: <why>" line per case and exits 1
# when any case failed, else 0. Starts nothing but node, /bin/bash and
# /bin/zsh (and the usual file tools). bash 3.2 compatible, ASCII only, no
# arrays.
set -u
cd "$(dirname "$0")/.." || { echo "FAIL - setup: cannot cd to the repo root"; exit 1; }
ROOT="$(pwd)"
HOOK="$ROOT/hooks/scripts/pre-bash-executor.mjs"
unset CLAUDE_CODE_SESSION_ID
TMP="$(mktemp -d "${TMPDIR:-/tmp}/e2e-fix-round-4-5b2-a.XXXXXX")" || { echo "FAIL - setup: mktemp"; exit 1; }
trap 'rm -rf "$TMP"' EXIT
TMP="$(cd "$TMP" && pwd -P)" || { echo "FAIL - setup: cannot enter the temp directory"; exit 1; }
export LC_ALL=C
export LANG=C
FAILED=0
pass() { echo "ok - $1"; }
bad() { echo "FAIL - $1"; FAILED=$((FAILED + 1)); }
# The premises' HOME, and a stub next that touches ran-next in the current
# directory.
mkdir -p "$TMP/home" "$TMP/bin" || { echo "FAIL - setup: cannot make the temp directories"; exit 1; }
printf '%s\n' '#!/bin/sh' 'touch ran-next' > "$TMP/bin/next" && chmod +x "$TMP/bin/next" \
|| { echo "FAIL - setup: cannot write the next stub"; exit 1; }
# hookrun <out> <line>...: the Bash hook with a Bash tool input whose command
# is the given lines joined by newlines; stderr goes to <out>. Returns the
# hook's exit status.
hookrun() {
out="$1"
shift
node -e 'process.stdout.write(JSON.stringify({ tool_name: "Bash", tool_input: { command: process.argv.slice(1).join("\n") } }));' "$@" \
| node "$HOOK" > /dev/null 2> "$out"
}
# hookfile <out> <file>: the Bash hook with the file's text as the command.
hookfile() {
node -e 'process.stdout.write(JSON.stringify({ tool_name: "Bash", tool_input: { command: require("fs").readFileSync(process.argv[1], "utf8") } }));' "$2" \
| node "$HOOK" > /dev/null 2> "$1"
}
D=VOYAGE_ARGS_EOF_7F3C9E
# blocked <name> <out> <line>...: the hook exits 2 and names args-heredoc-closed-early.
blocked() {
bname="$1"
bout="$2"
shift 2
hookrun "$bout" "$@"
c=$?
if [ "$c" -ne 2 ]; then
bad "$bname: the hook exited $c, not 2: $(cat "$bout")"
return 1
elif ! grep -q 'args-heredoc-closed-early' "$bout"; then
bad "$bname: the hook did not name args-heredoc-closed-early: $(cat "$bout")"
return 1
fi
return 0
}
# unquoted <name> <out> <line>...: blocked, with the unquoted reason.
unquoted() {
qname="$1"
uout="$2"
shift 2
blocked "$qname" "$uout" "$@" || return 1
if ! grep -q "opens $D unquoted" "$uout"; then
bad "$qname: the hook did not give the unquoted reason: $(cat "$uout")"
return 1
fi
return 0
}
# allowed <name> <out> <line>...: the hook exits 0.
allowed() {
aname="$1"
aout="$2"
shift 2
hookrun "$aout" "$@"
c=$?
if [ "$c" -ne 0 ]; then
bad "$aname: the hook exited $c: $(cat "$aout")"
return 1
fi
return 0
}
# premise <shell> <dir> <script> <out> <line>...: writes the lines to <script>
# and runs it in <dir> with /bin/bash (<shell> bash) or /bin/zsh -f (<shell>
# zsh), HOME=$TMP/home and the next stub first on PATH; stdout and stderr go
# to <out>, the shell's exit status to PSTATUS. Returns 1 when the script
# cannot be written or the shell is unknown or missing.
PSTATUS=0
premise() {
pshell="$1"
pdir="$2"
pscript="$3"
pout="$4"
shift 4
PSTATUS=0
mkdir -p "$pdir" || return 1
printf '%s\n' "$@" > "$pscript" || return 1
case "$pshell" in
bash) [ -x /bin/bash ] || return 1 ;;
zsh) [ -x /bin/zsh ] || return 1 ;;
*) return 1 ;;
esac
if [ "$pshell" = bash ]; then
( cd "$pdir" && HOME="$TMP/home" PATH="$TMP/bin:/usr/bin:/bin" /bin/bash "$pscript" ) > "$pout" 2>&1 < /dev/null
else
( cd "$pdir" && HOME="$TMP/home" PATH="$TMP/bin:/usr/bin:/bin" /bin/zsh -f "$pscript" ) > "$pout" 2>&1 < /dev/null
fi
PSTATUS=$?
return 0
}
# ---- 0. Setup: both shells are there ----
case_shells() {
if [ ! -x /bin/bash ]; then
bad 'setup: /bin/bash is missing'
fi
if [ ! -x /bin/zsh ]; then
bad 'setup: /bin/zsh is missing (a FAIL, not a skip)'
fi
}
# ---- 1. ESR: the commands that newly pass ----
# esr_case <tag> <name> <line>...: the hook passes the lines; under each shell
# the stub next runs (ran-next) and no PWNED file appears.
esr_case() {
etag="$1"
ename="$2"
shift 2
allowed "$ename" "$TMP/$etag.err" "$@" || return
for sh in bash zsh; do
edir="$TMP/$etag-$sh"
premise "$sh" "$edir" "$TMP/$etag-$sh.sh" "$TMP/$etag-$sh.out" "$@" \
|| { bad "$ename: could not run the premise under $sh"; return; }
if [ ! -e "$edir/ran-next" ]; then
bad "$ename: $sh did not run the line after the opener as a command: $(cat "$TMP/$etag-$sh.out")"
return
fi
if ls "$edir" | grep -q '^PWNED'; then
bad "$ename: $sh created a PWNED file"
return
fi
done
pass "$ename (exit 0; /bin/bash and /bin/zsh -f run next as a command)"
}
case_esr() {
esr_case esr-1 'ESR: a here-string before an escaped < leaves the rule on' \
'cat <<<x; echo \<<'"$D" 'next'
esr_case esr-2 'ESR: a closed quoted heredoc before an escaped < leaves the rule on' \
'cat <<'"'"'EOF'"'" "don't" 'EOF' 'echo \<<'"$D" 'next'
esr_case esr-3 'ESR: a quote closed before an escaped < leaves the rule on' \
'echo '"'"'x'"'"' && echo \<<'"$D" 'next'
}
# ---- 2. Blocks that are no false positive ----
# expanded_case <tag> <name> <line>...: the hook blocks the lines as unquoted;
# under each shell the body prints body EXPANDED.
expanded_case() {
xtag="$1"
xname="$2"
shift 2
unquoted "$xname" "$TMP/$xtag.err" "$@" || return
for sh in bash zsh; do
premise "$sh" "$TMP/$xtag-$sh" "$TMP/$xtag-$sh.sh" "$TMP/$xtag-$sh.out" "$@" \
|| { bad "$xname: could not run the premise under $sh"; return; }
if ! grep -q 'body EXPANDED' "$TMP/$xtag-$sh.out"; then
bad "$xname: $sh did not expand the body: $(cat "$TMP/$xtag-$sh.out")"
return
fi
done
pass "$xname (exit 2; /bin/bash and /bin/zsh -f print body EXPANDED)"
}
# pwned_case <tag> <name> <line>...: the hook blocks the lines as unquoted;
# under each shell the body creates PWNED.
pwned_case() {
ptag="$1"
pname="$2"
shift 2
unquoted "$pname" "$TMP/$ptag.err" "$@" || return
for sh in bash zsh; do
premise "$sh" "$TMP/$ptag-$sh" "$TMP/$ptag-$sh.sh" "$TMP/$ptag-$sh.out" "$@" \
|| { bad "$pname: could not run the premise under $sh"; return; }
if [ ! -e "$TMP/$ptag-$sh/PWNED" ]; then
bad "$pname: $sh did not expand the body: $(cat "$TMP/$ptag-$sh.out")"
return
fi
done
pass "$pname (exit 2; /bin/bash and /bin/zsh -f create PWNED)"
}
case_blocks() {
expanded_case sfx-x 'SFX: a split delimiter with an X suffix is blocked as unquoted' \
'cat <<VOYAGE_ARGS\' '_EOF_7F3C9EX' 'body $(echo EXPANDED)'
expanded_case sfx-dot 'SFX: a split delimiter with a . suffix is blocked as unquoted' \
'cat <<VOYAGE_ARGS\' '_EOF_7F3C9E.' 'body $(echo EXPANDED)'
pwned_case sis-1 'SIS: a split delimiter inside a bash -c string is blocked as unquoted' \
'bash -c '"'"'cat <<VOYAGE_ARGS\' '_EOF_7F3C9E'"'"'"' '\$(touch PWNED); touch RAN' '"'
pwned_case evl-x1 'EVL: an eval of a twice-escaped heredoc operator with a body is blocked as unquoted' \
'eval cat \<\<'"$D""'" '$(touch PWNED)' "'"
pwned_case evl-x1s 'EVL: the sh -c form of a twice-escaped heredoc operator is blocked as unquoted' \
'sh -c '"'"'cat '"'"'\<\<'"$D""'" '$(touch PWNED)' "'"
}
case_cnt() {
name='CNT: an outer opener split between its two < keeps the escape rule off'
L1='bash <\'
L2='<EOF'
L3='cat - \\\<<'"$D"
L4='\$(touch PWNED)'
L5="$D"
L6='EOF'
unquoted "$name" "$TMP/cnt.err" "$L1" "$L2" "$L3" "$L4" "$L5" "$L6" || return
premise bash "$TMP/cnt-bash" "$TMP/cnt-bash.sh" "$TMP/cnt-bash.out" "$L1" "$L2" "$L3" "$L4" "$L5" "$L6" \
|| { bad "$name: could not run the premise under bash"; return; }
if [ ! -e "$TMP/cnt-bash/PWNED" ]; then
bad "$name: /bin/bash did not expand the inner body: $(cat "$TMP/cnt-bash.out")"
return
fi
premise zsh "$TMP/cnt-zsh" "$TMP/cnt-zsh.sh" "$TMP/cnt-zsh.out" "$L1" "$L2" "$L3" "$L4" "$L5" "$L6" \
|| { bad "$name: could not run the premise under zsh"; return; }
if [ "$PSTATUS" -eq 0 ] || ! grep -q 'parse error' "$TMP/cnt-zsh.out"; then
bad "$name: /bin/zsh -f did not stop with a parse error (exit $PSTATUS): $(cat "$TMP/cnt-zsh.out")"
return
fi
pass "$name (exit 2; /bin/bash expands the inner body, /bin/zsh -f exits $PSTATUS with a parse error)"
}
# ---- 3. The shipped form still passes ----
case_shipped() {
name='shipped: the trekreview Phase 1 parser block with the typed argument fix "<<" handling passes'
# The block as written, with ${CLAUDE_PLUGIN_ROOT} and $ARGUMENTS filled in
# the way Claude Code fills them.
node -e '
const fs = require("fs");
const [md, root, args] = process.argv.slice(1);
const text = fs.readFileSync(md, "utf8");
const s = text.indexOf("## Phase 1 ");
if (s === -1) process.exit(3);
const rest = text.slice(s);
const e = rest.search(/\n## /);
const phase = e === -1 ? rest : rest.slice(0, e);
const blocks = phase.split("\n```bash\n").slice(1).map((b) => b.split("\n```")[0]).filter((b) => b.includes("<<" + String.fromCharCode(39) + "VOYAGE_ARGS_EOF_7F3C9E" + String.fromCharCode(39)));
if (blocks.length !== 1) { console.error("expected one Phase 1 parser block, found " + blocks.length); process.exit(3); }
process.stdout.write(blocks[0].split("${CLAUDE_PLUGIN_ROOT}").join(root).split("$ARGUMENTS").join(args) + "\n");
' "$ROOT/commands/trekreview.md" "$ROOT" 'fix "<<" handling' > "$TMP/phase1.sh" \
|| { bad "$name: could not read the Phase 1 block"; return; }
hookfile "$TMP/shipped.err" "$TMP/phase1.sh"
c=$?
if [ "$c" -ne 0 ]; then
bad "$name: the hook exited $c: $(cat "$TMP/shipped.err")"
else
pass "$name (exit 0)"
fi
}
case_shells
case_esr
case_blocks
case_cnt
case_shipped
if [ "$FAILED" -ne 0 ]; then
echo "e2e: $FAILED case(s) failed"
exit 1
fi
echo "e2e: ok"
exit 0