#!/usr/bin/env bash # tests/e2e-files-trial4.sh # End to end for the trial-4 plan (steps 11 and 13): the four CLIs run the # way a command runs them. # 1. lib/validators/plan-validator.mjs --strict --json on three plans built # with tests/helpers/plan-v18-builder.mjs: a valid 1.9 plan (exit 0), a # 1.9 plan with an unticked Files entry (exit 1, PLAN_STEP_PATH_NOT_FIRST), # a 1.8 plan with an unticked path (exit 0, PLAN_STEP_PATH_UNTICKED). # 2. lib/util/artifact-commit.mjs in a temp git repo: with a trailer (exit 0, # the trailer is the message's last line), without (exit 1, HEAD unchanged). # 3. lib/review/verdict-gate.mjs on the legal (exit 0) and the illegal # (exit 2) brief-conflict review fixtures. # 4. plan-validator on a 1.8 plan whose Test first File line names a source # path in its description (exit 0, no PLAN_STEP_TESTS_ONLY: decision 20). # 5. plan-validator on a 1.9 plan whose Files is a backticked None (exit 1, # PLAN_STEP_PATH_NOT_A_PATH). # 6. lib/verification/red-proof.mjs with --plan/--step and a --tests that # differs from the plan's list (exit 2, tests-mismatch). # 7. artifact-commit with an empty-model trailer (exit 1, HEAD unchanged). # 8. plan-validator on a 1.9 plan whose Files entry wraps onto a line that # opens with a backticked flag (exit 0, no PLAN_STEP_PATH_NOT_A_PATH). # 9. plan-validator on a 1.9 plan whose Test first File line wraps onto a # line that opens with the source path (exit 0, no PLAN_STEP_TESTS_ONLY). # 10. red-proof --red-only with --plan/--step and no --tests in a second temp # repo (exit 0; its JSON tests field is the plan's one test file). # 11. the same with a plan path that does not exist (exit 2, plan-unreadable). # 12. the same as 10 in text form with a --tests that differs (exit 2, the # line " plan_tests: tests/parser.test.mjs"). # 13. red-proof --red-only on a 1.8 plan whose File line names two tests, # each with a (new) note (exit 0; tests is both, in order; decision 31). # 14. red-proof --red-only on a 1.7 plan whose File line names its test # without backticks: no --tests (exit 2, no test files given, no tests # line), then --tests with the file the helper wrote (exit 0, the line # " tests: tests/parser.test.mjs"; decision 41). # 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)" unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE TMP="$(mktemp -d "${TMPDIR:-/tmp}/e2e-files-trial4.XXXXXX")" || { echo "e2e: FAIL - mktemp" >&2; exit 1; } trap 'rm -rf "$TMP"' EXIT fail() { echo "e2e: FAIL - $1" >&2 exit 1 } TRAILER='Co-Authored-By: Claude Opus 5.5 ' # ---- 1. the plan validator on three plans ---- node --input-type=module -e ' import { writeFileSync } from "node:fs"; import { buildPlan } from "./tests/helpers/plan-v18-builder.mjs"; const dir = process.argv[1]; const e2e = { endToEnd: "bash tests/e2e.sh" }; writeFileSync(dir + "/plan-1-9-valid.md", buildPlan({ version: "1.9" })); writeFileSync(dir + "/plan-1-9-unticked.md", buildPlan({ version: "1.9", steps: [{ filesRaw: "src/mod1.mjs - the parser" }, e2e] })); writeFileSync(dir + "/plan-1-8-unticked.md", buildPlan({ steps: [{ filesRaw: "`tests/mod1.test.mjs`, src/mod1.mjs" }, e2e] })); ' "$TMP" || fail "could not write the three plans with tests/helpers/plan-v18-builder.mjs" out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-9-valid.md" 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail "valid 1.9 plan: plan-validator exit $rc, expected 0: $out" out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-9-unticked.md" 2>&1)" rc=$? [ "$rc" -eq 1 ] || fail "1.9 plan with an unticked Files entry: plan-validator exit $rc, expected 1: $out" echo "$out" | grep -q 'PLAN_STEP_PATH_NOT_FIRST' \ || fail "1.9 plan with an unticked Files entry: no PLAN_STEP_PATH_NOT_FIRST in: $out" out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-8-unticked.md" 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail "1.8 plan with an unticked path: plan-validator exit $rc, expected 0: $out" echo "$out" | grep -q 'PLAN_STEP_PATH_UNTICKED' \ || fail "1.8 plan with an unticked path: no PLAN_STEP_PATH_UNTICKED in: $out" # ---- 2. the approval commit in a temp git repo ---- # make_repo : a temp git repo with one commit, no hooks, no signing. make_repo() { mkdir -p "$1" "$TMP/hooks-none" || fail "mkdir $1" git -C "$1" init -q || fail "git init $1" git -C "$1" config user.name 'E2E Test' git -C "$1" config user.email 'test@example.invalid' git -C "$1" config commit.gpgsign false git -C "$1" config core.hooksPath "$TMP/hooks-none" echo 'x' > "$1/README.md" git -C "$1" add README.md && git -C "$1" commit -q -m 'init' || fail "initial commit in $1" } REPO="$TMP/repo" make_repo "$REPO" echo '# brief' > "$REPO/brief.md" H1='cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' H2='dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd' (cd "$REPO" && node "$ROOT/lib/util/approval-log.mjs" append --kind intent --subject brief.md --sha256 "$H1" >/dev/null) \ || fail "approval-log append (first record)" [ -f "$REPO/.claude/voyage/approvals.jsonl" ] || fail "no .claude/voyage/approvals.jsonl in the temp repo" out="$(node lib/util/artifact-commit.mjs --repo "$REPO" --store local --message 'docs(voyage): approve intent e2e' --trailer "$TRAILER" 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail "artifact-commit with a trailer: exit $rc, expected 0: $out" last="$(git -C "$REPO" log -1 --format=%B | sed '/^$/d' | tail -n 1)" [ "$last" = "$TRAILER" ] || fail "artifact-commit with a trailer: the last message line is '$last', expected '$TRAILER'" (cd "$REPO" && node "$ROOT/lib/util/approval-log.mjs" append --kind intent --subject brief.md --sha256 "$H2" >/dev/null) \ || fail "approval-log append (second record)" before="$(git -C "$REPO" rev-parse HEAD)" out="$(node lib/util/artifact-commit.mjs --repo "$REPO" --store local --message 'docs(voyage): approve intent e2e' 2>&1)" rc=$? [ "$rc" -eq 1 ] || fail "artifact-commit without a trailer: exit $rc, expected 1: $out" after="$(git -C "$REPO" rev-parse HEAD)" [ "$before" = "$after" ] || fail "artifact-commit without a trailer: HEAD moved from $before to $after" # ---- 3. the verdict gate on the two brief-conflict fixtures ---- out="$(node lib/review/verdict-gate.mjs tests/fixtures/review-brief-conflict-legal.md 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail "verdict-gate on review-brief-conflict-legal.md: exit $rc, expected 0: $out" out="$(node lib/review/verdict-gate.mjs tests/fixtures/review-brief-conflict-illegal.md 2>&1)" rc=$? [ "$rc" -eq 2 ] || fail "verdict-gate on review-brief-conflict-illegal.md: exit $rc, expected 2: $out" # ---- 4. and 5. the plan validator on decision 20 and on a placeholder ---- node --input-type=module -e ' import { writeFileSync } from "node:fs"; import { buildPlan } from "./tests/helpers/plan-v18-builder.mjs"; const dir = process.argv[1]; const e2e = { endToEnd: "bash tests/e2e.sh" }; writeFileSync(dir + "/plan-1-8-description.md", buildPlan({ steps: [{ file: "src/parser.mjs", test: "tests/parser.test.mjs", filesRaw: "`src/parser.mjs`, `tests/parser.test.mjs`", testFirstRaw: " - File: `tests/parser.test.mjs` (new) - covers `parse()` in `src/parser.mjs`", }, e2e] })); writeFileSync(dir + "/plan-1-9-none.md", buildPlan({ version: "1.9", steps: [{ filesRaw: "`None`" }, e2e] })); ' "$TMP" || fail "could not write the plans for checks 4 and 5" out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-8-description.md" 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail "1.8 plan with a source path in the File line description: plan-validator exit $rc, expected 0: $out" if echo "$out" | grep -q 'PLAN_STEP_TESTS_ONLY'; then fail "1.8 plan with a source path in the File line description: PLAN_STEP_TESTS_ONLY in: $out" fi out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/plan-1-9-none.md" 2>&1)" rc=$? [ "$rc" -eq 1 ] || fail "1.9 plan whose Files is a backticked None: plan-validator exit $rc, expected 1: $out" echo "$out" | grep -q 'PLAN_STEP_PATH_NOT_A_PATH' \ || fail "1.9 plan whose Files is a backticked None: no PLAN_STEP_PATH_NOT_A_PATH in: $out" # ---- 6. red proof refuses a --tests that differs from the plan ---- out="$(node lib/verification/red-proof.mjs --red-only --base HEAD --cwd "$REPO" --cmd 'node --test tests/parser.test.mjs' --tests tests/other.test.mjs --plan "$TMP/plan-1-8-description.md" --step 1 --json 2>&1)" rc=$? [ "$rc" -eq 2 ] || fail "red-proof with a --tests that differs from the plan: exit $rc, expected 2: $out" echo "$out" | grep -q 'tests-mismatch' \ || fail "red-proof with a --tests that differs from the plan: no tests-mismatch in: $out" # ---- 7. the approval commit refuses an empty-model trailer ---- before="$(git -C "$REPO" rev-parse HEAD)" out="$(node lib/util/artifact-commit.mjs --repo "$REPO" --store local --message 'docs(voyage): approve intent e2e' --trailer 'Co-Authored-By: Claude ' 2>&1)" rc=$? [ "$rc" -eq 1 ] || fail "artifact-commit with an empty-model trailer: exit $rc, expected 1: $out" after="$(git -C "$REPO" rev-parse HEAD)" [ "$before" = "$after" ] || fail "artifact-commit with an empty-model trailer: HEAD moved from $before to $after" # ---- 8. to 13. decisions 29 to 31: wrapped 1.9 lines, red proof without --tests ---- node --input-type=module -e ' import { writeFileSync } from "node:fs"; import { buildPlan } from "./tests/helpers/plan-v18-builder.mjs"; const dir = process.argv[1]; const e2e = { endToEnd: "bash tests/e2e.sh" }; writeFileSync(dir + "/p8.md", buildPlan({ version: "1.9", steps: [{ filesRaw: "`src/a.mjs`\n `--strict` flag added", }, e2e] })); writeFileSync(dir + "/p9.md", buildPlan({ version: "1.9", steps: [{ file: "src/parser.mjs", test: "tests/parser.test.mjs", filesRaw: "`src/parser.mjs`, `tests/parser.test.mjs`", testFirstRaw: " - File: `tests/parser.test.mjs`\n `src/parser.mjs` is the module it drives", }, e2e] })); writeFileSync(dir + "/p13.md", buildPlan({ steps: [{ file: "src/parser.mjs", filesRaw: "`src/parser.mjs`, `tests/a.test.mjs`, `tests/b.test.mjs`", testFirstRaw: " - File: `tests/a.test.mjs` (new), `tests/b.test.mjs` (new)", }, e2e] })); writeFileSync(dir + "/p14.md", buildPlan({ version: "1.7", steps: [{ file: "src/parser.mjs", filesRaw: "`src/parser.mjs`, `tests/parser.test.mjs`", testFirstRaw: " - File: tests/parser.test.mjs (new)", }, e2e] })); ' "$TMP" || fail "could not write the plans for checks 8 to 14" out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/p8.md" 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail "8: 1.9 plan with a wrapped Files entry: plan-validator exit $rc, expected 0: $out" if echo "$out" | grep -q 'PLAN_STEP_PATH_NOT_A_PATH'; then fail "8: 1.9 plan with a wrapped Files entry: PLAN_STEP_PATH_NOT_A_PATH in: $out" fi out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/p9.md" 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail "9: 1.9 plan with a wrapped Test first File line: plan-validator exit $rc, expected 0: $out" if echo "$out" | grep -q 'PLAN_STEP_TESTS_ONLY'; then fail "9: 1.9 plan with a wrapped Test first File line: PLAN_STEP_TESTS_ONLY in: $out" fi R="$TMP/repo-red" make_repo "$R" mkdir -p "$R/src" "$R/tests" || fail "mkdir $R" echo 'export const parse = (s) => Number(s);' > "$R/src/parser.mjs" for t in parser a b; do cat > "$R/tests/$t.test.mjs" <<'EOT' import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { parse } from '../src/parser.mjs'; test('parse reads a number', () => { assert.equal(parse('2'), 2); }); EOT done # tests_equal : exit 0 when the JSON's tests deep-equals it. tests_equal() { node -e ' const assert = require("node:assert"); const fs = require("node:fs"); const r = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); try { assert.deepStrictEqual(r.tests, JSON.parse(process.argv[2])); } catch (e) { process.exit(1); } ' "$1" "$2" } node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/p9.md" --step 1 --json >"$TMP/rp10.json" 2>"$TMP/rp10.err" rc=$? [ "$rc" -eq 0 ] || fail "10: red-proof with --plan/--step and no --tests: exit $rc, expected 0: $(cat "$TMP/rp10.json" "$TMP/rp10.err")" tests_equal "$TMP/rp10.json" '["tests/parser.test.mjs"]' \ || fail "10: red-proof tests is not [\"tests/parser.test.mjs\"]: $(cat "$TMP/rp10.json")" out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/nope.md" --step 1 --json 2>&1)" rc=$? [ "$rc" -eq 2 ] || fail "11: red-proof with a plan that does not exist: exit $rc, expected 2: $out" echo "$out" | grep -q 'plan-unreadable' \ || fail "11: red-proof with a plan that does not exist: no plan-unreadable in: $out" out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --tests tests/a.test.mjs --plan "$TMP/p9.md" --step 1 2>&1)" rc=$? [ "$rc" -eq 2 ] || fail "12: red-proof with a --tests that differs (text): exit $rc, expected 2: $out" echo "$out" | grep -Fqx ' plan_tests: tests/parser.test.mjs' \ || fail "12: red-proof with a --tests that differs (text): no line ' plan_tests: tests/parser.test.mjs' in: $out" node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/a.test.mjs tests/b.test.mjs' --plan "$TMP/p13.md" --step 1 --json >"$TMP/rp13.json" 2>"$TMP/rp13.err" rc=$? [ "$rc" -eq 0 ] || fail "13: red-proof on a 1.8 plan with two (new) tests: exit $rc, expected 0: $(cat "$TMP/rp13.json" "$TMP/rp13.err")" tests_equal "$TMP/rp13.json" '["tests/a.test.mjs","tests/b.test.mjs"]' \ || fail "13: red-proof tests is not the two test paths in order: $(cat "$TMP/rp13.json")" # ---- 14. decision 41: a 1.7 plan that names no test file, then --tests ---- out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/p14.md" --step 1 2>&1)" rc=$? [ "$rc" -eq 2 ] || fail "14: red-proof on a 1.7 plan that names no test file: exit $rc, expected 2: $out" echo "$out" | grep -q 'no test files given' \ || fail "14: red-proof on a 1.7 plan that names no test file: no 'no test files given' in: $out" if echo "$out" | grep -Eq '^ tests: [^ ]'; then fail "14: red-proof on a 1.7 plan that names no test file: a tests line in: $out" fi out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/p14.md" --step 1 --tests tests/parser.test.mjs 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail "14: red-proof on a 1.7 plan with --tests the helper wrote: exit $rc, expected 0: $out" echo "$out" | grep -Fqx ' tests: tests/parser.test.mjs' \ || fail "14: red-proof on a 1.7 plan with --tests the helper wrote: no line ' tests: tests/parser.test.mjs' in: $out" echo 'e2e: ok' exit 0