219 lines
7.7 KiB
Bash
219 lines
7.7 KiB
Bash
#!/usr/bin/env bash
|
|
# tests/e2e-fix-round-4-5b-part-4.sh
|
|
# End to end for fix round 4-5b, part 4 (plan
|
|
# .claude/projects/2026-10-31-fix-round-4-5b-part-4, End-to-end:, decision
|
|
# 90): the Bash hook (hooks/scripts/pre-bash-executor.mjs) reads commands the
|
|
# way a session sends them, and /bin/bash shows in a temp directory that each
|
|
# block is real and each pass is text to the shell.
|
|
# 1. CSG: a continuation-split unquoted opener with a body and no closing
|
|
# line is blocked as unquoted; /bin/bash expands its body.
|
|
# 2. SLT: a grep pattern '<<' on a line that names the delimiter, and a
|
|
# string holding << before the shipped argument heredoc, pass.
|
|
# 3. ESC: a backslash-escaped < starts no opener: the hook passes it, and
|
|
# /bin/bash runs the next line as a command.
|
|
# 4. ESC, the heredoc fed to a shell: an odd run of backslashes after a <<
|
|
# is still blocked as unquoted; /bin/bash expands the inner body.
|
|
# 5. 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.
|
|
# Prints one "ok - <case>" or "FAIL - <case>: <why>" line per case and exits 1
|
|
# when any case failed, else 0. Starts nothing but node and /bin/bash (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-5b-part-4.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)); }
|
|
|
|
# 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
|
|
PARSER_LINE='node /plugin/lib/parsers/arg-parser.mjs --command trekendsession --stdin'
|
|
|
|
# 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 <dir> <script> <out> <line>...: writes the lines to <script> and
|
|
# runs it with /bin/bash in <dir>; stdout and stderr go to <out>.
|
|
premise() {
|
|
pdir="$1"
|
|
pscript="$2"
|
|
pout="$3"
|
|
shift 3
|
|
mkdir -p "$pdir" || return 1
|
|
printf '%s\n' "$@" > "$pscript" || return 1
|
|
( cd "$pdir" && /bin/bash "$pscript" ) > "$pout" 2>&1 < /dev/null
|
|
return 0
|
|
}
|
|
|
|
# ---- 1. CSG ----
|
|
|
|
case_csg() {
|
|
name='CSG: a continuation-split unquoted opener with a body and no closing line is blocked as unquoted'
|
|
L1='cat <<VOYAGE_ARGS\'
|
|
L2='_EOF_7F3C9E'
|
|
L3='$(touch "'"$TMP"'/ran-csg")'
|
|
unquoted "$name" "$TMP/csg.err" "$L1" "$L2" "$L3" || return
|
|
premise "$TMP/csg" "$TMP/csg.sh" "$TMP/csg.out" "$L1" "$L2" "$L3" \
|
|
|| { bad "$name: could not write the premise script"; return; }
|
|
if [ ! -e "$TMP/ran-csg" ]; then
|
|
bad "$name: /bin/bash did not expand the body: $(cat "$TMP/csg.out")"
|
|
else
|
|
pass "$name (exit 2; /bin/bash expands the body, so the block is not a false positive)"
|
|
fi
|
|
}
|
|
|
|
# ---- 2. SLT ----
|
|
|
|
case_slt() {
|
|
name="SLT: a grep pattern '<<' that names the delimiter, and a string holding << before the shipped heredoc, pass"
|
|
allowed "$name (grep)" "$TMP/slt-1.err" "grep -n '<<' file.md | grep $D" 'echo done' || return
|
|
allowed "$name (string)" "$TMP/slt-2.err" "echo \"a<<b\" && $PARSER_LINE <<'$D'" 'brief.md' "$D" || return
|
|
pass "$name (exit 0 for each)"
|
|
}
|
|
|
|
# ---- 3. ESC ----
|
|
|
|
case_esc() {
|
|
name='ESC: a backslash-escaped < starts no opener'
|
|
L1='echo \<<'"$D"
|
|
L2='echo next'
|
|
allowed "$name" "$TMP/esc.err" "$L1" "$L2" || return
|
|
premise "$TMP/esc" "$TMP/esc.sh" "$TMP/esc.out" "$L1" "$L2" \
|
|
|| { bad "$name: could not write the premise script"; return; }
|
|
if ! grep -qx 'next' "$TMP/esc.out"; then
|
|
bad "$name: /bin/bash did not run the second line as a command: $(cat "$TMP/esc.out")"
|
|
else
|
|
pass "$name (exit 0; /bin/bash prints next, so no heredoc took the second line)"
|
|
fi
|
|
}
|
|
|
|
# ---- 4. ESC, the heredoc fed to a shell ----
|
|
|
|
case_esc_fed() {
|
|
name='ESC: an odd run of backslashes after a << in a heredoc fed to a shell is still blocked as unquoted'
|
|
L1='/bin/bash <<EOF'
|
|
L2='cat - \\\<<'"$D"
|
|
L3='\$(touch "'"$TMP"'/ran-esc")'
|
|
L4="$D"
|
|
L5='EOF'
|
|
unquoted "$name" "$TMP/esc-fed.err" "$L1" "$L2" "$L3" "$L4" "$L5" || return
|
|
premise "$TMP/esc-fed" "$TMP/esc-fed.sh" "$TMP/esc-fed.out" "$L1" "$L2" "$L3" "$L4" "$L5" \
|
|
|| { bad "$name: could not write the premise script"; return; }
|
|
if [ ! -e "$TMP/ran-esc" ]; then
|
|
bad "$name: /bin/bash did not expand the inner body: $(cat "$TMP/esc-fed.out")"
|
|
else
|
|
pass "$name (exit 2; /bin/bash expands the inner body, so the block is not a false positive)"
|
|
fi
|
|
}
|
|
|
|
# ---- 5. 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_csg
|
|
case_slt
|
|
case_esc
|
|
case_esc_fed
|
|
case_shipped
|
|
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
echo "e2e: $FAILED case(s) failed"
|
|
exit 1
|
|
fi
|
|
echo "e2e: ok"
|
|
exit 0
|