#!/usr/bin/env bash # tests/e2e-fix-round-4-5a.sh # End to end for fix round 4-5a (plan # .claude/projects/2026-10-20-fix-round-serious-findings, End-to-end:): the # shipped CLIs run the way the commands run them, in temp git repos. # 1. The park tool (lib/util/park-attempt-files.mjs), on the diff the # "Back to planning" line saves: # a. a new file registered --intent-to-add is parked (exit 0, gone from # the tree and the index, kept under .git/voyage/parked/); # b. a coloured diff (--color=always, as color.diff=always gives it) # still parks its new file; # c. --cwd a subdirectory of the repo parks the root-relative path. # 2. The verdict gate (lib/review/verdict-gate.mjs --json --repo .): # a review of HEAD (0), a later lessons-only commit (0), a later source # commit (2, reason review-stale). # 3. The arg parser (lib/parsers/arg-parser.mjs --command trekendsession -- # fix-header): no help flag, the slug is the first positional. # 4. The gates shim (lib/util/autonomy-gate.mjs): `--gates closed` and a bare # `--gates` both give next_state gates_on. # Prints "e2e: ok" and exits 0, or names the failed check and exits 1. # bash 3.2 compatible, ASCII only. set -u cd "$(dirname "$0")/.." || { echo "e2e: FAIL - cannot cd to the repo root" >&2; exit 1; } ROOT="$(pwd)" PARK="$ROOT/lib/util/park-attempt-files.mjs" VERDICT="$ROOT/lib/review/verdict-gate.mjs" ARGS="$ROOT/lib/parsers/arg-parser.mjs" SHIM="$ROOT/lib/util/autonomy-gate.mjs" unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE TMP="$(mktemp -d "${TMPDIR:-/tmp}/e2e-fix-round-4-5a.XXXXXX")" || { echo "e2e: FAIL - mktemp" >&2; exit 1; } trap 'rm -rf "$TMP"' EXIT fail() { echo "e2e: FAIL - $1" >&2 exit 1 } mkdir -p "$TMP/hooks-none" || fail "mkdir hooks-none" # newrepo : git init with no hooks, no signing, one identity. newrepo() { mkdir -p "$1" || fail "mkdir $1" git -C "$1" init -q || fail "git init $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" || fail "git config $1" } # 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" } # ---- 1. the park tool ---- P="$TMP/park" newrepo "$P" mkdir -p "$P/bin" "$P/tests" || fail "mkdir park dirs" printf 'old\n' > "$P/bin/slug.mjs" git -C "$P" add bin/slug.mjs || fail "git add bin/slug.mjs" git -C "$P" commit -q -m "base" || fail "git commit base" START="$(git -C "$P" rev-parse HEAD)" || fail "rev-parse start" PARKED="$P/.git/voyage/parked" # savediff [color]: the "Back to planning" save line of commands/trekexecute.md; # with "color", the same diff coloured (--color=always in place of --no-color). savediff() { color=--no-color [ "${2:-}" = color ] && color=--color=always git -C "$P" -c core.quotepath=off diff --no-ext-diff "$color" --no-renames --no-relative --src-prefix=a/ --dst-prefix=b/ "$START" > "$1" \ || fail "git diff for $1" } # parked_ok : tests/ left the tree and the index and sits in the parked dir. parked_ok() { [ ! -e "$P/tests/$1" ] || fail "park $2: tests/$1 is still in the work tree" if git -C "$P" ls-files --error-unmatch -- "tests/$1" >/dev/null 2>&1; then fail "park $2: tests/$1 is still in the index" fi [ -f "$PARKED/step-1-attempt-$2/tests/$1" ] || fail "park $2: tests/$1 is not under the parked directory" } # 1a. an intent-to-add file printf 'stale | cat\n' > "$P/tests/a.sh" git -C "$P" add --intent-to-add -- tests/a.sh || fail "git add --intent-to-add tests/a.sh" savediff "$TMP/park-1.diff" grep -q '^new file mode' "$TMP/park-1.diff" || fail "park 1: the saved diff names no new file" out="$(node "$PARK" --diff "$TMP/park-1.diff" --step 1 --attempt 1 --cwd "$P")" || fail "park 1 (intent-to-add) exited non-zero: $out" parked_ok a.sh 1 # 1b. a coloured diff printf 'stale\n' > "$P/tests/b.sh" git -C "$P" add --intent-to-add -- tests/b.sh || fail "git add --intent-to-add tests/b.sh" savediff "$TMP/park-2.diff" color ESC="$(printf '\033')" grep -q "$ESC" "$TMP/park-2.diff" || fail "park 2: control - the diff holds no colour code" out="$(node "$PARK" --diff "$TMP/park-2.diff" --step 1 --attempt 2 --cwd "$P")" || fail "park 2 (coloured diff) exited non-zero: $out" parked_ok b.sh 2 # 1c. from a subdirectory printf 'stale\n' > "$P/tests/c.sh" git -C "$P" add --intent-to-add -- tests/c.sh || fail "git add --intent-to-add tests/c.sh" savediff "$TMP/park-3.diff" out="$(node "$PARK" --diff "$TMP/park-3.diff" --step 1 --attempt 3 --cwd "$P/bin")" || fail "park 3 (--cwd bin) exited non-zero: $out" parked_ok c.sh 3 [ "$(cat "$P/bin/slug.mjs")" = "old" ] || fail "park: a committed file was touched" # ---- 2. the verdict gate with --repo ---- V="$TMP/verdict" newrepo "$V" mkdir -p "$TMP/project" || fail "mkdir project" printf 'export const a = 1;\n' > "$V/a.mjs" git -C "$V" add a.mjs || fail "git add a.mjs" git -C "$V" commit -q -m "feat: the reviewed work" || fail "git commit reviewed" REVIEWED="$(git -C "$V" rev-parse HEAD)" || fail "rev-parse reviewed" REVIEW="$TMP/project/review.md" cat > "$REVIEW" <\nEach line: a finding class /trekreview saw in two reviews, approved by the operator. Format: mistake \302\267 fix \302\267 how to check.\n\n' \ > "$V/.claude/rules/voyage-review-lessons.md" git -C "$V" add .claude/rules/voyage-review-lessons.md || fail "git add lessons" git -C "$V" commit -q -m "docs(voyage): review lessons (round 1)" || fail "git commit lessons" out="$(gate)" || fail "verdict 2: a later lessons-only commit did not exit 0: $out" printf 'export const a = 2;\n' > "$V/a.mjs" git -C "$V" add a.mjs || fail "git add a.mjs (later)" git -C "$V" commit -q -m "feat: a later source change" || fail "git commit later" out="$(gate)" code=$? [ "$code" -eq 2 ] || fail "verdict 3: a review of an older commit exited $code, not 2: $out" printf '%s' "$out" | jsonok 'typeof j.reason === "string" && j.reason.indexOf("review-stale") === 0' \ || fail "verdict 3: the reason does not start review-stale: $out" # ---- 3. the arg parser for /trekendsession ---- out="$(node "$ARGS" --command trekendsession -- fix-header)" || fail "arg-parser on fix-header exited non-zero: $out" printf '%s' "$out" | jsonok 'j.flags["--help"] === undefined && j.positional[0] === "fix-header"' \ || fail "arg-parser: fix-header asked for help or is not the first positional: $out" # ---- 4. the gates shim ---- out="$(node "$SHIM" --state idle --event start --gates closed)" || fail "shim --gates closed exited non-zero: $out" printf '%s' "$out" | jsonok 'j.ok === true && j.next_state === "gates_on"' || fail "shim: --gates closed is not gates_on: $out" out="$(node "$SHIM" --state idle --event start --gates)" || fail "shim bare --gates exited non-zero: $out" printf '%s' "$out" | jsonok 'j.ok === true && j.next_state === "gates_on"' || fail "shim: a bare --gates is not gates_on: $out" echo "e2e: ok" exit 0