129 lines
7.1 KiB
JavaScript
129 lines
7.1 KiB
JavaScript
// tests/lib/plan-prose-pins.test.mjs
|
|
// Prose pins for the plan process (wave 1, order 3): /trekplan Phases 8-10, the
|
|
// plan and design templates, plan-critic and the Handover 4 contract must keep
|
|
// saying what the plan gate, the validator and the verification CLIs do.
|
|
// Counted as doc pins by lib/util/test-census.mjs (PIN_FILE_RE).
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { MAX_ROUNDS } from '../../lib/validators/plan-gate.mjs';
|
|
import { DESIGN_SUBSECTIONS, OPTIONAL_STEP_FIELDS } from '../../lib/validators/plan-validator.mjs';
|
|
import { findSteps, PLAN_VERSION_CURRENT } from '../../lib/parsers/plan-schema.mjs';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const read = rel => readFileSync(join(ROOT, rel), 'utf-8');
|
|
|
|
function phase(text, n) {
|
|
const start = text.indexOf(`\n## Phase ${n} `);
|
|
assert.ok(start >= 0, `## Phase ${n} missing`);
|
|
const end = text.indexOf('\n## Phase ', start + 1);
|
|
return text.slice(start, end < 0 ? undefined : end);
|
|
}
|
|
|
|
const PLAN_CMD = read('commands/trekplan.md');
|
|
|
|
test('Phase 9 is a bounded loop: --record after the dedup, the cap equals MAX_ROUNDS, stop and ask after it', () => {
|
|
const p9 = phase(PLAN_CMD, 9);
|
|
assert.match(p9, /plan-review-dedup\.mjs --stdin <<'JSON' \| node \$\{CLAUDE_PLUGIN_ROOT\}\/lib\/validators\/plan-gate\.mjs --record/);
|
|
assert.ok(p9.includes(`at most **${MAX_ROUNDS} critic rounds**`), `Phase 9 must state the cap of ${MAX_ROUNDS}`);
|
|
assert.match(p9, /After round 3 with a BLOCKER open/);
|
|
assert.match(p9, /never goes on silently/);
|
|
assert.match(p9, /--extra-round/);
|
|
assert.ok(p9.indexOf('plan-gate.mjs --hash') < p9.indexOf('Then launch two review agents'), 'the hash is taken before the reviewers read the plan');
|
|
assert.match(p9, /--record "\{plan_path\}" --reviewed \{the hash from --hash\}/);
|
|
assert.match(p9, /Kiro/, 'the bounded loop is borrowed and credited');
|
|
});
|
|
|
|
test('Phase 10 runs the plan gate before offering "execute", and covers plan mode', () => {
|
|
const p10 = phase(PLAN_CMD, 10);
|
|
assert.ok(p10.indexOf('plan-gate.mjs --json') < p10.indexOf('Say **"execute"**'), 'the gate runs before the offer');
|
|
assert.match(p10, /only when the gate exited 0/);
|
|
assert.match(p10, /ExitPlanMode/);
|
|
assert.match(p10, /\*\*Not\s+verified:\*\* that `\/trekplan` runs end to end inside plan mode/);
|
|
assert.match(p10, /hedged reply/i);
|
|
});
|
|
|
|
test('Phase 8 reads ceremony (absent = architectural), the design template and the arch-rules runner', () => {
|
|
const p8 = phase(PLAN_CMD, 8);
|
|
assert.match(p8, /Absent means `architectural`/);
|
|
assert.ok(p8.includes('templates/design-template.md'));
|
|
assert.ok(p8.includes('lib/verification/arch-rules.mjs --repo .'));
|
|
assert.ok(p8.includes('--prove --rule <rule name> --plant'), 'the proof names the rule the plant must trip');
|
|
assert.match(p8, /End-to-end:/);
|
|
for (const f of OPTIONAL_STEP_FIELDS) assert.ok(p8.includes(`${f}:`), `Phase 8 names the optional field ${f}:`);
|
|
for (const credit of ['obra/superpowers', 'Spec Kit', 'mattpocock/skills', 'Cherny']) {
|
|
assert.ok(p8.includes(credit), `Phase 8 credits ${credit}`);
|
|
}
|
|
});
|
|
|
|
test('the CLIs and templates /trekplan points at exist', () => {
|
|
for (const rel of [
|
|
'lib/validators/plan-gate.mjs', 'lib/verification/arch-rules.mjs',
|
|
'lib/verification/plan-scope-sweep.mjs', 'templates/design-template.md',
|
|
]) assert.ok(existsSync(join(ROOT, rel)), `${rel} missing`);
|
|
});
|
|
|
|
test('the hard rules carry the bounded critic loop and the gate', () => {
|
|
const rules = PLAN_CMD.slice(PLAN_CMD.indexOf('\n## Hard rules'));
|
|
assert.match(rules, /\*\*Bounded critic loop\*\*: at most 3 critic rounds/);
|
|
assert.match(rules, /never while `plan-gate\.mjs` exits non-zero/);
|
|
});
|
|
|
|
test('design template: every subsection the validator requires is a ### heading, and the borrowings are credited', () => {
|
|
const t = read('templates/design-template.md');
|
|
for (const s of DESIGN_SUBSECTIONS) assert.match(t, new RegExp(`^### ${s}$`, 'm'), `design template lacks ### ${s}`);
|
|
for (const credit of ['Kiro', 'obra/superpowers', 'addyosmani/agent-skills', 'mattpocock/skills', 'Spec Kit', 'ai-native-sdlc']) {
|
|
assert.ok(t.includes(credit), `design template credits ${credit}`);
|
|
}
|
|
});
|
|
|
|
test(`plan template: plan_version ${PLAN_VERSION_CURRENT} with ceremony, Test first on every example step, one End-to-end step, no hand-written Critic Verdict`, () => {
|
|
const t = read('templates/plan-template.md');
|
|
assert.ok(t.includes(`\`plan_version: ${PLAN_VERSION_CURRENT}\` · \`ceremony:`));
|
|
const steps = findSteps(t);
|
|
assert.ok(steps.length >= 2);
|
|
const bodies = steps.map((s, i) => t.slice(s.offset, i + 1 < steps.length ? steps[i + 1].offset : undefined));
|
|
for (const [i, b] of bodies.entries()) assert.match(b, /- \*\*Test first:\*\*/, `example Step ${i + 1} lacks Test first`);
|
|
assert.equal(bodies.filter(b => /- \*\*End-to-end:\*\* `/.test(b)).length, 1);
|
|
assert.doesNotMatch(t, /^## Critic Verdict$/m, 'the planner must not copy a verdict heading: only plan-gate --record writes it');
|
|
assert.match(t, /plan-gate\.mjs\s+--record` appends it/);
|
|
assert.match(t, /^## Global Constraints$/m);
|
|
assert.doesNotMatch(t, /omit "Test first"/, 'a 1.8 plan never omits Test first');
|
|
});
|
|
|
|
test('plan-critic carries dimensions 11 (interfaces and slices) and 12 (design and architecture rules)', () => {
|
|
const c = read('agents/plan-critic.md');
|
|
assert.match(c, /^### 11\. Interfaces and slices$/m);
|
|
assert.match(c, /^### 12\. Design and architecture rules$/m);
|
|
assert.match(c, /`severity` is exactly one of `blocker`, `major`, `minor`/);
|
|
});
|
|
|
|
test('Handover 4 documents every PLAN_* code the validator and the gate emit', () => {
|
|
const contract = read('docs/HANDOVER-CONTRACTS.md');
|
|
const h4 = contract.slice(contract.indexOf('## Handover 4'), contract.indexOf('## Handover 5'));
|
|
const codes = new Set();
|
|
for (const rel of ['lib/validators/plan-validator.mjs', 'lib/validators/plan-gate.mjs']) {
|
|
for (const m of read(rel).matchAll(/'(PLAN_[A-Z_]+)'/g)) codes.add(m[1]);
|
|
}
|
|
assert.ok(codes.size >= 15, `expected the 1.8 and gate codes, found ${codes.size}`);
|
|
const missing = [...codes].filter(c => !h4.includes(c));
|
|
assert.deepEqual(missing, [], `Handover 4 does not document: ${missing.join(', ')}`);
|
|
});
|
|
|
|
test('Handover 4 states 1.9 as current and 1.8 and 1.7 as still valid', () => {
|
|
const contract = read('docs/HANDOVER-CONTRACTS.md');
|
|
assert.match(contract, /\| `plan\.md` \| `plan_version` \(plan header line\) \| `1\.9` \(1\.8 and 1\.7 still valid\) \|/);
|
|
assert.match(contract, /`"1\.9"` \(current\), `"1\.8"` \(valid\), `"1\.7"` \(valid\)/);
|
|
});
|
|
|
|
test('the verification CLIs credit what they borrow and say they install nothing', () => {
|
|
const arch = read('lib/verification/arch-rules.mjs');
|
|
assert.match(arch, /installs nothing/);
|
|
assert.match(arch, /agent-skills/);
|
|
assert.match(arch, /Pocock/);
|
|
assert.match(read('lib/verification/plan-scope-sweep.mjs'), /ai-native-sdlc \(MIT\)/);
|
|
assert.match(read('lib/validators/plan-gate.mjs'), /Kiro/);
|
|
});
|