test(intent): M1 + M2 red — hidden/extra Intent sections pass; --approve only finds <dir>/brief.md
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>
This commit is contained in:
parent
d906999036
commit
ff760edba2
1 changed files with 136 additions and 2 deletions
|
|
@ -16,7 +16,7 @@
|
|||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
||||
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';
|
||||
|
|
@ -86,7 +86,9 @@ function gate(briefPath) {
|
|||
}
|
||||
|
||||
function stampInto(dir, env) {
|
||||
return run(commandLine('commands/trekbrief.md', '--stamp'), { '{PROJECT_DIR}': 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) {
|
||||
|
|
@ -306,3 +308,135 @@ test('/trekbrief --approve is a parsed flag and a documented mode', async () =>
|
|||
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>/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue