#!/usr/bin/env bash # tests/e2e-fix-round-4-5b-part-2.sh # End to end for fix round 4-5b, part 2 (plan # .claude/projects/2026-10-27-fix-round-4-5b-part-2, End-to-end:, decision # 88): the shipped products run the way a session runs them, in temp # directories and temp git repos. # 1. TOK: the arg parser (lib/parsers/arg-parser.mjs --command trekreview # --stdin) reads `--project "my dir"/x --since "HEAD~1"` from a quoted # heredoc, and its JSON keeps `--since` = `HEAD~1`. # 2. HDG: the Bash hook (hooks/scripts/pre-bash-executor.mjs) with a Bash # tool input on stdin: # a. a one-line `grep` that mentions the parser's heredoc opener exits 0; # b. a parser heredoc that closes early exits 2 (args-heredoc-closed-early); # c. a quoted first mention hiding an unquoted parser heredoc exits 2 # (args-heredoc-closed-early). # 3. CKI: `recurrence.mjs apply` in a repo whose `.claude` is a symlink to an # in-repo directory: the lesson is written, `pointer` is `check-failed`, # and CLAUDE.md is unchanged. # DLC is prose (commands/trekexecute.md Sub-step G); it is checked by # tests/lib/fix-round-4-5b-part-2-prose.test.mjs, not by this run. # Prints one "ok - " or "FAIL - : " line per case and exits 1 # when any case failed, else 0. Starts nothing but node and git (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)" ARGS="$ROOT/lib/parsers/arg-parser.mjs" HOOK="$ROOT/hooks/scripts/pre-bash-executor.mjs" REC="$ROOT/lib/review/recurrence.mjs" unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE CLAUDE_CODE_SESSION_ID TMP="$(mktemp -d "${TMPDIR:-/tmp}/e2e-fix-round-4-5b-part-2.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; } mkdir -p "$TMP/hooks-none" "$TMP/home" || { echo "FAIL - setup: mkdir"; exit 1; } : > "$TMP/empty-gitconfig" || { echo "FAIL - setup: empty git config"; exit 1; } # The git environment is fixed here, so a global core.excludesFile, the temp # directory's location or a translated git cannot change a result. export HOME="$TMP/home" export XDG_CONFIG_HOME="$TMP/home" export GIT_CONFIG_GLOBAL="$TMP/empty-gitconfig" export GIT_CONFIG_NOSYSTEM=1 export GIT_CEILING_DIRECTORIES="$TMP" export LC_ALL=C export LANG=C unset LANGUAGE FAILED=0 pass() { echo "ok - $1"; } bad() { echo "FAIL - $1"; FAILED=$((FAILED + 1)); } # newrepo : git init with no hooks, no signing, one repo-local identity. newrepo() { mkdir -p "$1" || return 1 git init -q "$1" || return 1 printf '[user]\n\tname = Test\n\temail = test@example.invalid\n[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "%s"\n' \ "$TMP/hooks-none" >> "$1/.git/config" } # jsonok : stdin is JSON; exit 0 when the expression is true. jsonok() { node -e 'let s="";process.stdin.on("data",(d)=>{s+=d;}).on("end",()=>{let j;try{j=JSON.parse(s);}catch(e){process.exit(3);}process.exit(Function("j","return ("+process.argv[1]+");")(j)?0:1);});' "$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" } D=VOYAGE_ARGS_EOF_7F3C9E PARSER_LINE='node /plugin/lib/parsers/arg-parser.mjs --command trekendsession --stdin' # ---- 1. TOK: the arg parser keeps a later flag ---- case_tok() { name="arg-parser --stdin: a quoted value never swallows --since" W="$TMP/args" mkdir -p "$W" || { bad "$name: mkdir"; return; } ( cd "$W" && node "$ARGS" --command trekreview --stdin > "$TMP/args.json" ) <<'VOYAGE_ARGS_EOF_7F3C9E' --project "my dir"/x --since "HEAD~1" VOYAGE_ARGS_EOF_7F3C9E c=$? if [ "$c" -ne 0 ]; then bad "$name: the parser exited $c: $(cat "$TMP/args.json")" elif ! jsonok 'j.flags["--since"] === "HEAD~1"' < "$TMP/args.json"; then bad "$name: --since is not HEAD~1: $(cat "$TMP/args.json")" else pass "$name (--since = HEAD~1)" fi } # ---- 2. HDG: the Bash hook ---- case_hdg_one_line() { name="Bash hook: a one-line grep that mentions the opener passes" hookrun "$TMP/hdg-1.err" "grep -n \"<<'$D'\" commands/trekreview.md" c=$? if [ "$c" -ne 0 ]; then bad "$name: the hook exited $c: $(cat "$TMP/hdg-1.err")" else pass "$name (exit 0)" fi } case_hdg_early_close() { name="Bash hook: a parser heredoc that closes early is blocked" hookrun "$TMP/hdg-2.err" "$PARSER_LINE <<'$D'" 'brief.md' "$D" 'touch PWNED' c=$? if [ "$c" -ne 2 ]; then bad "$name: the hook exited $c, not 2: $(cat "$TMP/hdg-2.err")" elif ! grep -q 'args-heredoc-closed-early' "$TMP/hdg-2.err"; then bad "$name: the hook did not name args-heredoc-closed-early: $(cat "$TMP/hdg-2.err")" else pass "$name (exit 2, args-heredoc-closed-early)" fi } case_hdg_hidden() { name="Bash hook: a quoted first mention does not hide an unquoted parser heredoc" hookrun "$TMP/hdg-3.err" "echo \"<<'$D'\"" "$PARSER_LINE <<$D" 'brief.md $(touch PWNED)' "$D" c=$? if [ "$c" -ne 2 ]; then bad "$name: the hook exited $c, not 2: $(cat "$TMP/hdg-3.err")" elif ! grep -q 'args-heredoc-closed-early' "$TMP/hdg-3.err"; then bad "$name: the hook did not name args-heredoc-closed-early: $(cat "$TMP/hdg-3.err")" else pass "$name (exit 2, args-heredoc-closed-early)" fi } # ---- 3. CKI: applyLesson with a symlinked .claude ---- # lessonreview : a review.md with one MISSING_TEST finding. lessonreview() { mkdir -p "$(dirname "$1")" || return 1 fid="$(printf '%s0000000000000000000000000000000000000000' "$4" | cut -c1-40)" cat > "$1" < "$R/CLAUDE.md" out="$(node "$REC" apply --repo "$R" --rule-key MISSING_TEST)"; c=$? if [ "$c" -ne 0 ]; then bad "$name: apply exited $c: $out" elif ! printf '%s' "$out" | jsonok 'j.written === true && j.pointer === "check-failed" && /check-ignore/.test(String(j.pointer_note))'; then bad "$name: the result is not written with pointer check-failed and a check-ignore note: $out" elif [ "$(cat "$R/CLAUDE.md")" != "# Project" ]; then bad "$name: CLAUDE.md was changed" else pass "$name" fi } case_tok case_hdg_one_line case_hdg_early_close case_hdg_hidden case_cki_symlink echo "e2e: DLC is prose; tests/lib/fix-round-4-5b-part-2-prose.test.mjs checks it, not this run" if [ "$FAILED" -ne 0 ]; then echo "e2e: $FAILED case(s) failed" exit 1 fi echo "e2e: ok" exit 0