fix: round 3 of the release review of 6.3.0
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>
This commit is contained in:
parent
96cb977856
commit
0e612c88ac
14 changed files with 284 additions and 40 deletions
|
|
@ -265,3 +265,116 @@ test('review 630 r2 (operator 2026-10-10): Phase 7.2 says plainly that template-
|
|||
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 }); }
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue