Round 3 (v6.2.0..96cb977, BLOCK: 1 BLOCKER, 8 MAJOR, 2 MINOR), each fixed behind a test that failed first. - Every allowlisted runner, not only git, takes plain words and a short list of options that only read: an option that writes a file or loads code (--test-reporter-destination, --outputFile, --junitxml, --basetemp, --import, --config, -p, -o, -exec, a reporter path) is on no list; after `npm test --` only paths follow; `make test` takes no VAR=value. A built-in node test reporter (tap, spec, dot, junit, lcov) stays allowed. - /trekexecute: a restore the guard refuses (destructive git, in the mode that shows no prompts) or the operator declines is never recorded as reverted: simplify = "revert-refused" and the run stops before review; a refused step revert is reported with its paths. - stepTestsOnly: a fixture document under tests/ is a test file, "e.g." is not a path, ./ is dropped; the plan-file sweep reads Files: with the same reader (fieldPaths); the validator hands its parsed fields over. - plan-validator --steps-from asks for Verify: on a 1.8 plan too. - skill frontmatter: a value starting with # is a hazard; a quote after an escaped backslash closes the value. Routing reads kind tags with hasOwn. - release-check reads scope_sha_end as written (an all-digit sha keeps its leading zero), and Voyage's own artifacts under .claude/projects/ and .claude/voyage/ committed after the review do not count as unreviewed. Suite 2170 (2168/0/2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
380 lines
23 KiB
JavaScript
380 lines
23 KiB
JavaScript
// 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';
|
|
import * as routing from '../../lib/skills/routing.mjs';
|
|
|
|
const awaitImportSync = () => routing;
|
|
|
|
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, /<rev>:<path>:<line>/);
|
|
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'), /<rev>:<path>:<line>/, '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=<path>` 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/);
|
|
}
|
|
});
|
|
|
|
// ==== round 2 of the release review (2026-10-10), each red on 0d044f6 ===========
|
|
|
|
test('review 630 r2 / BLOCKER: a quoted, escaped or expanded --output is refused too', async () => {
|
|
const { allowedCommand } = await import('../../lib/verification/criteria-runner.mjs');
|
|
for (const cmd of [
|
|
'git diff "--output=/tmp/x"', "git log -p '--output'=x", "git diff '--output=q1'", 'git log "--output" q1',
|
|
'git diff --output\\=x', 'git diff --\\output=q1', 'git diff --out{put,x}=y', 'git diff --outp$EMPTY=x',
|
|
'git diff --outp?t=x', 'git diff --outpu=x', 'git log --out x',
|
|
]) {
|
|
assert.equal(allowedCommand(cmd).allowed, false, `${cmd} must not reach the shell`);
|
|
}
|
|
for (const cmd of ['git log --oneline -1', 'git diff --stat HEAD~1', 'git show HEAD^:package.json', 'git log --format=%h -1',
|
|
'git ls-files tests/', 'git log --output-indicator-new=+ -1']) {
|
|
assert.equal(allowedCommand(cmd).allowed, true, `the control: ${cmd} is a read-only check`);
|
|
}
|
|
});
|
|
|
|
test('review 630 r2: a Files: entry without backticks names every path in it', () => {
|
|
const body = '- **Files:** tests/a.test.mjs + src/b.mjs\n- **Changes:** x\n';
|
|
assert.deepEqual(stepTestsOnly(body), { testsOnly: false, files: ['tests/a.test.mjs', 'src/b.mjs'] });
|
|
const and = '- **Files:** tests/a.test.mjs and src/b.mjs\n- **Changes:** x\n';
|
|
assert.equal(stepTestsOnly(and).testsOnly, false);
|
|
const plain = '- **Files:** Makefile\n- **Changes:** x\n';
|
|
assert.deepEqual(stepTestsOnly(plain).files, ['Makefile'], 'the control: a bare file name is still a path');
|
|
});
|
|
|
|
test('review 630 r2: a backticked path with parentheses keeps them', () => {
|
|
const body = '- **Files:** `app/(auth)/login.check.sh` (new)\n- **Changes:** x\n' +
|
|
'- **Test first:**\n - File: `app/(auth)/login.check.sh` (new)\n - Verifies: y\n';
|
|
assert.deepEqual(stepTestsOnly(body), { testsOnly: true, files: ['app/(auth)/login.check.sh'] });
|
|
});
|
|
|
|
test('review 630 r2: --steps-from reads the plan\'s ## Verification for an appended End-to-end step', () => {
|
|
const plan = buildPlan({ version: '1.7', design: false });
|
|
const e2e = '### Step 3: Run it\n\n- **Files:** `src/run.mjs`\n- **Changes:** wire it\n' +
|
|
'- **Test first:**\n - File: `tests/run.test.mjs` (new)\n - Verifies: it runs\n' +
|
|
'- **Verify:** `node --test tests/run.test.mjs` → expected: exit 0\n' +
|
|
'- **End-to-end:** `bash tests/e2e.sh` → expected: the CLI prints ok\n' +
|
|
'- **Manifest:**\n ```yaml\n manifest:\n expected_paths:\n - src/run.mjs\n min_file_count: 1\n' +
|
|
' commit_message_pattern: "^fix"\n bash_syntax_check: []\n forbidden_paths: []\n must_contain: []\n ```\n';
|
|
const edited = plan.replace('\n## Test Strategy', `\n${e2e}\n## Test Strategy`);
|
|
assert.match(edited, /- \[ \] `bash tests\/e2e\.sh`/, 'the fixture lists the command under ## Verification');
|
|
const r = validatePlanContent(edited, { strict: true, stepsFrom: 3 });
|
|
assert.deepEqual(r.errors.map((e) => e.code), [], JSON.stringify(r.errors));
|
|
});
|
|
|
|
test('review 630 r2: --soft --steps-from warns instead of failing', () => {
|
|
const plan = withTemplateStep(buildPlan({ version: '1.7', design: false }), 3);
|
|
const soft = validatePlanContent(plan, { strict: false, stepsFrom: 3 });
|
|
assert.equal(soft.valid, true, JSON.stringify(soft.errors));
|
|
assert.ok(soft.warnings.some((w) => /VERIFY_NOT_RUNNABLE|TEST_FIRST/.test(w.code)), 'the issue is still reported');
|
|
assert.equal(validatePlanContent(plan, { strict: true, stepsFrom: 3 }).valid, false, 'the control: strict fails');
|
|
});
|
|
|
|
test('review 630 r2: regression-check prints the status progress.json takes', async () => {
|
|
const { checkRegression } = await import('../../lib/verification/regression-check.mjs');
|
|
const { PROGRESS_REGRESSION_VALUES } = await import('../../lib/validators/progress-validator.mjs');
|
|
const plan = '## Implementation Plan\n\n### Step 1: x\n\n- **Regression:** `npm test`\n';
|
|
const blocked = checkRegression({ planText: plan, step: 1, run: () => [{ status: 'blocked', command: 'npm test' }] });
|
|
assert.equal(blocked.progress_status, 'unrunnable');
|
|
const missing = checkRegression({ planText: plan, step: 9 });
|
|
assert.equal(missing.progress_status, 'not-run');
|
|
for (const r of [blocked, missing]) assert.ok(PROGRESS_REGRESSION_VALUES.includes(r.progress_status));
|
|
assert.match(read('commands/trekexecute.md'), /`progress_status`/, 'the executor writes that field');
|
|
});
|
|
|
|
test('review 630 r2: a skill is found by its name even when an agent has the same name', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'r630-route-'));
|
|
try {
|
|
for (const d of ['skills', 'commands', 'agents', 'evals']) cpSync(join(ROOT, d), join(dir, d), { recursive: true });
|
|
writeFileSync(join(dir, 'agents', 'debugging.md'),
|
|
'---\nname: debugging\ndescription: Bakes sourdough loaves and schedules oven time for the shared kitchen rota.\nmodel: opus\n---\n\nBread.\n');
|
|
const { checkRouting } = awaitImportSync();
|
|
const codes = checkRouting(dir).errors.map((e) => e.code);
|
|
assert.ok(!codes.includes('ROUTING_UNKNOWN_SKILL'), codes.join(','));
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('review 630 r2: rank() does not rebuild the document vectors per call', () => {
|
|
const { buildCorpus, rank } = awaitImportSync();
|
|
const corpus = buildCorpus([
|
|
{ id: 'a', name: 'alpha', description: 'parses plans and validates steps' },
|
|
{ id: 'b', name: 'beta', description: 'runs tests and reports failures' },
|
|
]);
|
|
let calls = 0;
|
|
const idf = corpus.idf;
|
|
corpus.idf = (t) => { calls++; return idf(t); };
|
|
rank('validates plans', corpus);
|
|
rank('reports failures', corpus);
|
|
assert.ok(calls <= 4, `idf was called ${calls} times for two two-word prompts`);
|
|
});
|
|
|
|
test('review 630 r2 (operator 2026-10-10): Phase 7.2 says plainly that template-form steps stop with a re-plan', () => {
|
|
const cmd = read('commands/trekexecute.md');
|
|
assert.match(cmd, /Today the loop runs only for steps written as\s+runnable plan steps/);
|
|
assert.match(cmd, /re-plan from the review" — the\s+usual outcome until a later order/);
|
|
});
|
|
|
|
// ==== round 3 of the release review (2026-10-10), each red on 96cb977 ===========
|
|
|
|
test('review 630 r3 / BLOCKER: no runner takes an option that writes a file or loads code', async () => {
|
|
const { allowedCommand } = await import('../../lib/verification/criteria-runner.mjs');
|
|
for (const cmd of [
|
|
'node --test --test-reporter=tap --test-reporter-destination=/tmp/x',
|
|
'node --test --import=data:text/javascript,1',
|
|
'node --test "--import=data:text/javascript,import(\'node:fs\')"',
|
|
'node --test --require ./evil.cjs',
|
|
'node --test --test-reporter=./evil-reporter.mjs',
|
|
'node --test --test-reporter ./evil-reporter.mjs',
|
|
'jest --json --outputFile=/tmp/x',
|
|
'jest --config=evil.js',
|
|
'vitest --outputFile=/tmp/x',
|
|
'vitest run --reporter=./evil.mjs',
|
|
'pytest --junitxml=/tmp/x',
|
|
'pytest --basetemp=/tmp/anydir',
|
|
'pytest -p evil_plugin',
|
|
'python -m pytest --junitxml=/tmp/x',
|
|
'uv run pytest --basetemp=/tmp/x',
|
|
'go test -o /tmp/x',
|
|
'go test -exec /tmp/evil ./...',
|
|
'cargo test --target-dir=/tmp/x',
|
|
'npm test -- --test-reporter-destination=/tmp/x',
|
|
'npm test --script-shell=/tmp/evil',
|
|
'npm run verify -- --outputFile=/tmp/x',
|
|
'make test SHELL=/tmp/evil',
|
|
'make test -f /tmp/evil.mk',
|
|
'bash tests/x.sh "$(id)"',
|
|
]) {
|
|
assert.equal(allowedCommand(cmd, { cwd: ROOT }).allowed, false, `${cmd} must not reach the shell`);
|
|
}
|
|
for (const cmd of [
|
|
'npm test', 'npm test -- tests/lib/criteria-runner.test.mjs', 'node --test tests/lib/criteria-runner.test.mjs',
|
|
'node --test tests/lib/*.test.mjs', 'node --test --test-reporter=tap tests/lib/review-630.test.mjs', 'node --test --test-reporter dot', 'node --test --test-name-pattern=review tests/lib/review-630.test.mjs',
|
|
'vitest run', 'jest --ci', 'pytest -q', 'pytest -x -q tests/', 'python3 -m pytest tests/', 'uv run pytest',
|
|
'cargo test', 'cargo test --release', 'go test ./...', 'go test -run TestX -count=1 ./pkg', 'make test',
|
|
'bash tests/fixtures/criteria-exit-0.sh', 'bash tests/fixtures/criteria-exit-0.sh --fast',
|
|
]) {
|
|
const r = allowedCommand(cmd, { cwd: ROOT });
|
|
assert.equal(r.allowed, true, `the control: ${cmd} is a test run (${r.reason})`);
|
|
}
|
|
});
|
|
|
|
test('review 630 r3: a restore the guard refuses is never recorded as reverted', async () => {
|
|
const { PROGRESS_SIMPLIFY_VALUES } = await import('../../lib/validators/progress-validator.mjs');
|
|
assert.ok(PROGRESS_SIMPLIFY_VALUES.includes('revert-refused'), 'progress.json can say the restore was refused');
|
|
assert.match(read('docs/HANDOVER-CONTRACTS.md'), /`applied \\| clean \\| reverted \\| revert-refused \\| not-run`/);
|
|
const cmd = read('commands/trekexecute.md');
|
|
const onFailure = cmd.slice(cmd.indexOf('**`On failure: revert`** and **`On failure: retry`**'), cmd.indexOf('**`On failure: skip`**'));
|
|
assert.match(onFailure, /guard refuses the restore/, 'the step revert says what happens when the guard refuses it');
|
|
const simplify = cmd.slice(cmd.indexOf('2. **`/simplify`**'), cmd.indexOf('3. **`/trekreview`**'));
|
|
assert.match(simplify, /simplify = "revert-refused"/);
|
|
assert.match(simplify, /never `"reverted"`/);
|
|
});
|
|
|
|
test('review 630 r3: a fixture document under tests/ is a test file, and "e.g." is not a path', () => {
|
|
const step = (files) => `- **Files:** ${files}\n- **Changes:** x\n`;
|
|
assert.equal(stepTestsOnly(step('`tests/a.test.mjs`, `tests/fixtures/sample.md`')).testsOnly, true);
|
|
assert.deepEqual(stepTestsOnly(step('tests/a.test.mjs (new), e.g. the fixture tests/fixtures/x.json')),
|
|
{ testsOnly: true, files: ['tests/a.test.mjs', 'tests/fixtures/x.json'] });
|
|
assert.equal(stepTestsOnly(step('`docs/specs/api.md`')).testsOnly, false, 'a document outside tests/ is still not a test');
|
|
assert.deepEqual(stepTestsOnly(step('`./tests/a.test.mjs`')).files, ['tests/a.test.mjs'], './ is stripped');
|
|
});
|
|
|
|
test('review 630 r3: the plan-file sweep reads Files: with the same reader as the tests-only check', async () => {
|
|
const { plannedPaths } = await import('../../lib/verification/plan-scope-sweep.mjs');
|
|
const plan = '## Implementation Plan\n\n### Step 1: x\n\n- **Files:** `./src/a.mjs`, tests/b.test.mjs + src/c.mjs\n- **Changes:** x\n';
|
|
const body = plan.slice(plan.indexOf('### Step 1'));
|
|
assert.deepEqual(plannedPaths(plan), [...stepTestsOnly(body).files].sort());
|
|
});
|
|
|
|
test('review 630 r3: --steps-from refuses an appended step with no Verify: on a 1.8 plan too', () => {
|
|
const plan = buildPlan({});
|
|
const step = '### Step 3: Fix it\n\n- **Files:** `src/fix.mjs`, `tests/fix.test.mjs`\n- **Changes:** fix it\n' +
|
|
'- **Test first:**\n - File: `tests/fix.test.mjs` (new)\n - Verifies: it is fixed\n- **On failure:** escalate\n';
|
|
const edited = plan.replace('\n## Test Strategy', `\n${step}\n## Test Strategy`);
|
|
assert.ok(edited.includes('### Step 3: Fix it'), 'the fixture appends the step');
|
|
const r = validatePlanContent(edited, { strict: true, stepsFrom: 3 });
|
|
assert.ok(r.errors.some((e) => e.code === 'PLAN_STEP_VERIFY_NOT_RUNNABLE' && /Step 3/.test(e.message)),
|
|
JSON.stringify(r.errors.map((e) => e.code)));
|
|
});
|
|
|
|
test('review 630 r3: the plan validator hands its parsed fields to the tests-only check', async () => {
|
|
const schema = await import('../../lib/parsers/plan-schema.mjs');
|
|
assert.equal(typeof schema.stepTestsOnlyFromFields, 'function');
|
|
const body = '- **Files:** `tests/a.test.mjs`\n- **Changes:** x\n';
|
|
assert.deepEqual(schema.stepTestsOnlyFromFields(schema.parseStepFields(body)), stepTestsOnly(body));
|
|
assert.match(read('lib/validators/plan-validator.mjs'), /stepTestsOnlyFromFields\(f\)/);
|
|
});
|
|
|
|
test('review 630 r3: skill frontmatter - a value starting with # is a hazard, an escaped backslash closes a quote', async () => {
|
|
const { plainScalarHazards, readFrontmatterFields } = await import('../../lib/skills/skill-frontmatter.mjs');
|
|
assert.ok(plainScalarHazards('#1 helper. Use when testing.').length > 0, 'YAML reads "#..." as a comment: the value is null');
|
|
const r = readFrontmatterFields('---\nname: x\ndescription: "Path C:\\\\"\n---\nbody\n');
|
|
assert.equal(r.fields.description?.value, 'Path C:\\', JSON.stringify(r));
|
|
const open = readFrontmatterFields('---\nname: x\ndescription: "a \\"\n---\nbody\n');
|
|
assert.ok(JSON.stringify(open).includes('unclosed double quote'), 'a quote escaped by one backslash is still open');
|
|
});
|
|
|
|
test('review 630 r3: routing reads a kind tag only from its own table, never Object.prototype', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'r630-proto-'));
|
|
try {
|
|
for (const d of ['skills', 'commands', 'agents', 'evals']) cpSync(join(ROOT, d), join(dir, d), { recursive: true });
|
|
const c = join(dir, 'evals', 'zz-proto');
|
|
cpSync(join(ROOT, 'evals', 'skill-test-first-quiet-1'), c, { recursive: true });
|
|
writeFileSync(join(c, 'prompt.md'), '---\ntags: [constructor, owner=nobody]\n---\n\nWhat time is it?\n');
|
|
const { checkRouting } = awaitImportSync();
|
|
const r = checkRouting(dir);
|
|
assert.ok(!r.errors.some((e) => String(e.message ?? '').includes('zz-proto') || e.case === 'zz-proto'), JSON.stringify(r.errors));
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|