M1 (PM probes vi-a, vi-b, vii-a, vii-b, vii-c): extractSection takes the first
## Intent and does not see code blocks or HTML comments, so an added, hidden
or cut-off section rides on the old approval. 5 red. vii-d (comment inside
Intent) and the plain-brief hash are pinned green so the fix cannot move them.
M2: --approve resolves only <dir>/brief.md; a brief planned with
--brief docs/x-brief.md has no approval path but a raw --stamp. 4 red.
The stamp helper now substitutes {BRIEF_PATH} as well as {PROJECT_DIR}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
442 lines
21 KiB
JavaScript
442 lines
21 KiB
JavaScript
// tests/validators/intent-approval.test.mjs
|
|
// Veikart steg 1 — «Intensjonen først»: /trekplan stops without an approved intent.
|
|
//
|
|
// The four order cases, each bound to the COMMAND PROSE rather than to a module
|
|
// name alone: the gate line is extracted from commands/trekplan.md and the stamp
|
|
// line from commands/trekbrief.md, and those exact lines are executed. Remove the
|
|
// line from the prose, or stub the module, and these tests fall.
|
|
// (a) brief without a marker → /trekplan's check exits non-zero
|
|
// (b) marker + intent text edited after → exits non-zero (stale)
|
|
// (c) valid marker → exits 0 (known negative)
|
|
// (d) the stamp runs before the manual/auto fork and emits brief-approved
|
|
//
|
|
// The module is imported dynamically so a missing module fails each test on its
|
|
// own instead of crashing the file into a single failure.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } 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 MODULE = join(ROOT, 'lib', 'validators', 'intent-approval.mjs');
|
|
const load = () => import(MODULE);
|
|
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
|
|
|
// A generic brief — no real organisation, no real corpus.
|
|
const BRIEF = `---
|
|
type: trekbrief
|
|
brief_version: "2.2"
|
|
created: 2026-09-23
|
|
task: "Add a dark-mode toggle to a recipe app"
|
|
slug: dark-mode-toggle
|
|
project_dir: .claude/projects/2026-09-23-dark-mode-toggle/
|
|
research_topics: 0
|
|
research_status: complete
|
|
auto_research: false
|
|
interview_turns: 4
|
|
source: interview
|
|
framing: new-direction
|
|
phase_signals_partial: true
|
|
---
|
|
|
|
# Task: Dark-mode toggle
|
|
|
|
## TL;DR
|
|
|
|
Readers cook at night; the white page glares.
|
|
|
|
## Intent
|
|
|
|
Readers cook in dim kitchens and the bright page glares.
|
|
They want the app to follow the system theme.
|
|
|
|
## Goal
|
|
|
|
A toggle in settings, defaulting to the system theme,
|
|
remembered across restarts.
|
|
|
|
## Success Criteria
|
|
|
|
- Toggle persists across restarts.
|
|
`;
|
|
|
|
function tmpDir() { return mkdtempSync(join(tmpdir(), 'intent-approval-')); }
|
|
|
|
/** The single bash line in `rel` that invokes intent-approval.mjs with `verb`. */
|
|
function commandLine(rel, verb) {
|
|
const lines = read(rel).split('\n').filter((l) =>
|
|
/^\s*node \$\{CLAUDE_PLUGIN_ROOT\}\/lib\/validators\/intent-approval\.mjs\b/.test(l) && l.includes(verb));
|
|
assert.equal(lines.length, 1, `${rel} must carry exactly one \`intent-approval.mjs ${verb}\` line; found ${lines.length}`);
|
|
return lines[0].trim();
|
|
}
|
|
|
|
function run(line, subs, env = {}) {
|
|
let cmd = line.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT);
|
|
for (const [k, v] of Object.entries(subs)) cmd = cmd.split(k).join(v);
|
|
return spawnSync('bash', ['-c', cmd], { encoding: 'utf8', env: { ...process.env, ...env } });
|
|
}
|
|
|
|
function gate(briefPath) {
|
|
return run(commandLine('commands/trekplan.md', '--check'), { '{brief_path}': briefPath });
|
|
}
|
|
|
|
function stampInto(dir, env) {
|
|
// {BRIEF_PATH} since M2 (the resolved --approve target); {PROJECT_DIR} kept for the old spelling
|
|
return run(commandLine('commands/trekbrief.md', '--stamp'),
|
|
{ '{BRIEF_PATH}': join(dir, 'brief.md'), '{PROJECT_DIR}': dir }, env);
|
|
}
|
|
|
|
function codes(res) {
|
|
const out = JSON.parse(res.stdout);
|
|
return out.errors.map((e) => e.code);
|
|
}
|
|
|
|
// ---- (a) (b) (c): the /trekplan gate, executed as the command prose spells it ----
|
|
|
|
test('(a) /trekplan gate: brief WITHOUT an approval marker stops (exit != 0, BRIEF_INTENT_NOT_APPROVED)', () => {
|
|
const dir = tmpDir();
|
|
try {
|
|
const p = join(dir, 'brief.md');
|
|
writeFileSync(p, BRIEF);
|
|
const r = gate(p);
|
|
assert.notEqual(r.status, 0, `gate must stop on an unapproved brief; stdout=${r.stdout} stderr=${r.stderr}`);
|
|
assert.ok(codes(r).includes('BRIEF_INTENT_NOT_APPROVED'), r.stdout);
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('(b) /trekplan gate: marker present but ## Intent edited after approval stops (BRIEF_INTENT_APPROVAL_STALE)', async () => {
|
|
const { stampIntentApproval } = await load();
|
|
const dir = tmpDir();
|
|
try {
|
|
const p = join(dir, 'brief.md');
|
|
const stamped = stampIntentApproval(BRIEF, new Date('2026-09-23T08:00:00Z')).text;
|
|
writeFileSync(p, stamped.replace('follow the system theme', 'always be dark'));
|
|
const r = gate(p);
|
|
assert.notEqual(r.status, 0, `gate must stop on a stale approval; stdout=${r.stdout}`);
|
|
assert.ok(codes(r).includes('BRIEF_INTENT_APPROVAL_STALE'), r.stdout);
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('(b) /trekplan gate: editing ## Goal after approval also stops', async () => {
|
|
const { stampIntentApproval } = await load();
|
|
const dir = tmpDir();
|
|
try {
|
|
const p = join(dir, 'brief.md');
|
|
const stamped = stampIntentApproval(BRIEF, new Date('2026-09-23T08:00:00Z')).text;
|
|
writeFileSync(p, stamped.replace('remembered across restarts', 'reset on every launch'));
|
|
const r = gate(p);
|
|
assert.notEqual(r.status, 0);
|
|
assert.ok(codes(r).includes('BRIEF_INTENT_APPROVAL_STALE'), r.stdout);
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('(c) /trekplan gate: a valid marker passes (exit 0) — known negative', async () => {
|
|
const { stampIntentApproval } = await load();
|
|
const dir = tmpDir();
|
|
try {
|
|
const p = join(dir, 'brief.md');
|
|
writeFileSync(p, stampIntentApproval(BRIEF, new Date('2026-09-23T08:00:00Z')).text);
|
|
const r = gate(p);
|
|
assert.equal(r.status, 0, `valid approval must pass; stdout=${r.stdout} stderr=${r.stderr}`);
|
|
assert.deepEqual(codes(r), []);
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('(c) editing text OUTSIDE ## Intent / ## Goal keeps the approval valid', async () => {
|
|
const { stampIntentApproval, checkIntentApprovalContent } = await load();
|
|
const stamped = stampIntentApproval(BRIEF, new Date('2026-09-23T08:00:00Z')).text;
|
|
const edited = stamped.replace('- Toggle persists across restarts.', '- Toggle persists across restarts.\n- Contrast passes AA.');
|
|
assert.deepEqual(checkIntentApprovalContent(edited).errors, []);
|
|
});
|
|
|
|
test('whitespace-only reflow of ## Intent keeps the approval valid (normalized hash)', async () => {
|
|
const { stampIntentApproval, checkIntentApprovalContent } = await load();
|
|
const stamped = stampIntentApproval(BRIEF, new Date('2026-09-23T08:00:00Z')).text;
|
|
const reflowed = stamped.replace(
|
|
'Readers cook in dim kitchens and the bright page glares.\nThey want',
|
|
'Readers cook in dim kitchens and the bright page glares.\n\n They want');
|
|
assert.notEqual(reflowed, stamped);
|
|
assert.deepEqual(checkIntentApprovalContent(reflowed).errors, []);
|
|
});
|
|
|
|
test('a malformed marker is BRIEF_INTENT_APPROVAL_INVALID, not a pass', async () => {
|
|
const { checkIntentApprovalContent } = await load();
|
|
const bad = BRIEF.replace('source: interview\n', 'source: interview\nintent_approved_hash: "yes"\n');
|
|
const codesOut = checkIntentApprovalContent(bad).errors.map((e) => e.code);
|
|
assert.deepEqual(codesOut, ['BRIEF_INTENT_APPROVAL_INVALID']);
|
|
});
|
|
|
|
test('a brief missing ## Goal cannot be approved: stamp refuses, check is INVALID', async () => {
|
|
const { stampIntentApproval, checkIntentApprovalContent } = await load();
|
|
const noGoal = BRIEF.replace(/## Goal\n\n[\s\S]*?\n\n## Success/, '## Success');
|
|
assert.equal(stampIntentApproval(noGoal).stamped, false);
|
|
const forged = noGoal.replace('source: interview\n',
|
|
`source: interview\nintent_approved_hash: "sha256:${'0'.repeat(64)}"\n`);
|
|
assert.deepEqual(checkIntentApprovalContent(forged).errors.map((e) => e.code), ['BRIEF_INTENT_APPROVAL_INVALID']);
|
|
});
|
|
|
|
test('trekreview briefs are exempt from the gate (they carry no ## Intent of their own)', async () => {
|
|
const { checkIntentApprovalContent } = await load();
|
|
const review = `---
|
|
type: trekreview
|
|
task: "Review delivered toggle"
|
|
slug: dark-mode-toggle
|
|
project_dir: .claude/projects/2026-09-23-dark-mode-toggle/
|
|
findings:
|
|
- 0123456789abcdef0123456789abcdef01234567
|
|
---
|
|
|
|
# Review brief
|
|
`;
|
|
assert.deepEqual(checkIntentApprovalContent(review).errors, []);
|
|
});
|
|
|
|
test('the gate message tells the operator what to do (/trekbrief --approve)', () => {
|
|
const dir = tmpDir();
|
|
try {
|
|
const p = join(dir, 'brief.md');
|
|
writeFileSync(p, BRIEF);
|
|
const out = JSON.parse(gate(p).stdout);
|
|
const e = out.errors.find((x) => x.code === 'BRIEF_INTENT_NOT_APPROVED');
|
|
assert.ok(e && /\/trekbrief --approve/.test(`${e.message} ${e.hint || ''}`), JSON.stringify(e));
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
// ---- /trekplan prose: the gate halts, on both brief modes, before any planning ----
|
|
|
|
test('trekplan prose: the intent-approval gate sits in "Read the brief" and says HALT', () => {
|
|
const text = read('commands/trekplan.md');
|
|
const readIdx = text.indexOf('### Read the brief');
|
|
const gateIdx = text.indexOf(commandLine('commands/trekplan.md', '--check'));
|
|
const phase2 = text.indexOf('\n## Phase 2');
|
|
assert.ok(readIdx >= 0 && gateIdx > readIdx, 'gate must sit under ### Read the brief (runs for --brief AND --project)');
|
|
assert.ok(phase2 < 0 || gateIdx < phase2, 'gate must run before Phase 2');
|
|
const section = text.slice(gateIdx, gateIdx + 2500);
|
|
assert.match(section, /BRIEF_INTENT_NOT_APPROVED/);
|
|
assert.match(section, /BRIEF_INTENT_APPROVAL_STALE/);
|
|
assert.match(section, /\*\*halt\*\*/i);
|
|
});
|
|
|
|
// ---- (d) the stamp: operator answer → marker + brief-approved, on BOTH paths ----
|
|
|
|
test('(d) trekbrief: the stamp step runs BEFORE the Phase 5 manual/auto fork', () => {
|
|
const text = read('commands/trekbrief.md');
|
|
const stampIdx = text.indexOf(commandLine('commands/trekbrief.md', '--stamp'));
|
|
const phase5 = text.indexOf('## Phase 5');
|
|
const manual = text.indexOf('### Manual path');
|
|
assert.ok(phase5 > 0 && manual > 0, 'Phase 5 / Manual path headings must exist');
|
|
assert.ok(stampIdx > 0 && stampIdx < phase5 && stampIdx < manual,
|
|
'the stamp must run before the fork, so the manual path passes through it');
|
|
// The approval is the operator's explicit answer, asked even in --quick.
|
|
const before = text.slice(text.lastIndexOf('\n## Phase 4h', stampIdx), stampIdx);
|
|
assert.match(before, /AskUserQuestion/);
|
|
assert.match(before, /--quick/);
|
|
});
|
|
|
|
test('(d) trekbrief: the auto path no longer emits brief-approved on its own (one emission, not two)', () => {
|
|
const text = read('commands/trekbrief.md');
|
|
const auto = text.indexOf('### Auto path');
|
|
const phase6 = text.indexOf('## Phase 6');
|
|
assert.ok(auto > 0 && phase6 > auto);
|
|
assert.ok(!/--event brief-approved/.test(text.slice(auto, phase6)),
|
|
'brief-approved is emitted by the stamp; a second emission on the auto path would double-count');
|
|
});
|
|
|
|
test('(d) the manual-path stamp line writes the marker AND emits a brief-approved record', () => {
|
|
const dir = tmpDir();
|
|
const data = tmpDir();
|
|
try {
|
|
writeFileSync(join(dir, 'brief.md'), BRIEF);
|
|
const r = stampInto(dir, { CLAUDE_PLUGIN_DATA: data });
|
|
assert.equal(r.status, 0, `stamp failed: ${r.stdout} ${r.stderr}`);
|
|
const after = readFileSync(join(dir, 'brief.md'), 'utf8');
|
|
assert.match(after, /^intent_approved_hash: "sha256:[0-9a-f]{64}"$/m);
|
|
assert.match(after, /^intent_approved_at: "\d{4}-\d\d-\d\dT[^"]+"$/m);
|
|
const statsFile = join(data, 'trekexecute-stats.jsonl');
|
|
assert.ok(existsSync(statsFile), 'brief-approved record must be written');
|
|
const recs = readFileSync(statsFile, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
const ev = recs.filter((x) => x.event === 'brief-approved');
|
|
assert.equal(ev.length, 1);
|
|
assert.equal(ev[0].payload.slug, 'dark-mode-toggle');
|
|
assert.match(ev[0].payload.intent_hash, /^sha256:[0-9a-f]{64}$/);
|
|
// and the gate now passes on the stamped file
|
|
assert.equal(gate(join(dir, 'brief.md')).status, 0);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
rmSync(data, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('(d) with CLAUDE_PLUGIN_DATA unset the record still lands (fallback = the dir the yardstick reads)', async () => {
|
|
const { resolveApprovalDataDir } = await load();
|
|
const d = resolveApprovalDataDir({ HOME: '/home/someone' });
|
|
assert.equal(d, '/home/someone/.claude/plugins/data/voyage-ktg-plugin-marketplace');
|
|
assert.equal(resolveApprovalDataDir({ CLAUDE_PLUGIN_DATA: '/x/y', HOME: '/h' }), '/x/y');
|
|
});
|
|
|
|
test('re-stamping replaces the marker instead of duplicating it', async () => {
|
|
const { stampIntentApproval } = await load();
|
|
const once = stampIntentApproval(BRIEF, new Date('2026-09-23T08:00:00Z')).text;
|
|
const twice = stampIntentApproval(once, new Date('2026-09-24T08:00:00Z')).text;
|
|
assert.equal((twice.match(/^intent_approved_hash:/gm) || []).length, 1);
|
|
assert.equal((twice.match(/^intent_approved_at:/gm) || []).length, 1);
|
|
assert.match(twice, /2026-09-24T08:00:00/);
|
|
});
|
|
|
|
// ---- honesty: the code says what the marker does NOT prove ----
|
|
|
|
test('the module states what the marker does NOT prove (trace, not a signature)', () => {
|
|
assert.ok(existsSync(MODULE), 'intent-approval.mjs missing');
|
|
const src = readFileSync(MODULE, 'utf8');
|
|
assert.match(src, /NOT a signature/);
|
|
assert.match(src, /same user, same machine/i);
|
|
});
|
|
|
|
// ---- /trekbrief --approve: the stop message's remedy exists ----
|
|
|
|
test('/trekbrief --approve is a parsed flag and a documented mode', async () => {
|
|
const { parseArgs } = await import(join(ROOT, 'lib', 'parsers', 'arg-parser.mjs'));
|
|
const r = parseArgs('--approve .claude/projects/2026-09-23-dark-mode-toggle', 'trekbrief');
|
|
assert.equal(r.flags['--approve'], '.claude/projects/2026-09-23-dark-mode-toggle');
|
|
assert.deepEqual(r.unknown, []);
|
|
assert.deepEqual(r.errors, []);
|
|
assert.match(read('commands/trekbrief.md'), /--approve <project-dir>/);
|
|
assert.match(read('docs/command-modes.md'), /`--approve <project-dir>`/);
|
|
});
|
|
|
|
// ---- M1: the parser fails CLOSED on extra, hidden or cut-off sections ----
|
|
// (PM re-measurement 2026-09-23, probes vi-a/vi-b/vii-a/vii-b/vii-c: all passed the gate)
|
|
|
|
const STAMP_AT = new Date('2026-09-23T08:00:00Z');
|
|
const FENCE = '```';
|
|
|
|
test('M1 (vi-a): a second ## Intent added AFTER approval does not pass the gate', async () => {
|
|
const { stampIntentApproval, checkIntentApprovalContent } = await load();
|
|
const stamped = stampIntentApproval(BRIEF, STAMP_AT).text;
|
|
const extra = stamped.replace('## Success Criteria', '## Intent\n\nActually: make it always dark.\n\n## Success Criteria');
|
|
const r = checkIntentApprovalContent(extra);
|
|
assert.equal(r.valid, false, 'an added ## Intent must not ride on the old approval');
|
|
assert.deepEqual(r.errors.map((e) => e.code), ['BRIEF_INTENT_APPROVAL_INVALID']);
|
|
});
|
|
|
|
test('M1 (vi-b): a brief with two ## Intent (or two ## Goal) sections cannot be stamped', async () => {
|
|
const { stampIntentApproval } = await load();
|
|
const twoIntents = BRIEF.replace('## Goal', '## Intent\n\nA second intent.\n\n## Goal');
|
|
assert.equal(stampIntentApproval(twoIntents).stamped, false);
|
|
const twoGoals = BRIEF.replace('## Success Criteria', '## Goal\n\nA second goal.\n\n## Success Criteria');
|
|
assert.equal(stampIntentApproval(twoGoals).stamped, false);
|
|
});
|
|
|
|
test('M1 (vii-a): a ## Intent inside a code block before the real one does not capture the hash', async () => {
|
|
const { stampIntentApproval, checkIntentApprovalContent } = await load();
|
|
const withExample = BRIEF.replace('## TL;DR', `## TL;DR\n\n${FENCE}markdown\n## Intent\n\nExample text.\n${FENCE}`);
|
|
const stamped = stampIntentApproval(withExample, STAMP_AT).text;
|
|
const edited = stamped.replace('follow the system theme', 'always be dark');
|
|
assert.deepEqual(checkIntentApprovalContent(edited).errors.map((e) => e.code), ['BRIEF_INTENT_APPROVAL_STALE']);
|
|
});
|
|
|
|
test('M1 (vii-b): a ## Intent inside an HTML comment before the real one does not capture the hash', async () => {
|
|
const { stampIntentApproval, checkIntentApprovalContent } = await load();
|
|
const withComment = BRIEF.replace('## TL;DR', '## TL;DR\n\n<!--\n## Intent\n\nOld draft.\n-->');
|
|
const stamped = stampIntentApproval(withComment, STAMP_AT).text;
|
|
const edited = stamped.replace('follow the system theme', 'always be dark');
|
|
assert.deepEqual(checkIntentApprovalContent(edited).errors.map((e) => e.code), ['BRIEF_INTENT_APPROVAL_STALE']);
|
|
});
|
|
|
|
test('M1 (vii-c): a "## " line inside a code block in ## Intent does not cut the section short', async () => {
|
|
const { stampIntentApproval, checkIntentApprovalContent } = await load();
|
|
const withBlock = BRIEF.replace('They want the app to follow the system theme.',
|
|
`${FENCE}\n## not a heading\n${FENCE}\nThey want the app to follow the system theme.`);
|
|
const stamped = stampIntentApproval(withBlock, STAMP_AT).text;
|
|
const edited = stamped.replace('follow the system theme', 'always be dark');
|
|
assert.deepEqual(checkIntentApprovalContent(edited).errors.map((e) => e.code), ['BRIEF_INTENT_APPROVAL_STALE']);
|
|
});
|
|
|
|
test('M1 (vii-d, kept): an HTML comment INSIDE ## Intent is part of the approved text', async () => {
|
|
const { stampIntentApproval, checkIntentApprovalContent } = await load();
|
|
const withNote = BRIEF.replace('They want', '<!-- note: v1 -->\nThey want');
|
|
const stamped = stampIntentApproval(withNote, STAMP_AT).text;
|
|
const edited = stamped.replace('note: v1', 'note: v2');
|
|
assert.deepEqual(checkIntentApprovalContent(edited).errors.map((e) => e.code), ['BRIEF_INTENT_APPROVAL_STALE']);
|
|
});
|
|
|
|
test('M1: a plain brief hashes the same as before the parser change (no stamped brief goes stale)', async () => {
|
|
const { computeIntentHash } = await load();
|
|
// pinned from 66e1fa1's computeIntentHash(BRIEF)
|
|
assert.equal(computeIntentHash(BRIEF.split('---\n').slice(2).join('---\n')).hash,
|
|
'sha256:e722491047867492fde099d6048c46ee673229b00bebe90f0484a1943a199da9');
|
|
});
|
|
|
|
// ---- M2: --approve reaches a brief that is not <dir>/brief.md ----
|
|
// (/trekplan --brief docs/x-brief.md had no approval path but a raw --stamp)
|
|
|
|
function resolveApprove(arg) {
|
|
const line = commandLine('commands/trekbrief.md', '--resolve');
|
|
return run(line, { '{APPROVE_ARG}': arg });
|
|
}
|
|
|
|
test('M2: /trekbrief --approve <brief-file> resolves to that file and names /trekplan --brief', () => {
|
|
const dir = tmpDir();
|
|
try {
|
|
mkdirSync(join(dir, 'docs'));
|
|
const p = join(dir, 'docs', 'x-brief.md');
|
|
writeFileSync(p, BRIEF);
|
|
const r = resolveApprove(p);
|
|
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
const out = JSON.parse(r.stdout);
|
|
assert.equal(out.brief_path, p);
|
|
assert.equal(out.plan_command, `/trekplan --brief ${p}`);
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('M2: /trekbrief --approve <project-dir> still resolves to <dir>/brief.md and /trekplan --project', () => {
|
|
const dir = tmpDir();
|
|
try {
|
|
writeFileSync(join(dir, 'brief.md'), BRIEF);
|
|
const r = resolveApprove(`${dir}/`);
|
|
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
const out = JSON.parse(r.stdout);
|
|
assert.equal(out.brief_path, join(dir, 'brief.md'));
|
|
assert.equal(out.plan_command, `/trekplan --project ${dir}`);
|
|
const missing = resolveApprove(join(dir, 'nope'));
|
|
assert.equal(missing.status, 1);
|
|
assert.match(JSON.parse(missing.stdout).error, /no brief/i);
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('M2: the Phase 4h stamp line stamps the resolved brief file, whatever its name', () => {
|
|
const dir = tmpDir();
|
|
const data = tmpDir();
|
|
try {
|
|
const p = join(dir, 'x-brief.md');
|
|
writeFileSync(p, BRIEF);
|
|
const line = commandLine('commands/trekbrief.md', '--stamp');
|
|
assert.ok(line.includes('{BRIEF_PATH}'), 'the stamp must take the resolved brief path, not {PROJECT_DIR}/brief.md');
|
|
const r = run(line, { '{BRIEF_PATH}': p }, { CLAUDE_PLUGIN_DATA: data });
|
|
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
assert.equal(gate(p).status, 0);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
rmSync(data, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('M2: the gate names a remedy that works for --brief too (/trekbrief --approve {brief_path})', () => {
|
|
const text = read('commands/trekplan.md');
|
|
const section = text.slice(text.indexOf('### Read the brief'), text.indexOf('Read the brief file and parse'));
|
|
assert.match(section, /\/trekbrief --approve \{brief_path\}/);
|
|
assert.doesNotMatch(section, /\/trekbrief --approve \{project_dir\}/);
|
|
const dir = tmpDir();
|
|
try {
|
|
const p = join(dir, 'brief.md');
|
|
writeFileSync(p, BRIEF);
|
|
const e = JSON.parse(gate(p).stdout).errors[0];
|
|
assert.match(e.hint, /brief-file/);
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
assert.match(read('docs/HANDOVER-CONTRACTS.md'), /--approve <project-dir \| brief-file>/);
|
|
});
|