voyage/tests/lib/intent-prose-pins.test.mjs
Kjell Tore Guttormsen 651be3468b
test(r8): move the live tests out of the round files
R8 of cleanup R-a (plan .claude/projects/2026-11-07-cleanup-r-a, appendix A).
The live tests of the round files move into the test named after their
source; each moved test was seen red under a mutation of its source.
New: tests/lib/lib-imports.test.mjs, tests/lib/timing.test.mjs (from
timing-under-load). The [#1] --gates rule and shim-line pins move into
tests/lib/autonomy-gate.test.mjs.

Removed: the round test files (retterunde, retterunde-prose-pins,
trial-mechanical-fixes, trial2-fixes, trial2-minor, trial2-minor-prose-pins,
fix-round-4-5a-prose, fix-round-4-5b2-b-prose, timing-under-load), the 8 R8
e2e scripts, and seven round test files on the R-a removal list that pinned
them (fix-round-4-5b-prose, fix-round-4-5b-part-2/3/4-scripts,
fix-round-4-5b-part-4-prose, fix-round-4-5b2-b/c-scripts). The diff-mode e2e
pin (test 24) and the red-script round-wrapper cases go too.

tests/synthetic/plan-run-C.md becomes tests/fixtures/plan-run-c.md,
regenerated from buildPlan({ version: '1.9' }). The red judge, reporter and
judge test headers now name the shared red script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:19:49 +01:00

206 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tests/lib/intent-prose-pins.test.mjs
// Order 2 (P1 — intent as a stored artifact): the prose pins for /trekbrief's
// ceremony by size, the question round, answers that are not approval, the
// approval log and the commit; and /trekplan's Phase 1 log gate and Phase 11
// plan approval. The command lines are EXTRACTED from the prose and executed,
// so removing a line, or pointing it at the wrong module, fails here.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
/** Text from `## <heading>` up to the next `## ` heading. */
function section(text, heading) {
const start = text.indexOf(`\n## ${heading}`);
assert.ok(start >= 0, `missing ## ${heading}`);
const next = text.indexOf('\n## ', start + 4);
return text.slice(start, next < 0 ? undefined : next);
}
/** The single line in `text` that runs `lib/<mod>` with `verb`. */
function line(text, mod, verb) {
const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
const re = new RegExp(`^\\s*node \\$\\{CLAUDE_PLUGIN_ROOT\\}/lib/${esc}\\b`);
const hits = text.split('\n').filter((l) => re.test(l) && l.includes(verb));
assert.equal(hits.length, 1, `expected exactly one \`${mod} ${verb}\` line; found ${hits.length}`);
return hits[0].trim();
}
function bash(cmd, cwd) {
return spawnSync('bash', ['-c', cmd.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)], { cwd, encoding: 'utf8' });
}
const brief = read('commands/trekbrief.md');
const plan = read('commands/trekplan.md');
// ---- /trekbrief: ceremony by size ----
test('trekbrief: three classes, heaviest in doubt, said aloud, overridable, only upward mid-task', () => {
const p3 = section(brief, 'Phase 3');
for (const c of ['small', 'bounded', 'architectural']) assert.ok(p3.includes(`\`${c}\``), `class ${c}`);
assert.match(p3, /heaviest/i);
assert.match(p3, /aloud/i);
assert.match(p3, /override/i);
assert.match(p3, /only (ever )?(moves? )?up/i);
assert.match(p3, /`ceremony:/, 'the class is written to the brief (K1)');
});
// The interview form moved into the skill /trekbrief asks for by name (order
// 7e, one source): the same assertions, now read where the text lives.
const interviewing = () => read('skills/interviewing/SKILL.md');
test('trekbrief: the round — all answerable questions at once, recommendation first, no fact questions', () => {
const p3 = section(brief, 'Phase 3');
const iv = interviewing();
assert.match(p3, /Use `AskUserQuestion` for every question/);
assert.match(p3, /`voyage:interviewing`/, 'Phase 3 asks for the interview form by name');
assert.doesNotMatch(brief, /Ask one question at a time/, 'the one-at-a-time rule is replaced by the round');
assert.doesNotMatch(iv, /Ask one question at a time/, 'the one-at-a-time rule is replaced by the round');
assert.match(p3, /one round/i);
assert.match(iv, /one round/i);
assert.match(iv, /recommend(ed|ation)[^.\n]*first/i);
assert.match(iv, /never ask[^.\n]*fact/i);
assert.match(iv, /Impact × Uncertainty/);
assert.match(iv, /[Dd]eferred/);
assert.match(iv, /next three answers/);
});
test('trekbrief: the budget for a small task is at most 5 questions and fits one AskUserQuestion call', () => {
const p3 = section(brief, 'Phase 3');
const m = p3.match(/`small`[^\n|]*\|\s*(\d+)/);
assert.ok(m, 'the budget table must give a number for `small`');
assert.ok(Number(m[1]) <= 5 && Number(m[1]) <= 4, `small budget ${m[1]} must be ≤ 5 and fit one call (max 4 questions)`);
assert.match(p3, /framing/i, 'framing is one of the round\'s questions');
assert.match(p3, /artifact_store/, 'the storage choice is one of the round\'s questions');
});
test('trekbrief: credits every borrowing of the round and the classes', () => {
// The classes stay in /trekbrief with their credits; the round's credits
// moved with the round into the skill.
const p3 = section(brief, 'Phase 3');
for (const src of ['obra/superpowers@8ca22db', 'kiro.dev']) {
assert.ok(p3.includes(src), `credit for ${src}`);
}
const iv = interviewing();
for (const re of [/mattpocock\/skills ?@c55ee46/, /GitHub Spec Kit/, /addyosmani\/agent-skills ?@9d0c60d/]) {
assert.match(iv, re, `credit for ${re}`);
}
});
test('trekbrief: hard rule 8 no longer says Phase 3 has no cap', () => {
assert.doesNotMatch(brief, /Phase 3 has no cap/);
});
// ---- /trekbrief Phase 4h: answers that are not approval, the log, the commit ----
test('trekbrief 4h: the answer check runs before the stamp, then append, then commit', () => {
const p4h = section(brief, 'Phase 4h');
const ans = p4h.indexOf(line(p4h, 'util/approval-log.mjs', ' answer'));
const stamp = p4h.indexOf('intent-approval.mjs --stamp');
const app = p4h.indexOf(line(p4h, 'util/approval-log.mjs', ' append'));
const com = p4h.indexOf(line(p4h, 'util/artifact-commit.mjs', '--store'));
assert.ok(ans >= 0 && ans < stamp && stamp < app && app < com, 'order: answer → stamp → append → commit');
assert.match(p4h, /høres bra ut/);
assert.match(p4h, /gjør som du vil/);
assert.match(p4h, /empty/i);
});
test('trekbrief 4h: the answer line, run as written, refuses «høres bra ut» and «gjør som du vil»', () => {
const p4h = section(brief, 'Phase 4h');
const start = p4h.indexOf(line(p4h, 'util/approval-log.mjs', ' answer'));
const block = p4h.slice(start, p4h.indexOf('```', start)).trim(); // the heredoc, up to the fence
const d = mkdtempSync(join(tmpdir(), 'intent-pins-'));
try {
for (const [answer, code] of [['høres bra ut', 1], ['gjør som du vil', 1], ['', 1], ['Approve', 0]]) {
const r = bash(block.split('{OPERATOR_ANSWER}').join(answer), d);
assert.equal(r.status, code, `«${answer}» → ${r.status} (${r.stdout}${r.stderr})`);
}
} finally { rmSync(d, { recursive: true, force: true }); }
});
test('trekbrief 4h: append binds the intent hash; commit uses the operator\'s storage choice', () => {
const p4h = section(brief, 'Phase 4h');
const app = line(p4h, 'util/approval-log.mjs', ' append');
assert.match(app, /--kind intent/);
assert.match(app, /--subject "\{BRIEF_PATH\}"/);
assert.match(app, /--sha256 "\{INTENT_HASH\}"/);
const com = line(p4h, 'util/artifact-commit.mjs', '--store');
assert.match(com, /--store \{ARTIFACT_STORE\}/);
assert.match(com, /--artifact "\{BRIEF_PATH\}"/);
});
// ---- /trekplan Phase 1: the log gate ----
function gateLine() {
const read1 = plan.slice(plan.indexOf('### Read the brief'), plan.indexOf('## Phase 1.5'));
return line(read1, 'util/approval-log.mjs', ' verify');
}
test('trekplan Phase 1: the log gate sits after the marker check, before planning, and says halt', () => {
const readIdx = plan.indexOf('### Read the brief');
const check = plan.indexOf('intent-approval.mjs --check');
const gate = plan.indexOf(gateLine());
const p15 = plan.indexOf('## Phase 1.5');
assert.ok(readIdx < check && check < gate && gate < p15);
const after = plan.slice(gate, gate + 2000);
assert.match(after, /\*\*halt\*\*/i);
for (const s of ['broken', 'diverged', 'not-logged', 'mismatch']) assert.ok(after.includes(`\`${s}\``), s);
assert.match(gateLine(), /--kind intent --subject "\{brief_path\}" --sha256 "\{intent_approved_hash\}"/);
});
test('trekplan Phase 1: the gate line, run as written, stops (exit 1) when the brief\'s log is broken', () => {
const d = mkdtempSync(join(tmpdir(), 'intent-pins-gate-'));
try {
const H = 'd'.repeat(64);
const logCli = join(ROOT, 'lib', 'util', 'approval-log.mjs');
writeFileSync(join(d, 'brief.md'), 'x');
spawnSync(process.execPath, [logCli, 'append', '--kind', 'intent', '--subject', 'brief.md', '--sha256', H], { cwd: d });
spawnSync(process.execPath, [logCli, 'append', '--kind', 'plan', '--subject', 'plan.md', '--sha256', H], { cwd: d });
const cmd = gateLine().split('{brief_path}').join('brief.md').split('{intent_approved_hash}').join(`sha256:${H}`);
assert.equal(bash(cmd, d).status, 0, 'intact log with the brief\'s approval passes');
const p = join(d, '.claude', 'voyage', 'approvals.jsonl');
const text = readFileSync(p, 'utf8');
writeFileSync(p, text.replace('"intent"', '"intenT"'));
const r = bash(cmd, d);
assert.equal(r.status, 1, r.stdout + r.stderr);
assert.equal(JSON.parse(r.stdout).status, 'broken');
} finally { rmSync(d, { recursive: true, force: true }); }
});
test('trekplan Phase 1: reads ceremony (K1, absent = architectural) and artifact_store (absent = local)', () => {
const read1 = plan.slice(plan.indexOf('### Read the brief'), plan.indexOf('### When the input is type:trekreview'));
assert.match(read1, /`ceremony`[^\n]*absent[^\n]*`architectural`/);
assert.match(read1, /`artifact_store`[^\n]*absent[^\n]*`local`/);
});
// ---- /trekplan Phase 11: plan approval before any handoff ----
test('trekplan Phase 11: plan approval (answer check → append plan → commit) comes before every handoff', () => {
const p11 = section(plan, 'Phase 11');
const ans = p11.indexOf(line(p11, 'util/approval-log.mjs', ' answer'));
const app = p11.indexOf(line(p11, 'util/approval-log.mjs', ' append'));
const com = p11.indexOf(line(p11, 'util/artifact-commit.mjs', '--store'));
const save = p11.indexOf('### "save"');
assert.ok(ans >= 0 && ans < app && app < com && com < save);
assert.match(line(p11, 'util/approval-log.mjs', ' append'), /--kind plan --subject "\{plan_path\}" --sha256-of "\{plan_path\}"/);
assert.match(line(p11, 'util/artifact-commit.mjs', '--store'), /--store \{artifact_store\} --artifact "\{plan_path\}"/);
assert.match(p11, /AskUserQuestion/);
});
test('/trekbrief Step 4c does not call the brief-reviewer spawn foreground or blocking, and waits for it', () => {
const start = brief.indexOf('**Step 4c — Launch brief-reviewer**');
const end = brief.indexOf('**Step 4d — Parse JSON scores**');
assert.ok(start !== -1 && end > start);
const step = brief.slice(start, end);
assert.doesNotMatch(step, /foreground, blocking/);
assert.match(step, /wait\s+for\s+(its|the agent's)\s+(result|completion)/i);
assert.match(step, /(background|asynchronous)/i, 'says how the spawn actually runs');
});