docs(changelog): fix round 4.5b2 part A, the part 4 bullets brought in line, two findings and the minor ones as known issues
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ebec492f6a
commit
023ccc32af
3 changed files with 232 additions and 5 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -89,11 +89,23 @@ Part 3 fixes two MAJOR findings of part 2's review, in the two places part 2 rew
|
||||||
|
|
||||||
Part 4 fixes four MAJOR findings of part 3's review in the Bash guard's check of the argument heredoc; each has a test seen failing first at part 3's end point `591f3c4` (`tests/fix-round-4-5b-part-4-red.sh` proves it). The tokenizer headline in part 2's group above no longer claims more than the tokenizer does (see the tokenizer Known issue).
|
Part 4 fixes four MAJOR findings of part 3's review in the Bash guard's check of the argument heredoc; each has a test seen failing first at part 3's end point `591f3c4` (`tests/fix-round-4-5b-part-4-red.sh` proves it). The tokenizer headline in part 2's group above no longer claims more than the tokenizer does (see the tokenizer Known issue).
|
||||||
|
|
||||||
- **The guard checks the parser's delimiter when a backslash-newline splits it** (CSG). `hooks/scripts/pre-bash-executor.mjs` checks a command that names the parser's delimiter only once a backslash-newline is removed, so an unquoted opener split that way with a body and no closing line is blocked as unquoted; such a command with a quoted opener and no closing line, or with an unreadable opener elsewhere, is now checked too and refused, as it would be with the delimiter written out. It names review finding `3a34adbe`.
|
- **The guard checks the parser's delimiter when a backslash-newline splits it** (CSG). `hooks/scripts/pre-bash-executor.mjs` checks a command that names the parser's delimiter only once a backslash-newline is removed, so an unquoted opener split that way with a body and no closing line is blocked as unquoted; such a command with a quoted opener and no closing line, or with an unreadable opener elsewhere, is now checked too and refused, as it would be with the delimiter written out. It names review finding `3a34adbe`. A split delimiter with a suffix was not refused; it is now, fixed below as SFX, and inside a string as SIS. Part 4's review found the gap: review finding `27b0b46a`.
|
||||||
- **A `<<` cut by the quote that closes a string on its line is text** (SLT). A `<<` inside a string on the opener's line, whose cutting quote closes that string, is text and passes, so commit messages, grep patterns and file writes that hold `"<<"` and name the delimiter pass again; a quote with no string open before it stays unreadable and blocked; a string opened on an earlier line is still refused (see Known issues). It names review finding `ad8ad828`.
|
- **A `<<` cut by the quote that closes a string on its line is text to the shell reading** (SLT). A `<<` inside a string on the opener's line, whose cutting quote closes that string, is text to the shell reading, unless a backslash-newline splits its opener (see SIS below). The regex reading still reads a `<<` that the delimiter follows, and the string test counts one quote character from the line start, so such a command passes only where neither reading blocks it: a grep pattern `'<<'` on a line that names the delimiter further on, a commit message or file write whose heredoc body holds `"<<"` with the delimiter elsewhere on its line, and a string holding `<<` before the commands' argument heredoc. A grep pattern or commit message whose string puts the delimiter right after `<<`, and one whose string holds a quote of the other kind or an escaped quote before the `<<`, are refused as before (see Known issues); a quote with no string open before it stays unreadable and blocked; a string opened on an earlier line is still refused. It names review finding `ad8ad828`. Part 4's review found the gap: review findings `943f9841` and `a6735ef9`.
|
||||||
- **A backslash-escaped `<` with no quote or heredoc operator before it starts no opener** (ESC). A `<` after an odd run of backslashes starts no opener when no quote, backquote or heredoc operator stands before the run, in both readings, so `echo \<<` followed by the delimiter and a line passes; the `<` after it is read as usual, so a `<<` right after an escaped `<`, which the guard took for part of a here-string before, is now checked; an escaped `<` inside a heredoc body fed to a shell is still checked. It names review finding `797784e0`.
|
- **A backslash-escaped `<` starts no opener while no quote is still open before it, no heredoc body holds it and the guard can follow the shell up to it, with one exception** (ESC). A `<` after an odd run of backslashes starts no opener when no quote is still open before the run, no earlier heredoc is still open, and the scan can follow the shell there, in both readings, so `echo \<<` followed by the delimiter and a line passes. The exception: two escaped `<` in a row that name the delimiter with a line after them are refused, also where no second shell reads them (see EVL below). The `<` after an escaped one is read as usual, so a `<<` right after an escaped `<`, which the guard took for part of a here-string before, is now checked; an escaped `<` inside a heredoc body fed to a shell is still checked. It names review finding `797784e0`. The limits covered the whole command, fixed below as ESR. Part 4's review found the gap: review finding `ed40abdb`.
|
||||||
- **The guard's error path is tested** (CTB). A test forces an error inside the guard's shell reading and sees it block with exit 2, through a test-side preload; the hook gains no switch. It names review finding `1658fc64`.
|
- **The guard's error path is tested** (CTB). A test forces an error inside the guard's shell reading and sees it block with exit 2, through a test-side preload; the hook gains no switch. It names review finding `1658fc64`.
|
||||||
|
|
||||||
|
**Fix round 4.5b2, part A (decision 91)**
|
||||||
|
|
||||||
|
Part A fixes four of the six MAJOR findings of part 4's review in the Bash guard's check of the argument heredoc, one more gap the operator added (decision 107), a split opener inside a string that part 4's string skip let through (decision 123), and a false test header; the other two MAJOR findings are listed under Known issues (decision 122). Each fix has a test seen failing first at part 4's end point `e2d6bec`, or under a named mutation where the code already held (`tests/fix-round-4-5b2-a-red.sh` proves both), and a fixed bypass corpus (`tests/guard-bypass-corpus-4-5b2-a.sh`) keeps the commands blocked that each loosened rule must not let through.
|
||||||
|
|
||||||
|
- **An error inside the regex reading's escape limit is tested** (RXT). A test forces an error inside the regex reading's escape limit and sees the command blocked with exit 2. It names review finding `5f1cc34c`.
|
||||||
|
- **An opener split between its two `<` keeps the escape rule off** (CNT). A test pins an outer opener split between its two `<` as keeping the escape rule off. It names review finding `97d1f547`.
|
||||||
|
- **A split delimiter with a suffix is judged as written out** (SFX). `hooks/scripts/pre-bash-executor.mjs` judges a delimiter that a backslash-newline splits, followed by any suffix, as it judges the same opener written out. It names review finding `27b0b46a`.
|
||||||
|
- **The escape rule reads the text before the run, not the whole command** (ESR). The escape rule is off only while a quote is still open before the run, an earlier heredoc is not closed before it, or the scan cannot follow the shell (for example after `$'`, `$"`, a backquote, a parenthesis, a `#` or a continuation). It names review finding `ed40abdb`.
|
||||||
|
- **A heredoc operator a second shell would read from `\<\<` is blocked** (EVL). A heredoc operator a second shell would read from `\<\<`, with the delimiter after it (also when a backslash-newline splits it) and a line after it, is blocked, through `eval`, `bash -c`, `sh -c` or no second shell at all. It is decision 107.
|
||||||
|
- **A `<<` whose opener a backslash-newline splits is never skipped** (SIS). A `<<` whose opener a backslash-newline splits is never skipped, as text in a string or as text in the body of a quoted heredoc that passes, because the regex reading cannot see it; such an opener inside a `bash -c` string with a body was let through, also inside a quoted heredoc fed to `bash`, and the inner shell expanded the body. A harmless one with a line after the command may now be refused. Decision 126 widened it to the body skip. It is decision 123.
|
||||||
|
- **A test header names its red base** (HDR). The header of `tests/hooks/bash-guard-false-stops.test.mjs` named the wrong red base; it now names part 3's end point `591f3c4`. It names review finding `86488cd6`.
|
||||||
|
|
||||||
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**
|
||||||
|
|
@ -176,7 +188,7 @@ Fix round 4.5a (decisions 57, 67 and 68): the round's 30 findings, where a Voyag
|
||||||
- **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 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 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.
|
- **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 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. Two kinds of such command pass, under the conditions the ESC and SLT bullets above give: one whose opener's first `<` follows an odd run of backslashes, unless two escaped `<` in a row name the delimiter (see EVL), a case fix round 4.5b2 part A widens, and one whose opener is text inside a string on its line, unless a backslash-newline splits it (see SIS).
|
||||||
- **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`.
|
- **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'` gives `'the` and `90s were great` (accepted by the operator on 2026-10-27, part 2's brief). The argv form's self-quoting of a word that holds spaces (`-- 'say "hi"'`) splits (accepted on 2026-10-28). Since part 3, a quoted value that ends in whitespace pairs again, so such a value may also hold what follows it up to a quote that ends a word: `--project "x --since " HEAD` gives `--project` = `x --since `. Review finding `564d6dff` (decision 90) names this case: such a value can swallow a later flag with no error.
|
- **The tokenizer splits a quoted value at a quote of the same kind that starts a word** (TOK, decision 88). `'the '90s were great'` gives `'the` and `90s were great` (accepted by the operator on 2026-10-27, part 2's brief). The argv form's self-quoting of a word that holds spaces (`-- 'say "hi"'`) splits (accepted on 2026-10-28). Since part 3, a quoted value that ends in whitespace pairs again, so such a value may also hold what follows it up to a quote that ends a word: `--project "x --since " HEAD` gives `--project` = `x --since `. Review finding `564d6dff` (decision 90) names this case: such a value can swallow a later flag with no error.
|
||||||
- **`gitEntryDir` does not follow git's `GIT_CEILING_DIRECTORIES` rules** (MAJOR `3b98d18b`, `lib/review/recurrence.mjs`; decision 89). It resolves the entries after an empty one through realpath, and it keeps relative entries and entries whose realpath fails. So a ceiling written that way can give `no-git-repo` and a written pointer where git sees a repository.
|
- **`gitEntryDir` does not follow git's `GIT_CEILING_DIRECTORIES` rules** (MAJOR `3b98d18b`, `lib/review/recurrence.mjs`; decision 89). It resolves the entries after an empty one through realpath, and it keeps relative entries and entries whose realpath fails. So a ceiling written that way can give `no-git-repo` and a written pointer where git sees a repository.
|
||||||
|
|
@ -188,6 +200,15 @@ Fix round 4.5a (decisions 57, 67 and 68): the round's 30 findings, where a Voyag
|
||||||
- **Part 3's entry did not name the false stops of a string-borne `<<`** (MAJOR `6f09b931`, `CHANGELOG.md`; decision 90). Part 4's SLT bullet lets most of them through, and a command whose string opens on an earlier line than its `<<`, for example a commit message heredoc whose quoted text spans two lines, is still refused.
|
- **Part 3's entry did not name the false stops of a string-borne `<<`** (MAJOR `6f09b931`, `CHANGELOG.md`; decision 90). Part 4's SLT bullet lets most of them through, and a command whose string opens on an earlier line than its `<<`, for example a commit message heredoc whose quoted text spans two lines, is still refused.
|
||||||
- **An error inside the guard is reported as the command's fault** (MAJOR `161545f6`, `hooks/scripts/pre-bash-executor.mjs`; decision 90). An error inside the guard's own reading is reported as an unquoted opener whose word could not be read, which blames the command for the guard's fault.
|
- **An error inside the guard is reported as the command's fault** (MAJOR `161545f6`, `hooks/scripts/pre-bash-executor.mjs`; decision 90). An error inside the guard's own reading is reported as an unquoted opener whose word could not be read, which blames the command for the guard's fault.
|
||||||
- **The guard re-splits the command for each candidate opener** (MINOR `fe3fe55b`, `hooks/scripts/pre-bash-executor.mjs`; decision 90). For each candidate opener the guard re-splits the command to find its line, quadratic work on a large command.
|
- **The guard re-splits the command for each candidate opener** (MINOR `fe3fe55b`, `hooks/scripts/pre-bash-executor.mjs`; decision 90). For each candidate opener the guard re-splits the command to find its line, quadratic work on a large command.
|
||||||
|
- **The guard keeps its narrow reader** (MINOR `03df3b0b`, `hooks/scripts/pre-bash-executor.mjs`; decision 91). The narrow reader is kept; a full reader that reads the command from start to finish like the shell is deferred (decision 92).
|
||||||
|
- **The guard's test files copy the same helpers** (MINOR `78040bd7`, `tests/hooks/bash-guard-false-stops.test.mjs`; decision 91). The Bash guard's test files copy the same helpers (`ARGS_DELIM`, `ARGS_RULE`, `PARSER_LINE` and others) instead of sharing them, so a change to the block message means editing every copy.
|
||||||
|
- **`argsShellOpeners` keeps an unused default parameter** (MINOR `49873813`, `hooks/scripts/pre-bash-executor.mjs`; decision 91). Its second parameter defaults to `argsEscapeLimit(text)`, and its one caller always passes it.
|
||||||
|
- **A simplify commit has no scope** (MINOR `79402171`, `hooks/scripts/pre-bash-executor.mjs`; decision 91). Part 4's commit `9b80e6c` (`refactor: simplify pass (trekexecute)`) has no scope after its type; it is already pushed, so it stays.
|
||||||
|
- **A grep pattern that puts the delimiter right after `<<` inside a string is refused** (MAJOR `943f9841`, `hooks/scripts/pre-bash-executor.mjs`; fix round 4.5b2 part A, decision 122). A grep pattern that puts the delimiter right after `<<` inside a string, with a line after the command, is refused although the shell reads the `<<` as text; the regex reading does not skip a `<<` the shell reading skips. Fixing it was dropped from part A: each version of the fix that was planned or built let through some command that part 4 blocks and that a nested shell or zsh then runs. A test pins the command as refused (`tests/hooks/bash-guard-4-5b2-a-known.test.mjs`).
|
||||||
|
- **A string whose `<<` follows a quote of the other kind or an escaped quote is refused** (MAJOR `a6735ef9`, `hooks/scripts/pre-bash-executor.mjs`; fix round 4.5b2 part A, decision 122). A string whose `<<` follows a quote of the other kind or an escaped quote (the review's commit message with a second `-m` that spans two lines, and its `echo "x\"y" && grep "<<" …` with a line after it) is refused, because the string test counts one quote character from the line start. Fixing it was dropped from part A: each version of the fix that was planned or built let through some command that part 4 blocks and that a nested shell or zsh then runs. A test pins each command as refused (`tests/hooks/bash-guard-4-5b2-a-known.test.mjs`).
|
||||||
|
- **Other spellings of the heredoc operator for a second shell still pass** (fix round 4.5b2 part A, decision 107). Other spellings that build the parser's heredoc operator or word for a second shell still pass and expand or close early: a quoted `<<` next to the word (`'<<'`), `<` and `<` in two strings, the word held in a variable, an empty quote pair before the word inside a string (decision 125), and a form on one physical line. Only `\<\<` with a line after it is checked (EVL), and it is checked with or without a second shell, so `\<\<` before the word with a line after it is refused even where no shell reads it again.
|
||||||
|
- **The escape scan models default shell options only** (fix round 4.5b2 part A, decision 124). Text before the escaped run that changes how the shell reads quotes, aliases or expansions (`setopt`, `emulate`, `set -o`, `shopt`, an alias) is not followed: for example `shopt -s expand_aliases`, an alias `q` for `bash -c "`, and `q echo x \\\<<…` with a body line and the closing line passes since part A, and `/bin/bash` expands the body (with `set -o posix` too); zsh does not.
|
||||||
|
- **A split opener inside a string or a quoted heredoc's body is refused** (fix round 4.5b2 part A, decision 123, widened by decision 126). A `<<` whose opener a backslash-newline splits is refused inside a string or in the body of a quoted heredoc that passes when a line follows it, also where the shell reads it as text.
|
||||||
|
|
||||||
### Doors
|
### Doors
|
||||||
|
|
||||||
|
|
@ -200,6 +221,7 @@ Fix round 4.5a (decisions 57, 67 and 68): the round's 30 findings, where a Voyag
|
||||||
- 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.
|
- 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.
|
||||||
- two-way: fix round 4.5b part 3 (decision 89) — a revert restores part 2's closing-quote search (a value whose closing quote follows whitespace splits again) and the guard's regex-only opener check.
|
- two-way: fix round 4.5b part 3 (decision 89) — a revert restores part 2's closing-quote search (a value whose closing quote follows whitespace splits again) and the guard's regex-only opener check.
|
||||||
- two-way: fix round 4.5b part 4 (decision 90) — a revert restores part 3's literal-delimiter gate and its refusal of a heredoc operator inside a string or after a backslash; the catch-branch test and the TOK wording go with it.
|
- two-way: fix round 4.5b part 4 (decision 90) — a revert restores part 3's literal-delimiter gate and its refusal of a heredoc operator inside a string or after a backslash; the catch-branch test and the TOK wording go with it.
|
||||||
|
- two-way: fix round 4.5b2 part A (decision 91, decision 107, decision 123) — a revert restores part 4's whole-command escape limits, the exact-word check for a split delimiter, the string and body skips of a split opener and the unchecked twice-escaped operator; the corpus, the end-to-end script, the keep-assertion test and the corrected test header go with it.
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
// expectArgsBlocked, expectArgsAllowed, UNQUOTED and expectUnquoted are copied
|
// expectArgsBlocked, expectArgsAllowed, UNQUOTED and expectUnquoted are copied
|
||||||
// from tests/hooks/bash-guard-args-gate.test.mjs:27-54, not imported, because
|
// from tests/hooks/bash-guard-args-gate.test.mjs:27-54, not imported, because
|
||||||
// importing a test file would run its tests. The guard is only spawned, so
|
// importing a test file would run its tests. The guard is only spawned, so
|
||||||
// this file loads at 12bca8b and its red is an assertion.
|
// this file loads at 591f3c4 and its red is an assertion.
|
||||||
|
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import { strict as assert } from 'node:assert';
|
import { strict as assert } from 'node:assert';
|
||||||
|
|
|
||||||
205
tests/lib/fix-round-4-5b2-a-prose.test.mjs
Normal file
205
tests/lib/fix-round-4-5b2-a-prose.test.mjs
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
// tests/lib/fix-round-4-5b2-a-prose.test.mjs
|
||||||
|
// Prose pins for fix round 4.5b2, part A (plan
|
||||||
|
// .claude/projects/2026-10-31-guard-4-5b2-part-a, decisions 91, 107, 122 and
|
||||||
|
// 123): the fixes that are text a session reads. The tag in brackets is the
|
||||||
|
// part-A id from the round's plan; each tagged test fails on an assertion at
|
||||||
|
// e2d6bec, part 4's end point. The keep-behaviour checks sit inside the tagged
|
||||||
|
// test as "Control:" assertions.
|
||||||
|
// KIA the CHANGELOG lists part A's fixes (RXT, CNT, SFX, ESR, EVL, SIS, HDR)
|
||||||
|
// under ### Fixed, brings part 4's CSG, SLT and ESC bullets in line,
|
||||||
|
// lists the two MAJOR findings decision 122 keeps, the four MINOR and
|
||||||
|
// what part A leaves open under ### Known issues, and its door under
|
||||||
|
// ### Doors.
|
||||||
|
// HDR tests/hooks/bash-guard-false-stops.test.mjs names its red base
|
||||||
|
// 591f3c4. In the red clone this test passes at e2d6bec, because the
|
||||||
|
// working tree's tests/ is copied in; its red is shown under a named
|
||||||
|
// mutation that writes the old header back.
|
||||||
|
// No static import of a new export: the file must load at e2d6bec.
|
||||||
|
// Fix the SOURCE a pin guards, not the pin.
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||||
|
|
||||||
|
const readCache = new Map();
|
||||||
|
|
||||||
|
/** A repo file's text; the file must exist. Read from disk once per run. */
|
||||||
|
function read(rel) {
|
||||||
|
if (!readCache.has(rel)) {
|
||||||
|
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
|
||||||
|
readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8'));
|
||||||
|
}
|
||||||
|
return readCache.get(rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whitespace (line breaks too) collapsed to one space. */
|
||||||
|
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
// --- KIA: the round's CHANGELOG entry ----------------------------------------------
|
||||||
|
// Part A stands in the same Unreleased section as parts 1 to 4. The section is
|
||||||
|
// found by its title, by the rule tests/lib/fix-round-4-5b-part-4-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part A's seven Fixed bullets, each with the ref it must name: the four review
|
||||||
|
// findings of part 4's review it fixes, the two operator decisions and the
|
||||||
|
// false test header. The ids are the review's, written out here.
|
||||||
|
const FIXED = [
|
||||||
|
['(RXT)', '5f1cc34c'], ['(CNT)', '97d1f547'], ['(SFX)', '27b0b46a'], ['(ESR)', 'ed40abdb'],
|
||||||
|
['(EVL)', 'decision 107'], ['(SIS)', 'decision 123'], ['(HDR)', '86488cd6'],
|
||||||
|
];
|
||||||
|
const FIXED_IDS = ['5f1cc34c', '97d1f547', '27b0b46a', 'ed40abdb'];
|
||||||
|
const NEW_TAGS = FIXED.map(([tag]) => tag);
|
||||||
|
// The two MAJOR findings decision 122 keeps open.
|
||||||
|
const KEPT_IDS = ['943f9841', 'a6735ef9'];
|
||||||
|
// The review's four MINOR findings, listed as open under decision 91.
|
||||||
|
const MINOR_IDS = ['03df3b0b', '78040bd7', '49873813', '79402171'];
|
||||||
|
|
||||||
|
const DELIM = 'VOYAGE_ARGS_EOF_7F3C9E';
|
||||||
|
const TWO_ESCAPED = 'two escaped `<` in a row';
|
||||||
|
|
||||||
|
test('[KIA] the CHANGELOG lists part A\'s seven fixes under ### Fixed, each with its ref', () => {
|
||||||
|
const fixed = changelogBullets('### Fixed');
|
||||||
|
|
||||||
|
// Red: one ### Fixed bullet per part-A tag, each naming its ref.
|
||||||
|
for (const [tag, ref] of FIXED) {
|
||||||
|
const hits = fixed.filter((b) => b.includes(tag) && b.includes(ref));
|
||||||
|
assert.equal(hits.length, 1, `### Fixed must hold exactly one bullet marked ${tag} naming ${ref}; found ${hits.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control: no fixed finding is listed as open.
|
||||||
|
const known = changelogBullets('### Known issues');
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[KIA] the two MAJOR findings decision 122 keeps stand under ### Known issues, one bullet each', () => {
|
||||||
|
const known = changelogBullets('### Known issues');
|
||||||
|
|
||||||
|
// Red: each kept finding has exactly one Known-issues bullet, and that bullet names decision 122.
|
||||||
|
const bullets = [];
|
||||||
|
for (const id of KEPT_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}`);
|
||||||
|
assert.ok(hits[0].includes('decision 122'), `the Known-issues bullet naming ${id} does not say decision 122: ${hits[0]}`);
|
||||||
|
bullets.push(hits[0]);
|
||||||
|
}
|
||||||
|
assert.notEqual(bullets[0], bullets[1], `${KEPT_IDS[0]} and ${KEPT_IDS[1]} must stand in two different Known-issues bullets`);
|
||||||
|
|
||||||
|
// Control: neither kept finding stands in a Fixed bullet marked with one of part A's tags.
|
||||||
|
const fixed = changelogBullets('### Fixed');
|
||||||
|
for (const id of KEPT_IDS) {
|
||||||
|
const hits = fixed.filter((b) => b.includes(id) && NEW_TAGS.some((tag) => b.includes(tag)));
|
||||||
|
assert.deepEqual(hits, [], `Control: ${id} must not stand in a ### Fixed bullet marked with one of ${NEW_TAGS.join(' ')}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[KIA] part 4\'s SLT bullet no longer promises a pass the guard refuses, and names the SIS exception', () => {
|
||||||
|
const slt = changelogBullets('### Fixed').filter((b) => b.includes('(SLT)'));
|
||||||
|
assert.equal(slt.length, 1, `### Fixed must hold exactly one bullet marked (SLT); found ${slt.length}`);
|
||||||
|
|
||||||
|
// Red: the old promise is gone.
|
||||||
|
assert.ok(!slt[0].includes('pass again'), `the (SLT) bullet must not hold "pass again": ${slt[0]}`);
|
||||||
|
|
||||||
|
// It names the gaps of part 4's review, what is refused as before, and the SIS exception.
|
||||||
|
for (const part of [...KEPT_IDS, 'refused as before', 'unless a backslash-newline splits']) {
|
||||||
|
assert.ok(slt[0].includes(part), `the (SLT) bullet must hold ${JSON.stringify(part)}: ${slt[0]}`);
|
||||||
|
}
|
||||||
|
assert.ok(!slt[0].includes('is text and passes, so'), `the (SLT) bullet must not hold "is text and passes, so": ${slt[0]}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[KIA] part 4\'s CSG and ESC bullets name the gaps part A fixes, ESC with the EVL exception', () => {
|
||||||
|
const fixed = changelogBullets('### Fixed');
|
||||||
|
|
||||||
|
// Red: the CSG bullet names the split-delimiter gap.
|
||||||
|
const csg = fixed.filter((b) => b.includes('(CSG)'));
|
||||||
|
assert.equal(csg.length, 1, `### Fixed must hold exactly one bullet marked (CSG); found ${csg.length}`);
|
||||||
|
assert.ok(csg[0].includes('27b0b46a'), `the (CSG) bullet must name review finding 27b0b46a: ${csg[0]}`);
|
||||||
|
|
||||||
|
// The ESC bullet names the whole-command limits gap and the EVL exception.
|
||||||
|
const esc = fixed.filter((b) => b.includes('(ESC)'));
|
||||||
|
assert.equal(esc.length, 1, `### Fixed must hold exactly one bullet marked (ESC); found ${esc.length}`);
|
||||||
|
for (const part of ['ed40abdb', 'still open before the run', TWO_ESCAPED]) {
|
||||||
|
assert.ok(esc[0].includes(part), `the (ESC) bullet must hold ${JSON.stringify(part)}: ${esc[0]}`);
|
||||||
|
}
|
||||||
|
for (const gone of ['no quote or heredoc operator before it', 'stands before the run']) {
|
||||||
|
assert.ok(!esc[0].includes(gone), `the (ESC) bullet must not hold ${JSON.stringify(gone)}: ${esc[0]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[KIA] ### Known issues lists the four MINOR, decisions 107, 123 and 124, and part 2\'s bullet with its exceptions', () => {
|
||||||
|
const known = changelogBullets('### Known issues');
|
||||||
|
|
||||||
|
// Red: each MINOR finding has exactly one Known-issues bullet, and that bullet names decision 91.
|
||||||
|
for (const id of MINOR_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}`);
|
||||||
|
assert.ok(hits[0].includes('decision 91'), `the Known-issues bullet naming ${id} does not say decision 91: ${hits[0]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What part A leaves open: one bullet per decision.
|
||||||
|
const d107 = known.filter((b) => b.includes('decision 107'));
|
||||||
|
assert.equal(d107.length, 1, `### Known issues must hold exactly one bullet naming decision 107; found ${d107.length}`);
|
||||||
|
const d124 = known.filter((b) => b.includes('decision 124'));
|
||||||
|
assert.equal(d124.length, 1, `### Known issues must hold exactly one bullet naming decision 124; found ${d124.length}`);
|
||||||
|
for (const part of ['setopt', 'emulate', 'set -o', 'shopt', 'alias']) {
|
||||||
|
assert.ok(d124[0].includes(part), `the decision 124 Known-issues bullet must hold ${JSON.stringify(part)}: ${d124[0]}`);
|
||||||
|
}
|
||||||
|
const d123 = known.filter((b) => b.includes('decision 123'));
|
||||||
|
assert.equal(d123.length, 1, `### Known issues must hold exactly one bullet naming decision 123; found ${d123.length}`);
|
||||||
|
|
||||||
|
// Part 2's bullet keeps its pair and names the exceptions part A widens.
|
||||||
|
const followed = known.filter((b) => b.includes(DELIM) && b.includes('stays blocked'));
|
||||||
|
assert.equal(followed.length, 1, `### Known issues must hold one bullet holding ${DELIM} and "stays blocked"; found ${followed.length}`);
|
||||||
|
for (const part of ['fix round 4.5b2 part A', TWO_ESCAPED]) {
|
||||||
|
assert.ok(followed[0].includes(part), `part 2's Known-issues bullet must hold ${JSON.stringify(part)}: ${followed[0]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[KIA] ### Doors holds one line for decision 91', () => {
|
||||||
|
const doors = changelogBullets('### Doors').filter((b) => b.includes('decision 91'));
|
||||||
|
assert.equal(doors.length, 1, `### Doors must hold exactly one bullet naming decision 91; found ${doors.length}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- HDR: the false-stops test header -----------------------------------------------
|
||||||
|
|
||||||
|
test('[HDR] tests/hooks/bash-guard-false-stops.test.mjs names its red base 591f3c4', () => {
|
||||||
|
const text = read('tests/hooks/bash-guard-false-stops.test.mjs');
|
||||||
|
assert.ok(!text.includes('loads at 12bca8b'), 'tests/hooks/bash-guard-false-stops.test.mjs must not hold "loads at 12bca8b"');
|
||||||
|
const hits = text.split('loads at 591f3c4').length - 1;
|
||||||
|
assert.equal(hits, 1, `tests/hooks/bash-guard-false-stops.test.mjs must hold "loads at 591f3c4" exactly once; found ${hits}`);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue