// tests/lib/review-630.test.mjs // The findings of the release review of 6.3.0 (2026-10-10), each held by a // test that failed on f669b87. The guard's and the release check's own tests // live next to their files (tests/commands/trekexecute-coupling.test.mjs, // tests/hooks/guards-review-regressions.test.mjs, tests/scripts/release-check.test.mjs). import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawnSync } from 'node:child_process'; import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { buildPlan } from '../helpers/plan-v18-builder.mjs'; import { validatePlanContent } from '../../lib/validators/plan-validator.mjs'; import { stepTestsOnly } from '../../lib/parsers/plan-schema.mjs'; import { validateFindings } from '../../lib/review/findings-schema.mjs'; import { lintSkill } from '../../lib/skills/skill-lint.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const read = (rel) => readFileSync(join(ROOT, rel), 'utf8'); const VALIDATOR = join(ROOT, 'lib', 'validators', 'plan-validator.mjs'); // The review template's own Step R1, appended the Phase 7.2 way. function withTemplateStep(plan, n) { const r1 = /### Step R1:[^\n]*\n[\s\S]*?(?=\n> )/.exec(read('templates/trekreview-template.md')); assert.ok(r1, 'templates/trekreview-template.md carries a ### Step R1'); // Phase 7.2 step 2 also adds the manifest the legacy rule synthesizes (expected_paths ← Files). const appended = r1[0].replace(/^### Step R1:/, `### Step ${n}:`) + '- **Checkpoint:** `git commit -m "fix(review): x [01234567]"`\n' + '- **Manifest:**\n ```yaml\n manifest:\n expected_paths:\n - lib/foo.mjs\n min_file_count: 1\n' + ' commit_message_pattern: "^fix\\\\(review\\\\):"\n bash_syntax_check: []\n forbidden_paths: []\n must_contain: []\n ```\n'; return plan.replace('\n## Test Strategy', `\n${appended}\n## Test Strategy`); } // ---- BLOCKER 1b25ba56: appended review steps hold the 1.8 step rules on any plan test('review 630 / 1b25ba56: on a 1.7 plan, an appended review step that cannot run is refused (--steps-from)', () => { const dir = mkdtempSync(join(tmpdir(), 'r630-')); try { const plan = withTemplateStep(buildPlan({ version: '1.7', design: false }), 3); const p = join(dir, 'plan.md'); writeFileSync(p, plan); const plain = spawnSync(process.execPath, [VALIDATOR, '--strict', '--json', p], { encoding: 'utf8' }); assert.equal(plain.status, 0, 'the control: a 1.7 plan is not held to the 1.8 step rules'); const r = spawnSync(process.execPath, [VALIDATOR, '--strict', '--json', '--steps-from', '3', p], { encoding: 'utf8' }); assert.equal(r.status, 1, `the appended step is held to them: ${r.stdout}`); const codes = JSON.parse(r.stdout).errors.map((e) => e.code); assert.ok(codes.includes('PLAN_STEP_VERIFY_NOT_RUNNABLE') || codes.includes('PLAN_STEP_NO_TEST_FIRST'), codes.join(',')); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('review 630 / 1b25ba56: --steps-from leaves the earlier steps of a 1.7 plan alone', () => { const plan = buildPlan({ version: '1.7', design: false, steps: [{ testFirst: false }, { endToEnd: 'bash tests/e2e.sh' }] }); assert.deepEqual(validatePlanContent(plan, { strict: true, stepsFrom: 3 }).errors, [], 'step 1 has no Test first, and it is not one of the appended steps'); }); test('review 630 / 1b25ba56: Phase 7.2 validates the appended steps with --steps-from', () => { const cmd = read('commands/trekexecute.md'); assert.match(cmd, /plan-validator\.mjs --strict --json --steps-from \{total \+ 1\} "\{plan_path\}"/); }); // ---- 57a5e67c: the wave path names the guard's refusal ------------------------ test('review 630 / 57a5e67c: the wave merge path says what to do when the guard refused because no one can be asked', () => { const cmd = read('commands/trekexecute.md'); const at = cmd.indexOf('**Merge session branches back (SEQUENTIAL, one at a time):**'); const before = cmd.slice(cmd.lastIndexOf('- **`all_completed: true`:**', at), at); assert.match(before, /no one to ask/, 'the guard-refused case is named on the wave path'); assert.match(before, /by hand/, 'and the way on: the operator runs the merge by hand'); assert.match(before, /wave\.json/, 'wave.json is kept'); }); // ---- 2cf29d6f / 727924c0: what counts as a step of only tests ------------------ test('review 630: a comma inside a Files: note is not a path', () => { const body = '- **Files:** `tests/a.test.mjs` (new: covers x, y)\n- **Changes:** add the test\n'; assert.deepEqual(stepTestsOnly(body), { testsOnly: true, files: ['tests/a.test.mjs'] }); }); test('review 630: a data file under specs/ is not a test', () => { const body = '- **Files:** `api/specs/openapi.yaml`\n- **Changes:** add the endpoint\n'; assert.equal(stepTestsOnly(body).testsOnly, false); const fixture = '- **Files:** `tests/fixtures/users.json`\n- **Changes:** add the fixture\n'; assert.equal(stepTestsOnly(fixture).testsOnly, true, 'the control: a fixture under tests/ is a test file'); }); test('review 630: a tests-only End-to-end step is told where End-to-end belongs', () => { const plan = buildPlan({ steps: [{}, { file: 'tests/e2e.sh', test: 'tests/e2e.sh', endToEnd: 'bash tests/e2e.sh' }] }); const w = validatePlanContent(plan, { strict: true }).warnings.find((x) => x.code === 'PLAN_STEP_TESTS_ONLY'); assert.ok(w, 'the warning fires: red proof would reject this step'); assert.match(`${w.message} ${w.hint ?? ''}`, /`End-to-end:` (rides|belongs) on the last step that builds/); assert.match(read('templates/plan-template.md'), /`End-to-end:` (rides|belongs) on the last step that builds/); }); // ---- c4623362: a removed line can still be cited -------------------------------- test('review 630 / c4623362: the out-of-range hint says how to cite a line that no longer exists', () => { const r = validateFindings({ reviewer: 'r', findings: [{ id: 'a'.repeat(40), severity: 'MAJOR', rule_key: 'MISSING_TEST', file: 'package.json', line: 1, brief_ref: 'x', title: 't', recommended_action: 'a', detail: 'the check formerly at package.json:9999 was removed', }] }, { repoRoot: ROOT }); const e = r.errors.find((x) => x.code === 'FINDING_PROSE_LINE_OUT_OF_RANGE'); assert.ok(e, 'the control: the plain citation is still checked'); assert.match(e.hint, /::/); const ok = validateFindings({ reviewer: 'r', findings: [{ id: 'a'.repeat(40), severity: 'MAJOR', rule_key: 'MISSING_TEST', file: 'package.json', line: 1, brief_ref: 'x', title: 't', recommended_action: 'a', detail: 'the check formerly at v6.2.0:package.json:9999 was removed', }] }, { repoRoot: ROOT }); assert.deepEqual(ok.errors, [], 'a revision-qualified citation is not judged against today\'s file'); assert.match(read('commands/trekreview.md'), /::/, 'the re-ask names the form'); }); // ---- 2261016d: a 499-line SKILL.md is under the 500-line limit ------------------- test('review 630 / 2261016d: a SKILL.md of 499 lines ending in a newline passes the size rule', () => { const dir = mkdtempSync(join(tmpdir(), 'r630-skill-')); try { const skill = join(dir, 'test-first'); cpSync(join(ROOT, 'skills', 'test-first'), skill, { recursive: true }); const text = readFileSync(join(skill, 'SKILL.md'), 'utf8').replace(/\n+$/, '\n'); const lines = text.split('\n').length - 1; assert.ok(lines < 499, `the fixture pads the real skill (${lines} lines)`); const padded = text.replace('\n## Credits', `${'\n'.repeat(499 - lines)}\n## Credits`); assert.equal(padded.split('\n').length - 1, 499); writeFileSync(join(skill, 'SKILL.md'), padded); const codes = lintSkill(skill).errors.map((e) => e.code); assert.ok(!codes.includes('SKILL_TOO_LONG'), codes.join(',')); writeFileSync(join(skill, 'SKILL.md'), padded + '\n'); assert.ok(lintSkill(skill).errors.map((e) => e.code).includes('SKILL_TOO_LONG'), 'the control: 500 lines fail'); } finally { rmSync(dir, { recursive: true, force: true }); } }); // ---- cb34c32e: regression-check takes commandEnv from the shared util ------------ test('review 630 / cb34c32e: regression-check imports commandEnv from lib/util/command-env.mjs', () => { const src = read('lib/verification/regression-check.mjs'); assert.match(src, /import \{ commandEnv \} from '\.\.\/util\/command-env\.mjs';/); assert.doesNotMatch(src, /from '\.\/red-proof\.mjs'/); assert.ok(src.indexOf('function exec(') > src.lastIndexOf('\nimport '), 'exec is defined after the imports'); }); // ---- CLAUDE.md names the mode-read exemptions as they are ----------------------- test('review 630: CLAUDE.md counts the mode-read exemptions the launch test holds', () => { const src = read('tests/lib/no-claude-code-launch.test.mjs'); const n = (src.match(/\['mode-read', \d+,/g) || []).length; assert.equal(n, 4, 'the launch test holds four mode-read entries'); assert.match(read('CLAUDE.md'), /four files that only read\s+(?:> )?the session's permission mode/); }); // ---- /security-review, older than v6.2.0: a read-only git that writes a file ---- test('review 630 / security: `git diff --output=` is not a read-only check', async () => { const { allowedCommand } = await import('../../lib/verification/criteria-runner.mjs'); assert.equal(allowedCommand('git diff --stat').allowed, true, 'the control: a read-only git check runs'); assert.equal(allowedCommand('git log --output-indicator-new=+ -1').allowed, true, 'an option that only starts with --output is not it'); for (const cmd of ['git diff --output=../../.zshrc', 'git diff --output ../x', 'git log -p --output=x', 'git show HEAD --output=x']) { const r = allowedCommand(cmd); assert.equal(r.allowed, false, `${cmd} writes a file`); assert.match(r.reason, /--output/); } });