voyage/tests/e2e-fix-round-4-5b-part-2.sh
Kjell Tore Guttormsen c75ae2d3f5
refactor: simplify pass (trekexecute)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 16:34:32 +01:00

220 lines
7.8 KiB
Bash

#!/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 - <case>" or "FAIL - <case>: <why>" 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 <dir>: 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 <js expression on j>: 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 <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"
}
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 <file> <slug> <sha> <id> <n>: a review.md with one MISSING_TEST finding.
lessonreview() {
mkdir -p "$(dirname "$1")" || return 1
fid="$(printf '%s0000000000000000000000000000000000000000' "$4" | cut -c1-40)"
cat > "$1" <<EOF
---
type: trekreview
review_version: "1.1"
task: "$2"
slug: $2
project_dir: .claude/projects/2026-10-0$5-$2/
brief_path: .claude/projects/x/brief.md
scope_sha_end: $3
reviewed_files_count: 1
verdict: WARN
findings:
- $fid
---
# Review: $2
## Executive Summary
x
## Coverage
x
## Remediation Summary
x
\`\`\`json
{
"verdict": "WARN",
"counts": { "BLOCKER": 0, "MAJOR": 1, "MINOR": 0, "SUGGESTION": 0 },
"findings": [
{ "id": "$fid", "severity": "MAJOR", "rule_key": "MISSING_TEST", "file": "lib/a.mjs", "line": 3,
"title": "MISSING_TEST title $4", "detail": "d", "recommended_action": "Fix it ($4)." }
]
}
\`\`\`
EOF
}
case_cki_symlink() {
name="applyLesson: a .claude symlinked inside the repo gets no pointer (check-failed)"
R="$TMP/lesson-link"
newrepo "$R" || { bad "$name: fixture repo"; return; }
lessonreview "$R/claude-data/projects/2026-10-01-one/review.md" one aaa a1 1 || { bad "$name: review one"; return; }
lessonreview "$R/claude-data/projects/2026-10-02-two/review.md" two bbb b1 2 || { bad "$name: review two"; return; }
( cd "$R" && ln -s claude-data .claude ) || { bad "$name: ln -s"; return; }
printf '# Project\n' > "$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