#!/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 - " or "FAIL - : " 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 ...: the Bash hook with a Bash tool input whose command # is the given lines joined by newlines; stderr goes to . 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 : 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 ...: 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 ...: 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 ...: 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