220 lines
7.8 KiB
Bash
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" > "$out.stdout" 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
|