docs(changelog): fix round 4.5b part 2, its red proof and the review's other findings as known issues
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f9cd08076e
commit
712a51591d
7 changed files with 535 additions and 0 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -69,6 +69,15 @@ Fix round 4.5b (decisions 80 to 87): the round's 39 findings, from the 4.5a revi
|
||||||
- **Applying a lesson writes no pointer through a link or for an ignored file** (DJ4, PTR). `lib/review/recurrence.mjs` skips the `CLAUDE.md` pointer when `CLAUDE.md` is a symlink or git ignores the lessons file, and refuses a lessons file that is a symlink or whose directory lies outside the repo; `/trekexecute` Phase 7.2 commits only the review's paths git does not ignore.
|
- **Applying a lesson writes no pointer through a link or for an ignored file** (DJ4, PTR). `lib/review/recurrence.mjs` skips the `CLAUDE.md` pointer when `CLAUDE.md` is a symlink or git ignores the lessons file, and refuses a lessons file that is a symlink or whose directory lies outside the repo; `/trekexecute` Phase 7.2 commits only the review's paths git does not ignore.
|
||||||
- **The research and review texts are said once and pinned exactly** (E17a, E17b, E18a, Da, Db, E16a). The two `/trekresearch` sentences on outbound queries and the F9 triage rows are pinned exactly; the deep-research adapter says where wave 2's contrarian findings merge; README's deep-research parenthesis says "when the engine is active", and the test that no README line stars `security-researcher` reads every line. The next-session validator's refusal of `--state-file` with no value is tested.
|
- **The research and review texts are said once and pinned exactly** (E17a, E17b, E18a, Da, Db, E16a). The two `/trekresearch` sentences on outbound queries and the F9 triage rows are pinned exactly; the deep-research adapter says where wave 2's contrarian findings merge; README's deep-research parenthesis says "when the engine is active", and the test that no README line stars `security-researcher` reads every line. The next-session validator's refusal of `--state-file` with no value is tested.
|
||||||
|
|
||||||
|
**Fix round 4.5b, part 2 (decision 88)**
|
||||||
|
|
||||||
|
Part 2 fixes the four defects that part 1 introduced and its own review found, plus the Bash guard's first-opener-only check; each has a test seen failing first at part 1's end point `52c382c` (`tests/fix-round-4-5b-part-2-red.sh` proves it).
|
||||||
|
|
||||||
|
- **A quoted value stops at a quote that starts a word, and never swallows a later flag** (TOK). In `lib/parsers/arg-parser.mjs`, a closing quote must end a word, and the search for it stops at a quote of the same kind that starts a word; with no pair, the opening quote stays a literal character. Before, `--project "my dir"/x --since "HEAD~1"` gave one `--project` value that held `--since`, so `/trekreview` reviewed the wrong range; now `--since` is `HEAD~1`. An apostrophe inside a word keeps its value (`'Fix the user's login'` is one value), and nothing is expanded.
|
||||||
|
- **The Bash guard checks every opener of the parser's heredoc that has a line after it, and lets a one-line mention through** (HDG). `hooks/scripts/pre-bash-executor.mjs` checks an opener of `VOYAGE_ARGS_EOF_7F3C9E` only when a non-blank line follows the opener's line, because bash reads a heredoc body only from the lines after it, so a one-line `grep`, `sed` or `git commit -m` that mentions the opener is no longer refused. It now checks every opener, not only the first, so a quoted first mention can no longer hide a later unquoted heredoc whose arguments would run as commands.
|
||||||
|
- **A failed `git check-ignore` writes no lessons pointer, whatever language git speaks** (CKI). `lib/review/recurrence.mjs` no longer reads every `git check-ignore` exit 128 as "no repository": on 128 it runs `git rev-parse --git-dir` and looks for a `.git` entry from the repo up to the nearest `GIT_CEILING_DIRECTORIES` entry above it, and writes the `CLAUDE.md` pointer only when rev-parse also exits 128, `GIT_DIR` is unset and no `.git` entry is found. Anything else gives `check-failed` with a note that names the reason, and no branch reads git's message text. A `.claude` that is a symlink inside the repo now gives `check-failed` (no pointer), also when it points at a tracked directory.
|
||||||
|
- **The executor records the full commit id for the done line** (DLC). `/trekexecute` Sub-step G says `{hash}` is the full commit id from `git rev-parse HEAD` after the step's last commit, read again after any later commit of the step, never the short id `git commit` prints: `lib/verification/done-line.mjs --check` refuses a short id with exit 2, which sent a passing step to the plan. The progress line stores that full id, and Handover 5's `commit` row says so.
|
||||||
|
|
||||||
Fix round 4.5a (decisions 57, 67 and 68): the round's 30 findings, where a Voyage control could be bypassed or was too lax, or where the text gave a guarantee no code held. Each is fixed, and each has a test that fails on the code before it, except DJ, whose sentences already stood and whose pins fail when they are deleted (`tests/fix-round-4-5a-red.sh` proves every other id red at the round's start point, and DJ under its two mutations).
|
Fix round 4.5a (decisions 57, 67 and 68): the round's 30 findings, where a Voyage control could be bypassed or was too lax, or where the text gave a guarantee no code held. Each is fixed, and each has a test that fails on the code before it, except DJ, whose sentences already stood and whose pins fail when they are deleted (`tests/fix-round-4-5a-red.sh` proves every other id red at the round's start point, and DJ under its two mutations).
|
||||||
|
|
||||||
**Execute**
|
**Execute**
|
||||||
|
|
@ -141,6 +150,19 @@ Fix round 4.5a (decisions 57, 67 and 68): the round's 30 findings, where a Voyag
|
||||||
- **A plan's `profile:` field has no writer.** README, `docs/`, `/trekexecute` and `/trekcontinue` say a plan carries or passes on a profile, but no command writes `profile:` to a plan; the plan validator's `MANIFEST_PROFILE_DRIFT` check reads it, and `resolveTrekcontinueProfile` has no caller outside its tests. Left whole for a later hardening part, which chooses between writing the field and taking the promises out.
|
- **A plan's `profile:` field has no writer.** README, `docs/`, `/trekexecute` and `/trekcontinue` say a plan carries or passes on a profile, but no command writes `profile:` to a plan; the plan validator's `MANIFEST_PROFILE_DRIFT` check reads it, and `resolveTrekcontinueProfile` has no caller outside its tests. Left whole for a later hardening part, which chooses between writing the field and taking the promises out.
|
||||||
- **Splices and dropped `--x=` options outside fix round 4.5b** (decision 82). The commands' bash blocks still splice `{placeholder}` values into shell lines outside the four places the round closed (the Phase 0 parser lines of `/trekendsession`, `/trekcontinue` and `/trekreview`, and `/trekendsession` Phase 3), for example `"{brief_path}"` in `/trekplan` and the unquoted `{project_dir}` in `/trekendsession` Phase 4; and the release check, the lessons apply (`lib/review/recurrence.mjs`), the next-session validator and the park tool still drop an unknown `--x=` option without a word. One open finding, for fix round 4.5c or 4.5d.
|
- **Splices and dropped `--x=` options outside fix round 4.5b** (decision 82). The commands' bash blocks still splice `{placeholder}` values into shell lines outside the four places the round closed (the Phase 0 parser lines of `/trekendsession`, `/trekcontinue` and `/trekreview`, and `/trekendsession` Phase 3), for example `"{brief_path}"` in `/trekplan` and the unquoted `{project_dir}` in `/trekendsession` Phase 4; and the release check, the lessons apply (`lib/review/recurrence.mjs`), the next-session validator and the park tool still drop an unknown `--x=` option without a word. One open finding, for fix round 4.5c or 4.5d.
|
||||||
- **Two 4.5b findings moved to fix round 4.5c.** M12 (`lib/review/reviewed-commit.mjs` makes four git calls per path per commit; at most two paths per commit reach that loop) goes with 4.5c's single `git cat-file --batch` read, and Dc (the trekcontinue `line_ref` row in `tests/fixtures/jsonl-schemas.md`) with 4.5c's change of `line_ref` to a heading anchor.
|
- **Two 4.5b findings moved to fix round 4.5c.** M12 (`lib/review/reviewed-commit.mjs` makes four git calls per path per commit; at most two paths per commit reach that loop) goes with 4.5c's single `git cat-file --batch` read, and Dc (the trekcontinue `line_ref` row in `tests/fixtures/jsonl-schemas.md`) with 4.5c's change of `line_ref` to a heading anchor.
|
||||||
|
- **Step 9's narrowed unborn-HEAD rule is not recorded as a plan deviation** (MAJOR `c76a0bb8`, `lib/util/park-attempt-files.mjs`; decision 88). The park tool reads HEAD as unborn only when `headIsUnborn` agrees, which narrows plan Step 9's rule; the narrowing was logged as an executor decision, not as a plan amendment.
|
||||||
|
- **The arg parser's stdin-unreadable branch has no test** (MAJOR `b0fd0a04`, `lib/parsers/arg-parser.mjs`; decision 88). No test exercises `ARG_STDIN_UNREADABLE` and its exit 1, the path that makes `/trekcontinue` and `/trekendsession` stop when stdin cannot be read.
|
||||||
|
- **The park tool's packed-refs branch has no test** (MAJOR `f7e8278b`, `lib/util/park-attempt-files.mjs`; decision 88). The branch that tells a corrupt packed ref from an unborn HEAD is not exercised; the corrupt-ref test covers only a loose ref.
|
||||||
|
- **`/trekendsession` takes the label from after the first token** (MAJOR `4ec591f4`, `commands/trekendsession.md`; decision 88). Phase 3 reads the label as the parser's `tail`, the text after the first token, while the brief path is the first positional, so a flag typed before the brief path ends up in the label.
|
||||||
|
- **Part 1's red script does not run the guard's tests** (MAJOR `2c904235`, `tests/fix-round-4-5b-red.sh`; decision 88). Its FILES leave out `tests/hooks/bash-guard.test.mjs`, so the `[ARG]` guard tests are never judged red at `be0b631`.
|
||||||
|
- **`/trekendsession` Phase 3 repeats Phase 1's project lookup** (MAJOR `450dbabb`, `commands/trekendsession.md`; decision 88). Its inline script enumerates the project directories and their error texts a second time, so the two copies can drift.
|
||||||
|
- **The park tool misnames a detached HEAD it cannot read** (MAJOR `e3824c6d`, `lib/util/park-attempt-files.mjs`; decision 88). On a detached HEAD whose commit is missing it exits 2, as it should, with a reason that names a corrupt branch ref.
|
||||||
|
- **The lessons-block check accepts any valid lesson line** (MINOR `5114fa84`, `lib/review/reviewed-commit.mjs`; decision 88). A line added inside the block that passes the lesson-line check, an instruction written in the lesson form included, does not make the review stale.
|
||||||
|
- **The label keeps inner quotes** (MINOR `e585ce27`, `lib/parsers/arg-parser.mjs` `tailOf`; decision 88). `tailOf` strips quotes only when one pair wraps the whole rest, so `brief.md "Session 2" of "3"` gives the label `"Session 2" of "3"`.
|
||||||
|
- **The Bash guard knows one heredoc delimiter** (MINOR `8959d2cd`, `hooks/scripts/pre-bash-executor.mjs`; decision 88). It has no general rule for a quoted heredoc's closing line, so another heredoc in the commands, such as a stats record's, can still be closed early by a typed line equal to its delimiter.
|
||||||
|
- **A command that mentions the parser's heredoc opener with a line after it stays blocked** (fix round 4.5b part 2, decision 88). The guard checks every opener of `VOYAGE_ARGS_EOF_7F3C9E` that has a non-blank line after the opener's line, so such a command stays blocked unless it is a quoted heredoc whose closing line is the one last non-empty line, for example a commit message written as a heredoc that mentions the opener. Write it on one line, or read it from a file.
|
||||||
|
- **A `bash -c $'...'` command on one physical line is not checked** (fix round 4.5b part 2, decision 88). It carries no literal newline, so the guard sees no line after the opener even when `bash -c` expands it into a heredoc of the parser's delimiter; part 1 blocked it by accident. The shipped commands never produce it from `$ARGUMENTS`.
|
||||||
|
- **The tokenizer splits a quoted value at a quote of the same kind that starts a word** (TOK, decision 88). `'the '90s were great'` now gives `'the` and `90s were great`; a quoted value that ends in whitespace (`"a "`) no longer pairs and gives `"a` and `"`; and the argv form's self-quoting of a word that holds spaces (`-- 'say "hi"'`) splits. The operator accepted these.
|
||||||
|
|
||||||
### Doors
|
### Doors
|
||||||
|
|
||||||
|
|
@ -150,6 +172,7 @@ Fix round 4.5a (decisions 57, 67 and 68): the round's 30 findings, where a Voyag
|
||||||
script, the page, the stored notes and the approval checks are unchanged.
|
script, the page, the stored notes and the approval checks are unchanged.
|
||||||
- two-way: fix round 4.5a (decisions 57, 67, 68) — a revert restores the old texts and checks
|
- two-way: fix round 4.5a (decisions 57, 67, 68) — a revert restores the old texts and checks
|
||||||
- two-way: fix round 4.5b (decisions 80 to 87) — a revert restores the old checks, texts and red script and takes out the Bash guard's heredoc rule; `review.md` keeps `review_version` 1.1 either way.
|
- two-way: fix round 4.5b (decisions 80 to 87) — a revert restores the old checks, texts and red script and takes out the Bash guard's heredoc rule; `review.md` keeps `review_version` 1.1 either way.
|
||||||
|
- two-way: fix round 4.5b part 2 (decision 88) — a revert restores part 1's four behaviours (the tokenizer's quote search, the guard's refusal of a one-line mention, the pointer written on any `git check-ignore` exit 128, the undefined `{hash}`) and the guard's first-opener-only check.
|
||||||
|
|
||||||
## v6.5.0 — 2026-10-18 — Three skills for brief, criteria and review, English labels, and three trial-5 fixes
|
## v6.5.0 — 2026-10-18 — Three skills for brief, criteria and review, English labels, and three trial-5 fixes
|
||||||
|
|
||||||
|
|
|
||||||
220
tests/e2e-fix-round-4-5b-part-2.sh
Normal file
220
tests/e2e-fix-round-4-5b-part-2.sh
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
#!/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
|
||||||
10
tests/fix-round-4-5b-part-2-mutations.mjs
Normal file
10
tests/fix-round-4-5b-part-2-mutations.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// tests/fix-round-4-5b-part-2-mutations.mjs
|
||||||
|
// The mutations of fix round 4-5b, part 2, for the shared red script
|
||||||
|
// (tests/helpers/red-round.sh, through tests/helpers/red-mutate.mjs). The
|
||||||
|
// round has none: every part-2 id (TOK, HDG, CKI, DLC, KI) has tests that fail
|
||||||
|
// on an assertion at the round's start point 52c382c, on that id's own code or
|
||||||
|
// text, so no id needs a mutation to be proven red. red-mutate.mjs list
|
||||||
|
// accepts the empty array, and the red script then reports 0 mutation(s).
|
||||||
|
// Data only; never matched by `npm test`.
|
||||||
|
|
||||||
|
export const MUTATIONS = [];
|
||||||
44
tests/fix-round-4-5b-part-2-red.sh
Normal file
44
tests/fix-round-4-5b-part-2-red.sh
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# tests/fix-round-4-5b-part-2-red.sh
|
||||||
|
# The red proof of fix round 4-5b, part 2 (plan
|
||||||
|
# .claude/projects/2026-10-27-fix-round-4-5b-part-2, decision 88), as a
|
||||||
|
# wrapper over the shared red script tests/helpers/red-round.sh. Every id of
|
||||||
|
# the round has tests whose names start with "[id]"; each of them must fail
|
||||||
|
# on an assertion at the round's start point 52c382c.
|
||||||
|
# This wrapper passes the shared script:
|
||||||
|
# --base BASE, --ids IDS (the round's 5 ids), the round's FILES;
|
||||||
|
# --other-ids: the ids of fix round 4-5a and of 4-5b part 1, read with sed
|
||||||
|
# from the IDS= lines of tests/fix-round-4-5a-red.sh and
|
||||||
|
# tests/fix-round-4-5b-red.sh and passed as ONE value, 4-5a's first (the
|
||||||
|
# shared script keeps only the last --other-ids it is given); a tag on
|
||||||
|
# that list is not judged, so the [ARG] tests in bash-guard.test.mjs are
|
||||||
|
# left to part 1;
|
||||||
|
# --mutations tests/fix-round-4-5b-part-2-mutations.mjs (empty: every id is
|
||||||
|
# red at the start point on its own code).
|
||||||
|
# No --require-sha256: the round has no SHA-256 test.
|
||||||
|
# Exit 0 all red, 1 a rule broken (the judge names it), 2 a setup failure
|
||||||
|
# (also: no ids on either older IDS= line); never 0 on a setup failure.
|
||||||
|
# bash 3.2 compatible, ASCII only, no arrays.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
set -f
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.." || { echo "red script: setup failure: cannot cd to the repo root" >&2; exit 2; }
|
||||||
|
ROOT="$(pwd -P)"
|
||||||
|
|
||||||
|
BASE=52c382c19885819d747bbcb23a34ae2015470f97
|
||||||
|
IDS='TOK HDG CKI DLC KI'
|
||||||
|
FILES='tests/lib/arg-parser-tokenize.test.mjs tests/hooks/bash-guard.test.mjs tests/lib/review-recurrence-check-ignore.test.mjs tests/lib/fix-round-4-5b-part-2-prose.test.mjs'
|
||||||
|
|
||||||
|
OTHER_A=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5a-red.sh")
|
||||||
|
if [ -z "$OTHER_A" ]; then
|
||||||
|
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5a-red.sh" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
OTHER_B=$(sed -n "s/^IDS='\(.*\)'\$/\1/p" "$ROOT/tests/fix-round-4-5b-red.sh")
|
||||||
|
if [ -z "$OTHER_B" ]; then
|
||||||
|
echo "red script: setup failure: no ids on the IDS= line of $ROOT/tests/fix-round-4-5b-red.sh" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec /bin/bash "$ROOT/tests/helpers/red-round.sh" --base "$BASE" --ids "$IDS" --other-ids "$OTHER_A $OTHER_B" --mutations "$ROOT/tests/fix-round-4-5b-part-2-mutations.mjs" $FILES
|
||||||
35
tests/fix-round-4-5b-part-2-regression.sh
Normal file
35
tests/fix-round-4-5b-part-2-regression.sh
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# tests/fix-round-4-5b-part-2-regression.sh
|
||||||
|
# The Regression: of fix round 4-5b, part 2 (decision 88): the whole suite
|
||||||
|
# (npm test), part 1's end to end (tests/e2e-fix-round-4-5b.sh, which drives
|
||||||
|
# the arg parser and `recurrence.mjs apply`, both changed by part 2) and
|
||||||
|
# part 2's red proof (tests/fix-round-4-5b-part-2-red.sh). The 4-5a and
|
||||||
|
# part-1 red proofs are not run here: part 1's alone re-runs its base and its
|
||||||
|
# mutations, and one Regression command has the criteria runner's 15-minute
|
||||||
|
# cap; they run as their own Verification lines. All three run; the script
|
||||||
|
# prints each exit and exits with the first non-zero status, or 0 when all
|
||||||
|
# three exit 0.
|
||||||
|
# bash 3.2 compatible, ASCII only, no arrays.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.." || { echo "regression: cannot cd to the repo root" >&2; exit 2; }
|
||||||
|
|
||||||
|
FIRST=0
|
||||||
|
|
||||||
|
npm test
|
||||||
|
rc=$?
|
||||||
|
echo "regression: npm test exited $rc"
|
||||||
|
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||||
|
|
||||||
|
/bin/bash tests/e2e-fix-round-4-5b.sh
|
||||||
|
rc=$?
|
||||||
|
echo "regression: tests/e2e-fix-round-4-5b.sh exited $rc"
|
||||||
|
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||||
|
|
||||||
|
/bin/bash tests/fix-round-4-5b-part-2-red.sh
|
||||||
|
rc=$?
|
||||||
|
echo "regression: tests/fix-round-4-5b-part-2-red.sh exited $rc"
|
||||||
|
[ "$FIRST" -eq 0 ] && FIRST=$rc
|
||||||
|
|
||||||
|
exit "$FIRST"
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
// id `git commit` prints (done-line --check refuses a short id with
|
// id `git commit` prints (done-line --check refuses a short id with
|
||||||
// exit 2). The progress line stores that full id, and Handover 5's
|
// exit 2). The progress line stores that full id, and Handover 5's
|
||||||
// `commit` row says so.
|
// `commit` row says so.
|
||||||
|
// KI the CHANGELOG lists part 2's fixes under ### Fixed, its door under
|
||||||
|
// ### Doors, and the review's other findings (7 MAJOR, 3 MINOR, by their
|
||||||
|
// 8-hex ids) and part 2's accepted side effects under ### Known issues.
|
||||||
// No static import of a new export: the file must load at 52c382c.
|
// No static import of a new export: the file must load at 52c382c.
|
||||||
// Fix the SOURCE a pin guards, not the pin.
|
// Fix the SOURCE a pin guards, not the pin.
|
||||||
|
|
||||||
|
|
@ -85,3 +88,82 @@ test('[DLC] Sub-step G records the full commit id from git rev-parse HEAD after
|
||||||
assert.ok(p75.includes('--commit {steps.N.commit}'), 'Control: Phase 7.5 keeps --commit {steps.N.commit}');
|
assert.ok(p75.includes('--commit {steps.N.commit}'), 'Control: Phase 7.5 keeps --commit {steps.N.commit}');
|
||||||
assert.doesNotMatch(g, /--short|%h|--abbrev/, 'Control: Sub-step G must not ask for an abbreviated commit id');
|
assert.doesNotMatch(g, /--short|%h|--abbrev/, 'Control: Sub-step G must not ask for an abbreviated commit id');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- KI: the round's CHANGELOG entry -----------------------------------------------
|
||||||
|
// Part 2 stands in the same Unreleased section as part 1. The section is found
|
||||||
|
// by its title, by the rule tests/lib/fix-round-4-5b-prose.test.mjs uses
|
||||||
|
// (copied, not imported): `## Unreleased — <title>` before the release,
|
||||||
|
// `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `.
|
||||||
|
|
||||||
|
const CHANGELOG_TITLE = "The annotating skill, the run coupling and P10's model-free part";
|
||||||
|
const CHANGELOG_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${CHANGELOG_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||||
|
|
||||||
|
/** The lines of the round's CHANGELOG section, heading excluded. */
|
||||||
|
function changelogSection() {
|
||||||
|
const lines = read('CHANGELOG.md').split('\n');
|
||||||
|
const at = [];
|
||||||
|
lines.forEach((l, i) => { if (CHANGELOG_HEADING.test(l.replace(/\s+$/, ''))) at.push(i); });
|
||||||
|
assert.equal(at.length, 1, `CHANGELOG.md must hold the "${CHANGELOG_TITLE}" section exactly once (found ${at.length})`);
|
||||||
|
let end = lines.length;
|
||||||
|
for (let i = at[0] + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; }
|
||||||
|
return lines.slice(at[0] + 1, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */
|
||||||
|
function changelogBullets(heading) {
|
||||||
|
const lines = changelogSection();
|
||||||
|
const s = lines.findIndex((l) => l.replace(/\s+$/, '') === heading);
|
||||||
|
assert.ok(s !== -1, `the "${CHANGELOG_TITLE}" section of CHANGELOG.md must have "${heading}"`);
|
||||||
|
const out = [];
|
||||||
|
for (const l of lines.slice(s + 1)) {
|
||||||
|
if (/^### /.test(l)) break;
|
||||||
|
if (/^- /.test(l)) out.push(l);
|
||||||
|
else if (out.length && l.trim() !== '' && /^\s/.test(l)) out[out.length - 1] += ` ${l}`;
|
||||||
|
else if (l.trim() !== '') out.push('');
|
||||||
|
}
|
||||||
|
return out.filter((b) => b !== '').map(flat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The review's findings part 2 leaves open (7 MAJOR, then 3 MINOR), and the
|
||||||
|
// ones it fixes; the ids are the review's, written out here.
|
||||||
|
const OPEN_IDS = ['c76a0bb8', 'b0fd0a04', 'f7e8278b', '4ec591f4', '2c904235', '450dbabb', 'e3824c6d', '5114fa84', 'e585ce27', '8959d2cd'];
|
||||||
|
const FIXED_IDS = ['c9b5bce4', '021996c6', '20375547', 'b30f057c', '49080c36'];
|
||||||
|
|
||||||
|
test('[KI] the CHANGELOG lists part 2\'s fixes and the review\'s other findings under Known issues', () => {
|
||||||
|
const known = changelogBullets('### Known issues');
|
||||||
|
|
||||||
|
// Red: each open finding has exactly one Known-issues bullet, and that bullet names decision 88.
|
||||||
|
for (const id of OPEN_IDS) {
|
||||||
|
const hits = known.filter((b) => b.includes(id));
|
||||||
|
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet naming ${id}; found ${hits.length}`);
|
||||||
|
}
|
||||||
|
for (const b of known.filter((k) => OPEN_IDS.some((id) => k.includes(id)))) {
|
||||||
|
assert.ok(b.includes('decision 88'), `a Known-issues bullet naming an open finding does not say decision 88: ${b}`);
|
||||||
|
}
|
||||||
|
const followed = known.filter((b) => b.includes('VOYAGE_ARGS_EOF_7F3C9E') && b.includes('stays blocked'));
|
||||||
|
assert.equal(followed.length, 1, `### Known issues must hold one bullet saying a command that mentions VOYAGE_ARGS_EOF_7F3C9E with a line after it stays blocked; found ${followed.length}`);
|
||||||
|
const bashC = known.filter((b) => b.includes("bash -c $'"));
|
||||||
|
assert.equal(bashC.length, 1, `### Known issues must hold one bullet on the one-physical-line bash -c $'...' case; found ${bashC.length}`);
|
||||||
|
const tok = known.filter((b) => b.includes("'90s"));
|
||||||
|
assert.equal(tok.length, 1, `### Known issues must hold one bullet on the tokenizer's word-initial quote ('90s); found ${tok.length}`);
|
||||||
|
|
||||||
|
// Red elsewhere: one ### Fixed bullet per part-2 id, and a door for the round.
|
||||||
|
const fixed = changelogBullets('### Fixed');
|
||||||
|
for (const id of ['(TOK)', '(HDG)', '(CKI)', '(DLC)']) {
|
||||||
|
const hits = fixed.filter((b) => b.includes(id));
|
||||||
|
assert.equal(hits.length, 1, `### Fixed must hold exactly one bullet marked ${id}; found ${hits.length}`);
|
||||||
|
}
|
||||||
|
const doors = changelogBullets('### Doors').filter((b) => b.includes('decision 88'));
|
||||||
|
assert.equal(doors.length, 1, `### Doors must hold one bullet naming decision 88; found ${doors.length}`);
|
||||||
|
|
||||||
|
// Control: the three earlier Known issues stay, no fixed finding is listed as
|
||||||
|
// open, and the R6 Fixed bullet keeps the one backticked rule name.
|
||||||
|
for (const start of ["- **A plan's `profile:` field has no writer.**", '- **Splices and dropped', '- **Two 4.5b findings moved to fix round 4.5c.**']) {
|
||||||
|
assert.equal(known.filter((b) => b.startsWith(start)).length, 1, `Control: ### Known issues must keep the bullet opening ${start}`);
|
||||||
|
}
|
||||||
|
for (const id of FIXED_IDS) {
|
||||||
|
assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`);
|
||||||
|
}
|
||||||
|
assert.equal(fixed.filter((b) => b.includes('`args-heredoc-closed-early`')).length, 1,
|
||||||
|
'Control: exactly one ### Fixed bullet holds `args-heredoc-closed-early`');
|
||||||
|
});
|
||||||
|
|
|
||||||
121
tests/lib/fix-round-4-5b-part-2-scripts.test.mjs
Normal file
121
tests/lib/fix-round-4-5b-part-2-scripts.test.mjs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
// tests/lib/fix-round-4-5b-part-2-scripts.test.mjs
|
||||||
|
// The form of fix round 4-5b part 2's three shell scripts (plan
|
||||||
|
// .claude/projects/2026-10-27-fix-round-4-5b-part-2, Step 5h, decision 88):
|
||||||
|
// the red wrapper, the regression script and the end-to-end run. Untagged on
|
||||||
|
// purpose and listed in no red wrapper's FILES: these checks are not a part-2
|
||||||
|
// fix, and they fail at no start point.
|
||||||
|
// - each script is ASCII only, passes /bin/bash -n and uses no bash 4 form;
|
||||||
|
// - the red wrapper hands the shared red script exactly one --other-ids,
|
||||||
|
// whose words are the 4-5a IDS= words followed by part 1's, and --ids
|
||||||
|
// TOK HDG CKI DLC KI (run from a temp root next to a stub red-round.sh
|
||||||
|
// that prints its arguments, the method of tests/lib/red-script.test.mjs);
|
||||||
|
// - with the 4-5a or the part-1 wrapper lacking its IDS= line, the red
|
||||||
|
// wrapper exits 2 with a setup failure and never runs the shared script.
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { childEnv } from '../helpers/child-env.mjs';
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||||
|
|
||||||
|
const RED = 'tests/fix-round-4-5b-part-2-red.sh';
|
||||||
|
const REGRESSION = 'tests/fix-round-4-5b-part-2-regression.sh';
|
||||||
|
const E2E = 'tests/e2e-fix-round-4-5b-part-2.sh';
|
||||||
|
const WRAP_A = 'tests/fix-round-4-5a-red.sh';
|
||||||
|
const WRAP_B = 'tests/fix-round-4-5b-red.sh';
|
||||||
|
const PART2_IDS = ['TOK', 'HDG', 'CKI', 'DLC', 'KI'];
|
||||||
|
|
||||||
|
/** A repo file's bytes; the file must exist. */
|
||||||
|
function bytes(rel) {
|
||||||
|
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
|
||||||
|
return readFileSync(join(ROOT, rel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The words of a wrapper's one IDS='...' line. */
|
||||||
|
function idsOf(rel) {
|
||||||
|
const lines = bytes(rel).toString('utf8').split('\n').filter((l) => l.startsWith('IDS='));
|
||||||
|
assert.equal(lines.length, 1, `expected one IDS= line in ${rel}; found ${lines.length}`);
|
||||||
|
const m = lines[0].match(/^IDS='([^']*)'$/);
|
||||||
|
assert.ok(m, `the IDS= line of ${rel} is not one single-quoted list: ${lines[0]}`);
|
||||||
|
return m[1].split(/\s+/).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('part-2 scripts: each is ASCII only, passes /bin/bash -n and uses no bash 4 form', () => {
|
||||||
|
for (const rel of [RED, REGRESSION, E2E]) {
|
||||||
|
const buf = bytes(rel);
|
||||||
|
assert.equal(buf.findIndex((b) => b > 127), -1, `${rel} is not ASCII only`);
|
||||||
|
const r = spawnSync('/bin/bash', ['-n', join(ROOT, rel)], { encoding: 'utf8', env: childEnv() });
|
||||||
|
assert.equal(r.status, 0, `${rel} does not pass /bin/bash -n: ${r.stderr}`);
|
||||||
|
const text = buf.toString('utf8');
|
||||||
|
for (const bash4 of [/\bdeclare -A\b/, /\bmapfile\b/, /\breadarray\b/, /\|&/]) {
|
||||||
|
assert.doesNotMatch(text, bash4, `${rel} uses ${bash4}, which bash 3.2 lacks`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** A stand-in for the shared script: one ARG: line per argument, exit 0. */
|
||||||
|
const STUB = `#!/bin/bash
|
||||||
|
for a in "$@"; do printf 'ARG:%s\\n' "$a"; done
|
||||||
|
exit 0
|
||||||
|
`;
|
||||||
|
|
||||||
|
/** A temp root with the part-2 wrapper, both older wrappers and the stub as tests/helpers/red-round.sh. */
|
||||||
|
function stubRoot(base) {
|
||||||
|
const dir = join(base, 'root');
|
||||||
|
mkdirSync(join(dir, 'tests', 'helpers'), { recursive: true });
|
||||||
|
for (const rel of [RED, WRAP_A, WRAP_B]) writeFileSync(join(dir, rel), bytes(rel));
|
||||||
|
writeFileSync(join(dir, 'tests', 'helpers', 'red-round.sh'), STUB, { mode: 0o755 });
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The part-2 wrapper run from a temp root: status, output and the arguments the stub saw. */
|
||||||
|
function runWrapper(dir) {
|
||||||
|
const r = spawnSync('/bin/bash', [join(dir, RED)], { cwd: tmpdir(), encoding: 'utf8', timeout: 30000, env: childEnv() });
|
||||||
|
const argv = (r.stdout || '').split('\n').filter((l) => l.startsWith('ARG:')).map((l) => l.slice(4));
|
||||||
|
return { status: r.status, out: `${r.stdout}${r.stderr}`, stderr: r.stderr || '', argv };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('part-2 scripts: the red wrapper passes one --other-ids, the 4-5a ids then part 1\'s, and its own five ids', () => {
|
||||||
|
const text = bytes(RED).toString('utf8');
|
||||||
|
const execLines = text.split('\n').filter((l) => /^exec /.test(l));
|
||||||
|
assert.equal(execLines.length, 1, `${RED} must hold one exec line; found ${execLines.length}`);
|
||||||
|
assert.equal(execLines[0].split('--other-ids').length - 1, 1, `${RED}: the exec line must pass --other-ids exactly once: ${execLines[0]}`);
|
||||||
|
|
||||||
|
const base = mkdtempSync(join(tmpdir(), 'part2-red-wrap-'));
|
||||||
|
try {
|
||||||
|
const r = runWrapper(stubRoot(base));
|
||||||
|
assert.equal(r.status, 0, `${RED} did not run the shared script:\n${r.out}`);
|
||||||
|
const at = r.argv.map((a, i) => (a === '--other-ids' ? i : -1)).filter((i) => i >= 0);
|
||||||
|
assert.equal(at.length, 1, `${RED} handed the shared script --other-ids ${at.length} times:\n${r.out}`);
|
||||||
|
const other = (r.argv[at[0] + 1] || '').split(/\s+/).filter(Boolean);
|
||||||
|
assert.deepEqual(other, [...idsOf(WRAP_A), ...idsOf(WRAP_B)],
|
||||||
|
`${RED}: --other-ids is not the IDS= words of ${WRAP_A} followed by those of ${WRAP_B}\n${r.out}`);
|
||||||
|
const idsAt = r.argv.indexOf('--ids');
|
||||||
|
assert.ok(idsAt >= 0, `${RED} passed no --ids:\n${r.out}`);
|
||||||
|
assert.deepEqual(r.argv[idsAt + 1].split(/\s+/).filter(Boolean), PART2_IDS, `${RED}: --ids is not ${PART2_IDS.join(' ')}\n${r.out}`);
|
||||||
|
} finally {
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('part-2 scripts: the red wrapper exits 2 with a setup failure when an older wrapper has no IDS= line', () => {
|
||||||
|
for (const missing of [WRAP_A, WRAP_B]) {
|
||||||
|
const base = mkdtempSync(join(tmpdir(), 'part2-red-noids-'));
|
||||||
|
try {
|
||||||
|
const dir = stubRoot(base);
|
||||||
|
const text = bytes(missing).toString('utf8');
|
||||||
|
writeFileSync(join(dir, missing), text.split('\n').filter((l) => !l.startsWith('IDS=')).join('\n'));
|
||||||
|
const r = runWrapper(dir);
|
||||||
|
assert.equal(r.status, 2, `${RED}: ${missing} without an IDS= line is not exit 2\n${r.out}`);
|
||||||
|
assert.match(r.stderr, /setup failure/, `${RED}: no setup failure line when ${missing} has no IDS= line\n${r.out}`);
|
||||||
|
assert.deepEqual(r.argv, [], `${RED} ran the shared script though ${missing} has no IDS= line\n${r.out}`);
|
||||||
|
} finally {
|
||||||
|
rmSync(base, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue