412 lines
19 KiB
Bash
412 lines
19 KiB
Bash
#!/usr/bin/env bash
|
|
# tests/e2e-fix-round-4-5b2-b.sh
|
|
# End to end for fix round 4.5b2, part B (plan
|
|
# .claude/projects/2026-10-31-review-gate-4-5b2, Step 11, End-to-end:). On
|
|
# throwaway fixture repos it runs the verdict gate the way /trekexecute's
|
|
# merge lock does (node lib/review/verdict-gate.mjs --json <review> --repo <fx>)
|
|
# and the Bash guard the way Claude Code does:
|
|
# 1. an unanswered counting MAJOR keeps the gate at exit 2; after
|
|
# review-answers.mjs record --answer accepted it exits 0;
|
|
# 2. a MAJOR answered "fixed" keeps exit 2 (fixed-pending), and a round-2
|
|
# review (review-rounds.mjs prepare, then review-finalize.mjs) keeps
|
|
# exit 2 while the finding is carried and exits 0 once it is resolved;
|
|
# 3. a pre-existing BLOCKER (a line the range did not change) exits 1;
|
|
# 3b. an engine MAJOR in a payload beside a WARN review that does not hold it
|
|
# keeps exit 2 (engine finding not in review, Step 15) until finalize puts
|
|
# it back, then it is unanswered;
|
|
# 4. merge-evidence.mjs write records a fixture run, and a
|
|
# `git merge --no-ff feat` hook input into hooks/scripts/pre-bash-executor.mjs
|
|
# answers "ask" with the Tested by and Undone by lines.
|
|
# Every fixture keeps HEAD at the reviewed commit when it asks the gate (R6).
|
|
# Prints one "ok - <case>" or "FAIL - <case>: <why>" line per check and
|
|
# "e2e: ok" at the end, exit 0; else exit 1. Starts nothing but node, 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)"
|
|
HOOK="$ROOT/hooks/scripts/pre-bash-executor.mjs"
|
|
GATE="$ROOT/lib/review/verdict-gate.mjs"
|
|
ANSWERS="$ROOT/lib/review/review-answers.mjs"
|
|
ROUNDS="$ROOT/lib/review/review-rounds.mjs"
|
|
FINALIZE="$ROOT/lib/review/review-finalize.mjs"
|
|
FILL="$ROOT/lib/review/fill-finding-ids.mjs"
|
|
WRITER="$ROOT/lib/review/merge-evidence.mjs"
|
|
|
|
unset CLAUDE_CODE_SESSION_ID
|
|
|
|
TMP="$(mktemp -d "${TMPDIR:-/tmp}/e2e-fix-round-4-5b2-b.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)); }
|
|
|
|
TRAILER='Co-Authored-By: Claude Test <noreply@example.invalid>'
|
|
PROJ_REL=.claude/projects/2026-11-02-e2e
|
|
mkdir -p "$TMP/hooks-none" || { echo "FAIL - setup: cannot make the temp directories"; exit 1; }
|
|
|
|
# A review_version 1.2 review with @@ID_<n>@@ placeholders, filled afterwards
|
|
# by fill-finding-ids.mjs as /trekreview Phase 7 fills them.
|
|
# node mkreview.mjs <out> <start> <end> <findings JSON> [rstep]: with rstep,
|
|
# finding 1 gets a "### Step R1" (every BLOCKER gets one anyway).
|
|
cat > "$TMP/mkreview.mjs" <<'EOF'
|
|
import { writeFileSync } from 'node:fs';
|
|
const [out, start, end, findingsJson, rstep] = process.argv.slice(2);
|
|
const list = JSON.parse(findingsJson).map((f, i) => ({
|
|
id: `@@ID_${i + 1}@@`, severity: f.severity, rule_key: f.rule_key || 'MISSING_TEST', file: f.file, line: f.line,
|
|
brief_ref: 'SC1', title: f.title, detail: f.detail || `detail ${i + 1}`, recommended_action: 'Fix it.',
|
|
}));
|
|
const counts = { BLOCKER: 0, MAJOR: 0, MINOR: 0, SUGGESTION: 0 };
|
|
for (const f of list) counts[f.severity] += 1;
|
|
const verdict = counts.BLOCKER > 0 ? 'BLOCK' : counts.MAJOR > 0 ? 'WARN' : 'ALLOW';
|
|
const sections = ['BLOCKER', 'MAJOR', 'MINOR', 'SUGGESTION'].map((sev) => {
|
|
const of = list.filter((f) => f.severity === sev);
|
|
if (of.length === 0) return '';
|
|
return `## Findings (${sev})\n\n${of.map((f) => `### ${f.id}\n\n- file: ${f.file}\n- line: ${f.line}\n- rule_key: ${f.rule_key}\n- title: ${f.title}\n`).join('\n')}\n`;
|
|
}).join('');
|
|
const stepFor = list.filter((f, i) => f.severity === 'BLOCKER' || (rstep === 'rstep' && i === 0));
|
|
const steps = stepFor.map((f, i) => `### Step R${i + 1}: fix ${f.title}\n\n- **Files:** \`${f.file}\`\n- **Source finding:** \`${f.id}\`\n- **Test first:** a test that fails today\n- **Verify:** re-run the review\n- **On failure:** escalate\n`).join('\n');
|
|
const json = JSON.stringify({ verdict, counts, allow_blocked_by: [], findings: list }, null, 2);
|
|
const fmList = list.length ? `\n${list.map((f) => ` - ${f.id}`).join('\n')}` : ' []';
|
|
writeFileSync(out, `---
|
|
type: trekreview
|
|
review_version: "1.2"
|
|
task: "t"
|
|
slug: e2e
|
|
project_dir: .claude/projects/2026-11-02-e2e/
|
|
brief_path: .claude/projects/2026-11-02-e2e/brief.md
|
|
scope_sha_start: ${start}
|
|
scope_sha_end: ${end}
|
|
reviewed_files_count: 2
|
|
verdict: ${verdict}
|
|
findings:${fmList}
|
|
---
|
|
|
|
# Review: t
|
|
|
|
## Executive Summary
|
|
|
|
x
|
|
|
|
## Coverage
|
|
|
|
| File | Treatment | Reason |
|
|
|------|-----------|--------|
|
|
| a.mjs | deep-review | default |
|
|
|
|
${sections}## Declined to Judge
|
|
|
|
None.
|
|
|
|
Trimmed: 0
|
|
|
|
## Remediation Steps
|
|
|
|
${steps || 'None.\n'}
|
|
## Remediation Summary
|
|
|
|
x
|
|
|
|
\`\`\`json
|
|
${json}
|
|
\`\`\`
|
|
`);
|
|
EOF
|
|
|
|
# jfield <file> <expr>: prints the JS expression <expr> over the parsed JSON
|
|
# value `j` read from <file> (empty when it does not parse).
|
|
jfield() {
|
|
node -e '
|
|
const fs = require("fs");
|
|
let j;
|
|
try { j = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); } catch (e) { process.exit(0); }
|
|
const v = Function("j", "return (" + process.argv[2] + ");")(j);
|
|
process.stdout.write(v === undefined || v === null ? "" : String(v));
|
|
' "$1" "$2"
|
|
}
|
|
|
|
# reviewjson <review> <expr>: <expr> over the review's trailing JSON block `j`.
|
|
reviewjson() {
|
|
node -e '
|
|
const fs = require("fs");
|
|
const t = fs.readFileSync(process.argv[1], "utf8");
|
|
const j = JSON.parse(t.slice(t.lastIndexOf("```json\n") + 8, t.lastIndexOf("```")));
|
|
const v = Function("j", "return (" + process.argv[2] + ");")(j);
|
|
process.stdout.write(v === undefined || v === null ? "" : String(v));
|
|
' "$1" "$2"
|
|
}
|
|
|
|
# ten <tag> [<line> <text>]: ten lines `export const <tag><n> = <n>;`, line <line> replaced.
|
|
ten() {
|
|
tag="$1"
|
|
at="${2:-0}"
|
|
txt="${3:-}"
|
|
n=1
|
|
while [ "$n" -le 10 ]; do
|
|
if [ "$n" -eq "$at" ]; then echo "$txt"; else echo "export const $tag$n = $n;"; fi
|
|
n=$((n + 1))
|
|
done
|
|
}
|
|
|
|
# mkrepo <dir>: a git repo on main with start (a.mjs, b.mjs) and c1 (line 3 of
|
|
# both changed); the project folder excluded from git. Sets START and C1.
|
|
mkrepo() {
|
|
r="$1"
|
|
mkdir -p "$r" || return 1
|
|
git -C "$r" init -q -b main || 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" >> "$r/.git/config"
|
|
printf '.claude/projects/\n' >> "$r/.git/info/exclude"
|
|
ten a > "$r/a.mjs"
|
|
ten b > "$r/b.mjs"
|
|
git -C "$r" add a.mjs b.mjs && git -C "$r" commit -q -m 'chore: start' || return 1
|
|
START="$(git -C "$r" rev-parse HEAD)"
|
|
git -C "$r" checkout -q -b feat || return 1
|
|
ten a 3 'export const a3 = 33;' > "$r/a.mjs"
|
|
ten b 3 'export const b3 = 33;' > "$r/b.mjs"
|
|
git -C "$r" add a.mjs b.mjs && git -C "$r" commit -q -m 'feat: round 1' || return 1
|
|
C1="$(git -C "$r" rev-parse HEAD)"
|
|
mkdir -p "$r/$PROJ_REL" || return 1
|
|
}
|
|
|
|
# gate <review> <repo> <out>: the merge lock's gate line; prints its exit status.
|
|
gate() {
|
|
node "$GATE" --json "$1" --repo "$2" > "$3" 2>&1
|
|
echo $?
|
|
}
|
|
|
|
# reviewer <project> <previous JSON>: the code-correctness reviewer's saved answer.
|
|
reviewer() {
|
|
printf 'Reviewer prose.\n\n```json\n{"reviewer": "code-correctness-reviewer", "findings": [], "previous": %s}\n```\n' "$2" \
|
|
> "$1/.review-out-code-correctness-reviewer.md"
|
|
}
|
|
|
|
MAJ='[{"severity":"MAJOR","file":"a.mjs","line":3,"title":"Parser drops the last token","detail":"parse() stops one token early"}]'
|
|
|
|
case_accepted() {
|
|
name="unanswered MAJOR, then accepted"
|
|
R="$TMP/accepted"
|
|
mkrepo "$R" || { bad "$name: setup"; return; }
|
|
P="$R/$PROJ_REL"
|
|
node "$TMP/mkreview.mjs" "$P/review.md" "$START" "$C1" "$MAJ" && node "$FILL" "$P/review.md" > /dev/null \
|
|
|| { bad "$name: could not write the review"; return; }
|
|
reviewer "$P" '[]'
|
|
node "$FINALIZE" --review "$P/review.md" --repo "$R" --project "$P" --round 1 > "$TMP/acc-fin.out" 2>&1 \
|
|
|| { bad "$name: finalize: $(cat "$TMP/acc-fin.out")"; return; }
|
|
c="$(gate "$P/review.md" "$R" "$TMP/acc-g1.out")"
|
|
if [ "$c" -eq 2 ] && grep -q '"reason":"review-unanswered:' "$TMP/acc-g1.out"; then
|
|
pass "$name: an unanswered MAJOR exits 2"
|
|
else
|
|
bad "$name: the unanswered MAJOR exited $c: $(cat "$TMP/acc-g1.out")"
|
|
fi
|
|
id="$(reviewjson "$P/review.md" 'j.findings[0].id')"
|
|
printf 'the parser keeps the old contract on purpose' | node "$ANSWERS" record --review "$P/review.md" --finding "$id" \
|
|
--answer accepted --repo "$R" --round 1 --store local --trailer "$TRAILER" > "$TMP/acc-rec.out" 2>&1
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then bad "$name: record exited $rc: $(cat "$TMP/acc-rec.out")"; return; fi
|
|
c="$(gate "$P/review.md" "$R" "$TMP/acc-g2.out")"
|
|
if [ "$c" -eq 0 ]; then
|
|
pass "$name: accepted, the gate exits 0"
|
|
else
|
|
bad "$name: after the accepted answer the gate exited $c: $(cat "$TMP/acc-g2.out")"
|
|
fi
|
|
}
|
|
|
|
case_fixed() {
|
|
name="MAJOR answered fixed"
|
|
R="$TMP/fixed"
|
|
mkrepo "$R" || { bad "$name: setup"; return; }
|
|
P="$R/$PROJ_REL"
|
|
node "$TMP/mkreview.mjs" "$P/review.md" "$START" "$C1" "$MAJ" rstep && node "$FILL" "$P/review.md" > /dev/null \
|
|
|| { bad "$name: could not write the review"; return; }
|
|
reviewer "$P" '[]'
|
|
node "$FINALIZE" --review "$P/review.md" --repo "$R" --project "$P" --round 1 > "$TMP/fix-fin1.out" 2>&1 \
|
|
|| { bad "$name: round-1 finalize: $(cat "$TMP/fix-fin1.out")"; return; }
|
|
id="$(reviewjson "$P/review.md" 'j.findings[0].id')"
|
|
printf 'the parser gets the missing token back' | node "$ANSWERS" record --review "$P/review.md" --finding "$id" \
|
|
--answer fixed --repo "$R" --round 1 --store local --trailer "$TRAILER" > "$TMP/fix-rec.out" 2>&1
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then bad "$name: record exited $rc: $(cat "$TMP/fix-rec.out")"; return; fi
|
|
c="$(gate "$P/review.md" "$R" "$TMP/fix-g1.out")"
|
|
if [ "$c" -eq 2 ] && grep -q '"reason":"review-unanswered: fixed-pending 1' "$TMP/fix-g1.out"; then
|
|
pass "$name: round 1 exits 2 (fixed-pending)"
|
|
else
|
|
bad "$name: round 1 exited $c: $(cat "$TMP/fix-g1.out")"
|
|
fi
|
|
# The fix: round 2's work, committed (only a.mjs; the local approval log stays untracked).
|
|
ten a 3 'export const a3 = 333;' > "$R/a.mjs"
|
|
git -C "$R" add a.mjs && git -C "$R" commit -q -m 'fix: the parser' || { bad "$name: commit"; return; }
|
|
C2="$(git -C "$R" rev-parse HEAD)"
|
|
(cd "$R" && node "$ROUNDS" prepare --project "$P" --since "$START" --json) > "$TMP/fix-prep.out" 2>&1 \
|
|
|| { bad "$name: prepare: $(cat "$TMP/fix-prep.out")"; return; }
|
|
round="$(jfield "$TMP/fix-prep.out" 'j.round')"
|
|
base="$(jfield "$TMP/fix-prep.out" 'j.round_base')"
|
|
prev="$(jfield "$TMP/fix-prep.out" 'j.previous')"
|
|
if [ "$round" != 2 ] || [ "$base" != "$C1" ] || [ -z "$prev" ]; then
|
|
bad "$name: prepare gave round $round base $base previous $prev"
|
|
return
|
|
fi
|
|
# Round 2, the reviewer not (yet) marking the finding resolved: carried, still open.
|
|
node "$TMP/mkreview.mjs" "$P/review.md" "$START" "$C2" '[]' && node "$FILL" "$P/review.md" > /dev/null \
|
|
|| { bad "$name: could not write the round-2 review"; return; }
|
|
reviewer "$P" "[{\"id\": \"$id\", \"resolved\": false}]"
|
|
node "$FINALIZE" --review "$P/review.md" --repo "$R" --project "$P" --round 2 --round-base "$base" --previous "$P/$prev" \
|
|
> "$TMP/fix-fin2.out" 2>&1 || { bad "$name: round-2 finalize: $(cat "$TMP/fix-fin2.out")"; return; }
|
|
carried="$(reviewjson "$P/review.md" 'j.findings.map((f) => f.carried_from).join(",")')"
|
|
c="$(gate "$P/review.md" "$R" "$TMP/fix-g2.out")"
|
|
if [ "$carried" = "$id" ] && [ "$c" -eq 2 ]; then
|
|
pass "$name: round 2 carries the finding and keeps exit 2"
|
|
else
|
|
bad "$name: round 2 not resolved: carried [$carried], gate exit $c: $(cat "$TMP/fix-g2.out")"
|
|
fi
|
|
# Round 2 again, the reviewer marking it resolved.
|
|
node "$TMP/mkreview.mjs" "$P/review.md" "$START" "$C2" '[]' && node "$FILL" "$P/review.md" > /dev/null \
|
|
|| { bad "$name: could not rewrite the round-2 review"; return; }
|
|
reviewer "$P" "[{\"id\": \"$id\", \"resolved\": true}]"
|
|
node "$FINALIZE" --review "$P/review.md" --repo "$R" --project "$P" --round 2 --round-base "$base" --previous "$P/$prev" \
|
|
> "$TMP/fix-fin3.out" 2>&1 || { bad "$name: round-2 finalize: $(cat "$TMP/fix-fin3.out")"; return; }
|
|
resolved="$(reviewjson "$P/review.md" '(j.resolved || []).join(",")')"
|
|
c="$(gate "$P/review.md" "$R" "$TMP/fix-g3.out")"
|
|
if [ "$resolved" = "$id" ] && [ "$c" -eq 0 ]; then
|
|
pass "$name: round 2 marks it resolved, the gate exits 0"
|
|
else
|
|
bad "$name: resolved [$resolved], gate exit $c: $(cat "$TMP/fix-g3.out")"
|
|
fi
|
|
}
|
|
|
|
case_preexisting() {
|
|
name="pre-existing BLOCKER"
|
|
R="$TMP/pre"
|
|
mkrepo "$R" || { bad "$name: setup"; return; }
|
|
P="$R/$PROJ_REL"
|
|
# b.mjs line 9: a line start..c1 did not change.
|
|
node "$TMP/mkreview.mjs" "$P/review.md" "$START" "$C1" \
|
|
'[{"severity":"BLOCKER","file":"b.mjs","line":9,"rule_key":"BROKEN_SUCCESS_CRITERION","title":"Build output is never written","detail":"write() returns before the flush"}]' \
|
|
&& node "$FILL" "$P/review.md" > /dev/null || { bad "$name: could not write the review"; return; }
|
|
reviewer "$P" '[]'
|
|
node "$FINALIZE" --review "$P/review.md" --repo "$R" --project "$P" --round 1 > "$TMP/pre-fin.out" 2>&1 \
|
|
|| { bad "$name: finalize: $(cat "$TMP/pre-fin.out")"; return; }
|
|
mark="$(reviewjson "$P/review.md" 'j.findings[0].pre_existing')"
|
|
c="$(gate "$P/review.md" "$R" "$TMP/pre-g.out")"
|
|
if [ "$mark" = true ] && [ "$c" -eq 1 ]; then
|
|
pass "$name: marked pre-existing, the gate exits 1"
|
|
else
|
|
bad "$name: pre_existing [$mark], gate exit $c: $(cat "$TMP/pre-g.out")"
|
|
fi
|
|
}
|
|
|
|
case_dropped_engine() {
|
|
name="dropped engine finding"
|
|
R="$TMP/dropped"
|
|
mkrepo "$R" || { bad "$name: setup"; return; }
|
|
P="$R/$PROJ_REL"
|
|
# A WARN review: its one MAJOR is answered accepted, so the gate passes it alone.
|
|
node "$TMP/mkreview.mjs" "$P/review.md" "$START" "$C1" "$MAJ" && node "$FILL" "$P/review.md" > /dev/null \
|
|
|| { bad "$name: could not write the review"; return; }
|
|
id="$(reviewjson "$P/review.md" 'j.findings[0].id')"
|
|
printf 'the parser keeps the old contract on purpose' | node "$ANSWERS" record --review "$P/review.md" --finding "$id" \
|
|
--answer accepted --repo "$R" --round 1 --store local --trailer "$TRAILER" > "$TMP/drop-rec.out" 2>&1
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then bad "$name: record exited $rc: $(cat "$TMP/drop-rec.out")"; return; fi
|
|
c="$(gate "$P/review.md" "$R" "$TMP/drop-g0.out")"
|
|
if [ "$c" -ne 0 ]; then bad "$name: the answered WARN alone exited $c: $(cat "$TMP/drop-g0.out")"; return; fi
|
|
# Phase 4.7's payload: a /code-review MAJOR on b.mjs line 3 that the review does not hold; finalize not run.
|
|
printf 'a.mjs\nb.mjs\n' > "$TMP/drop-scope.txt"
|
|
printf '{"engine": "code-review", "findings": [{"file": "b.mjs", "line": 3, "summary": "The counter overflows past 2^31", "failure_scenario": "a long run wraps the count"}]}\n' \
|
|
> "$TMP/drop-engine.json"
|
|
node "$ROOT/lib/review/engine-findings.mjs" --scope "$TMP/drop-scope.txt" "$TMP/drop-engine.json" \
|
|
> "$P/.review-engine-payload-code-review.json" 2> "$TMP/drop-eng.err" \
|
|
|| { bad "$name: engine-findings: $(cat "$TMP/drop-eng.err")"; return; }
|
|
c="$(gate "$P/review.md" "$R" "$TMP/drop-g1.out")"
|
|
if [ "$c" -eq 2 ] && grep -q 'engine finding not in review' "$TMP/drop-g1.out"; then
|
|
pass "$name: not finalized, the gate exits 2 (engine finding not in review)"
|
|
else
|
|
bad "$name: the dropped engine MAJOR exited $c: $(cat "$TMP/drop-g1.out")"
|
|
fi
|
|
reviewer "$P" '[]'
|
|
node "$FINALIZE" --review "$P/review.md" --repo "$R" --project "$P" --round 1 > "$TMP/drop-fin.out" 2>&1 \
|
|
|| { bad "$name: finalize: $(cat "$TMP/drop-fin.out")"; return; }
|
|
c="$(gate "$P/review.md" "$R" "$TMP/drop-g2.out")"
|
|
if [ "$c" -eq 2 ] && grep -q '"reason":"review-unanswered:' "$TMP/drop-g2.out"; then
|
|
pass "$name: finalized, the put-back MAJOR is unanswered"
|
|
else
|
|
bad "$name: after finalize the gate exited $c: $(cat "$TMP/drop-g2.out")"
|
|
fi
|
|
}
|
|
|
|
case_guard() {
|
|
name="merge question"
|
|
R="$TMP/merge"
|
|
mkdir -p "$R" || { bad "$name: setup"; return; }
|
|
git -C "$R" init -q -b main || { bad "$name: git init"; return; }
|
|
printf '[user]\n\tname = Test\n\temail = test@example.invalid\n[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "%s"\n' "$TMP/hooks-none" >> "$R/.git/config"
|
|
ten a > "$R/a.mjs"
|
|
git -C "$R" add a.mjs && git -C "$R" commit -q -m 'chore: start' || { bad "$name: commit"; return; }
|
|
MAIN_SHA="$(git -C "$R" rev-parse HEAD)"
|
|
git -C "$R" checkout -q -b feat || { bad "$name: branch"; return; }
|
|
ten a 3 'export const a3 = 33;' > "$R/a.mjs"
|
|
git -C "$R" add a.mjs && git -C "$R" commit -q -m 'feat: the work' || { bad "$name: commit"; return; }
|
|
FEAT="$(git -C "$R" rev-parse HEAD)"
|
|
# The run's artifacts, outside the repo.
|
|
P="$TMP/merge-project"
|
|
mkdir -p "$P" || { bad "$name: setup"; return; }
|
|
printf '# Plan\n\n### Step 1: End to end\n\n- **End-to-end:** `/bin/bash tests/e2e-demo.sh` -> expected: `e2e: ok`, exit 0.\n\n## Doors\n\n- two-way: a revert restores the old parser\n' > "$P/plan.md"
|
|
printf '{"schema_version": "1", "status": "completed", "end_to_end": "verified", "plan_verification": {"status": "passed", "summary": {"total": 3, "passed": 3, "failed": 0, "blocked": 0, "unrunnable": 0}}}\n' > "$P/progress.json"
|
|
node "$TMP/mkreview.mjs" "$P/review.md" "$MAIN_SHA" "$FEAT" '[]' && node "$FILL" "$P/review.md" > /dev/null \
|
|
|| { bad "$name: could not write the review"; return; }
|
|
# HEAD at the reviewed commit while the writer asks the verdict gate.
|
|
node "$WRITER" write --repo "$R" --branch feat --project "$P" --json > "$TMP/ev.out" 2>&1
|
|
rc=$?
|
|
written="$(jfield "$TMP/ev.out" 'j.written')"
|
|
path="$(jfield "$TMP/ev.out" 'j.path')"
|
|
if [ "$rc" -eq 0 ] && [ "$written" = true ] && [ "$path" = "$R/.git/voyage/merge-evidence/$FEAT.json" ] && [ -f "$path" ]; then
|
|
pass "$name: the evidence file is written under the git common dir"
|
|
else
|
|
bad "$name: the writer exited $rc: $(cat "$TMP/ev.out")"
|
|
fi
|
|
if [ -z "$(git -C "$R" status --porcelain --untracked-files=all)" ]; then
|
|
pass "$name: git status shows nothing new"
|
|
else
|
|
bad "$name: git status: $(git -C "$R" status --porcelain --untracked-files=all)"
|
|
fi
|
|
git -C "$R" checkout -q main || { bad "$name: checkout main"; return; }
|
|
node -e 'process.stdout.write(JSON.stringify({ hook_event_name: "PreToolUse", tool_name: "Bash", cwd: process.argv[1], tool_input: { command: "git merge --no-ff feat" } }));' "$R" \
|
|
| node "$HOOK" > "$TMP/hook.out" 2> "$TMP/hook.err"
|
|
rc=$?
|
|
decision="$(jfield "$TMP/hook.out" 'j.hookSpecificOutput.permissionDecision')"
|
|
reason="$(jfield "$TMP/hook.out" 'j.hookSpecificOutput.permissionDecisionReason')"
|
|
sha8="$(printf '%s' "$FEAT" | cut -c1-8)"
|
|
tested="Tested by: End-to-end \`/bin/bash tests/e2e-demo.sh\` exit 0; Verification 3/3; review ALLOW at $sha8."
|
|
undone='Undone by: `git revert -m 1 <merge>` (two-way).'
|
|
if [ "$rc" -eq 0 ] && [ "$decision" = ask ]; then
|
|
pass "$name: the guard asks"
|
|
else
|
|
bad "$name: the guard exited $rc with decision [$decision]: $(cat "$TMP/hook.out" "$TMP/hook.err")"
|
|
fi
|
|
case "$reason" in
|
|
*"$tested"*) pass "$name: the Tested by line" ;;
|
|
*) bad "$name: no Tested by line [$tested] in: $reason" ;;
|
|
esac
|
|
case "$reason" in
|
|
*"$undone"*) pass "$name: the Undone by line" ;;
|
|
*) bad "$name: no Undone by line [$undone] in: $reason" ;;
|
|
esac
|
|
}
|
|
|
|
case_accepted
|
|
case_fixed
|
|
case_preexisting
|
|
case_dropped_engine
|
|
case_guard
|
|
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
echo "e2e: $FAILED case(s) failed"
|
|
exit 1
|
|
fi
|
|
echo "e2e: ok"
|
|
exit 0
|