test(r8): move the live tests the move map missed
33 more tests of live behaviour from the deleted round files, each in the test file named after its source and seen failing under a mutation of that source (log in the project folder). Two stay dropped: one pinned the removed gate states, one the old Phase 1.5 file comparison. The two trekexecute.md sentences that still said /trekcontinue compares the two prompt files now say it uses the project-folder file (line count kept). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8295601965
commit
029e83117c
9 changed files with 504 additions and 3 deletions
|
|
@ -1310,7 +1310,7 @@ but do not block.
|
|||
**Also write sibling `NEXT-SESSION-PROMPT.local.md`** with frontmatter
|
||||
(`produced_by: trekexecute`, `produced_at: <ISO-8601>`,
|
||||
`status: stopped`) — same combined ESM pattern as Phase 8 — so Phase 1.5
|
||||
of `/trekcontinue` can compare project-dir and repo-root candidates.
|
||||
of `/trekcontinue` uses it over a repo-root candidate.
|
||||
|
||||
If the entry condition **passes**:
|
||||
```
|
||||
|
|
@ -2813,8 +2813,8 @@ to stderr but do NOT block — Phase 8 must always reach the final report.
|
|||
**Also write sibling `NEXT-SESSION-PROMPT.local.md`** (Bug 3 frontmatter
|
||||
contract — see `docs/HANDOVER-CONTRACTS.md` § Handover 7 Lifecycle) in the
|
||||
project directory. The frontmatter MUST contain `produced_by: trekexecute`
|
||||
and `produced_at: <ISO-8601>` so `/trekcontinue` Phase 1.5 can detect
|
||||
producer drift between project-dir and repo-root candidates. Use a single
|
||||
and `produced_at: <ISO-8601>`; `/trekcontinue` Phase 1.5 uses this file
|
||||
over a repo-root candidate. Use a single
|
||||
ESM inline block so state-file + prompt-file writes succeed or fail together:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { spawnSync } from 'node:child_process';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { childEnv } from '../helpers/child-env.mjs';
|
||||
import { findSection } from '../../lib/parsers/plan-schema.mjs';
|
||||
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
|
@ -309,3 +310,30 @@ test('trekendsession Phase 0 has no "contains --help or -h" check, and its parse
|
|||
assert.equal(two.status, 0, two.stdout + two.stderr);
|
||||
assert.deepEqual(JSON.parse(two.stdout).positional, ['fix-header', 'Session 2']);
|
||||
});
|
||||
|
||||
test('[#15] /trekendsession Phase 2 reads the brief path from the parser\'s positional and the label from its tail', () => {
|
||||
const phase2 = flat(sectionText(readCommand(), '## Phase 2 — Required args check (headless-safe)'));
|
||||
assert.match(phase2, /`positional\[0\]`/, 'Phase 2 does not read <next-brief-path> from positional[0]');
|
||||
assert.match(phase2, /`<next-label>` is the parser's `tail`/, 'Phase 2 does not read <next-label> from the parser\'s tail');
|
||||
});
|
||||
|
||||
// #67: "NEVER auto-invoke this command" was a sentence only; the frontmatter
|
||||
// carries disable-model-invocation, so the model cannot start it.
|
||||
test('[#67] /trekendsession sets disable-model-invocation, and CLAUDE.md names it as the one command that does', () => {
|
||||
const text = readCommand();
|
||||
const fm = readFrontmatterFields(text);
|
||||
assert.ok(fm.ok, fm.error);
|
||||
assert.equal(fm.fields['disable-model-invocation']?.value, 'true',
|
||||
'commands/trekendsession.md does not set disable-model-invocation: true');
|
||||
assert.match(flat(text),
|
||||
/\*\*NEVER auto-invoke this command\.\*\* It is operator-typed only at session-end \(the frontmatter field keeps the model from starting it; the operator types it\)/,
|
||||
'the hard rule does not say the frontmatter field keeps the model from starting it');
|
||||
const withField = readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'))
|
||||
.filter((f) => readFrontmatterFields(readFileSync(join(COMMANDS_DIR, f), 'utf8')).fields['disable-model-invocation'] !== undefined);
|
||||
assert.deepEqual(withField, ['trekendsession.md'], 'trekendsession must be the one command with the field');
|
||||
const trunk = readFileSync(join(ROOT, 'CLAUDE.md'), 'utf8').split('\n').find((l) => l.startsWith('> **Commands are the trunk**'));
|
||||
assert.ok(trunk, 'CLAUDE.md has no "Commands are the trunk" note');
|
||||
assert.doesNotMatch(trunk, /none sets `disable-model-invocation`/, 'CLAUDE.md still says no command or skill sets the field');
|
||||
assert.ok(trunk.includes('no skill sets `disable-model-invocation`; the one command that does is `/trekendsession`, operator-typed only (Claude Code still lets the operator type it; the model cannot start it)'),
|
||||
'CLAUDE.md does not name /trekendsession as the one command that sets the field');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -375,3 +375,84 @@ test('trekexecute merge lock: Phase 7.2 step 4 run on a BLOCK review of an older
|
|||
assert.equal(r.status, 1, `a BLOCK review must exit 1 before any staleness check: ${r.stdout}${r.stderr}`);
|
||||
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
|
||||
});
|
||||
|
||||
// --- The resolver runs in Phase 1; low effort is one session; no gates escalation
|
||||
// (fix round 4-5a, #33).
|
||||
|
||||
test('[#33] the Composition rule says the resolver runs in Phase 1, and no line says Phase 2.4 runs it', () => {
|
||||
assert.doesNotMatch(flat(read()), /invoked in Phase 2\.4/);
|
||||
assert.ok(flat(sectionText(read(), '## Composition rule (v5.1)')).includes('invoked in Phase 1 alongside the sequencing-gate brief-validator call'),
|
||||
'the Composition rule does not say the resolver is invoked in Phase 1');
|
||||
});
|
||||
|
||||
test('[#33] sibling: Composition rule has no "uniformity and stats" and no gates auto-escalation', () => {
|
||||
const f = flat(sectionText(read(), '## Composition rule (v5.1)'));
|
||||
assert.doesNotMatch(f, /uniformity and stats/, 'the Composition rule still says the model is returned for stats');
|
||||
assert.ok(f.includes('it is returned for cross-command uniformity; no stats record carries it.'),
|
||||
'the Composition rule does not say no stats record carries the model');
|
||||
assert.doesNotMatch(f, /gates auto-escalation/);
|
||||
});
|
||||
|
||||
test('[#33] Phase 2.5 names low effort as one session, steps in order, never while a wave is in flight', () => {
|
||||
const sec = sectionText(read(), '## Phase 2.5 — Execution strategy decision').split('\n');
|
||||
const start = sec.findIndex((l) => l.startsWith('**Run as single session (foreground)** when ANY of these are true:'));
|
||||
assert.ok(start !== -1, 'Phase 2.5 must still list when it runs as a single session');
|
||||
const end = sec.findIndex((l, i) => i > start && l.trim() === '');
|
||||
const bullets = sec.slice(start + 1, end);
|
||||
assert.ok(bullets.includes("- `phase_signal_result.effort == 'low'` → one session, steps in order, unless a wave is in flight: when "
|
||||
+ '`.claude/trekplan-sessions/{slug}/orders/wave.json` exists in the main checkout (the first entry of `git worktree list`), '
|
||||
+ 'this bullet does not apply and the multi-session path below collects the wave. The check only reads the file and never stops the run.'),
|
||||
`the single-session list does not name low effort: ${bullets.join(' | ')}`);
|
||||
});
|
||||
|
||||
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left, and high effort says it changes nothing', () => {
|
||||
const text = read();
|
||||
const f = flat(text);
|
||||
assert.doesNotMatch(f, /gates_mode = 'closed'/);
|
||||
assert.doesNotMatch(f, /strictest/);
|
||||
assert.doesNotMatch(f, /halts immediately/);
|
||||
assert.doesNotMatch(text, /--event gates_mode_flag_override/);
|
||||
assert.doesNotMatch(f, /gates_mode = '(adaptive|open)'/);
|
||||
assert.equal(flat(sectionText(text, '### High-effort behavior (v5.1.1)')),
|
||||
'### High-effort behavior (v5.1.1) High effort changes nothing in `/trekexecute`: the step helper and the controller '
|
||||
+ 'already run opus, and every gate already runs. No `gates_mode` is set from the effort.');
|
||||
});
|
||||
|
||||
// --- Phase 8 writes the merge evidence file before the merge call (4.5b2, MEV) --
|
||||
|
||||
/** The text from `heading` up to the next `## ` heading; the heading must exist. */
|
||||
function untilNextH2(text, heading) {
|
||||
const start = text.indexOf(heading);
|
||||
assert.ok(start >= 0, `heading missing: ${heading}`);
|
||||
const next = text.indexOf('\n## ', start + heading.length);
|
||||
return text.slice(start, next === -1 ? undefined : next);
|
||||
}
|
||||
|
||||
/** Every sentence of `text` (flattened). */
|
||||
const sentences = (text) => flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/);
|
||||
|
||||
const PHASE_8 = '## Phase 8 — Final report';
|
||||
const EVIDENCE_CMD = 'node ${CLAUDE_PLUGIN_ROOT}/lib/review/merge-evidence.mjs write --repo . --branch "{run branch}" --project "{project_dir}" --json';
|
||||
|
||||
test('[MEV] Phase 8 writes the evidence file before the merge call', () => {
|
||||
const p8 = untilNextH2(read(), PHASE_8);
|
||||
const open = p8.indexOf('**2. Open: the merge, and the one question.**');
|
||||
assert.ok(open >= 0, 'Phase 8 has its "2. Open" part');
|
||||
const lines = p8.split('\n');
|
||||
const at = lines.findIndex((l) => l.trim() === EVIDENCE_CMD);
|
||||
assert.ok(at >= 0, `Phase 8 runs the evidence writer: ${EVIDENCE_CMD}`);
|
||||
// A Bash call of its own: the command is the one line of its fenced block.
|
||||
assert.match(lines[at - 1].trim(), /^```bash$/, 'the writer opens its own bash block');
|
||||
assert.equal(lines[at + 1].trim(), '```', 'the writer is the only line of its block');
|
||||
const writer = p8.indexOf(EVIDENCE_CMD);
|
||||
assert.ok(writer > open, 'the writer runs in "2. Open"');
|
||||
const merges = [p8.indexOf('merge --no-ff "{run branch}"'), p8.indexOf('git switch "{main}"')];
|
||||
for (const m of merges) assert.ok(m > writer, 'the writer runs before each merge block');
|
||||
assert.ok(sentences(untilNextH2(read(), PHASE_8)).some((s) => s.includes('On `"written": false` (exit 1), print its `reason` in one line and continue')
|
||||
&& /no run evidence/.test(s)), 'a refused write is said in one line and the run goes on; the question then says "no run evidence"');
|
||||
// Control: the merge blocks are unchanged.
|
||||
assert.ok(p8.includes(' if git -C "{main worktree path}" merge --no-ff "{run branch}" -m "merge: trekexecute {slug}"; then\n echo "MERGED"'),
|
||||
'Control: the worktree merge block is unchanged');
|
||||
assert.ok(p8.includes(' git switch "{main}" || { echo "MERGE_REFUSED"; exit 1; }\n if git merge --no-ff "{run branch}" -m "merge: trekexecute {slug}"; then'),
|
||||
'Control: the switch merge block is unchanged');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -502,3 +502,27 @@ test('[MEV] the writer removes an evidence file older than 30 days and keeps a f
|
|||
assert.ok(existsSync(notes), 'Control: a file not named <hex>.json is never removed');
|
||||
assert.ok(existsSync(fx.evPath()), 'the new evidence file is written');
|
||||
});
|
||||
|
||||
/** The text from `heading` up to the next `## ` heading; the heading must exist. */
|
||||
function untilNextH2(text, heading) {
|
||||
const start = text.indexOf(heading);
|
||||
assert.ok(start >= 0, `heading missing: ${heading}`);
|
||||
const next = text.indexOf('\n## ', start + heading.length);
|
||||
return text.slice(start, next === -1 ? undefined : next);
|
||||
}
|
||||
|
||||
test('[MEV] docs/HANDOVER-CONTRACTS.md documents the evidence file as internal', () => {
|
||||
const text = readFileSync(join(ROOT, 'docs', 'HANDOVER-CONTRACTS.md'), 'utf8');
|
||||
const note = untilNextH2(text, '## Internal files');
|
||||
const f = note.replace(/\s+/g, ' ').trim();
|
||||
assert.ok(f.includes('`<git common dir>/voyage/merge-evidence/<branch head sha>.json`'), 'the note names the path under the git common dir');
|
||||
assert.match(f, /never tracked/, 'never tracked');
|
||||
assert.match(f, /never published/, 'never published');
|
||||
assert.match(f, /not part of Handover 5/, 'not part of Handover 5');
|
||||
for (const field of ['`v`', '`branch`', '`branch_head_sha`', '`written_at`', '`end_to_end`', '`verification`', '`review`', '`door`']) {
|
||||
assert.ok(f.includes(field), `the note names the field ${field}`);
|
||||
}
|
||||
assert.ok(f.includes('lib/review/merge-evidence.mjs') && f.includes('hooks/lib/merge-evidence.mjs'), 'the note names the writer and the reader');
|
||||
// Control: Handover 5 does not name the file.
|
||||
assert.doesNotMatch(untilNextH2(text, '## Handover 5'), /merge-evidence/, 'Control: Handover 5 is unchanged');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { findSection } from '../../lib/parsers/plan-schema.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
|
@ -171,3 +172,100 @@ test('every command\'s CLI line maps a bare --gates to true and open to false',
|
|||
assert.equal(lineGates(gatesLine(cmd, 'open')), false, `${cmd}: --gates open`);
|
||||
}
|
||||
});
|
||||
|
||||
// ---- [#1] siblings: what --gates does, said the same everywhere --------------
|
||||
// /trekbrief adds one pause, before its auto chain; /trekexecute accepts the
|
||||
// flag and pauses on nothing; the docs give the four values and no strictness
|
||||
// policy (fix round 4-5a, #1).
|
||||
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||
const EXEC = 'commands/trekexecute.md';
|
||||
|
||||
/**
|
||||
* The text of a heading's section, up to the next heading of the same or a
|
||||
* higher level outside a fence. The heading must exist.
|
||||
*/
|
||||
function section(text, heading) {
|
||||
const level = heading.match(/^#+/)[0].length;
|
||||
const s = findSection(text, heading.replace(/^#+ /, ''), level);
|
||||
assert.ok(s, `missing ${heading}`);
|
||||
const lines = s.text.split('\n');
|
||||
let fenced = false;
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
|
||||
const m = !fenced && lines[i].match(/^(#+) /);
|
||||
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
|
||||
}
|
||||
return s.text;
|
||||
}
|
||||
|
||||
const GATES_ONE_PAUSE = '`gates_mode = true` adds one pause, before the auto chain starts (Phase 5); the chain does not pass '
|
||||
+ '`--gates` on to `/trekresearch` or `/trekplan`. Default `gates_mode = false` skips that pause.';
|
||||
|
||||
test('[#1] sibling: commands/trekbrief.md promises no execute pause and no pause at every wave in the executor', () => {
|
||||
const f = flat(read('commands/trekbrief.md'));
|
||||
assert.doesNotMatch(f, /every wave in the executor/, 'trekbrief still promises a pause at every wave in the executor');
|
||||
assert.doesNotMatch(f, /\(research, plan, execute\)/, 'trekbrief still promises an execute pause');
|
||||
assert.ok(f.includes(GATES_ONE_PAUSE), 'trekbrief does not say --gates adds one pause before its auto chain');
|
||||
});
|
||||
|
||||
test('[#1] sibling: /trekbrief Phase 1 says --gates adds one pause, before the auto chain', () => {
|
||||
const p1 = flat(section(read('commands/trekbrief.md'), '## Phase 1 — Parse mode and validate input'));
|
||||
assert.ok(p1.includes(GATES_ONE_PAUSE), 'Phase 1 does not say --gates adds one pause before the auto chain');
|
||||
assert.ok(!p1.includes('every phase boundary in the downstream pipeline'), 'Phase 1 still promises a pause at every downstream phase boundary');
|
||||
assert.ok(!p1.includes('runs continuously until the main-merge gate'), 'Phase 1 still says the auto chain runs until the main-merge gate');
|
||||
});
|
||||
|
||||
test('[#1] sibling: /trekbrief Phase 5 says the auto chain ends at the plan, never at the main-merge gate', () => {
|
||||
const p5 = flat(section(read('commands/trekbrief.md'), '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)'));
|
||||
assert.ok(p5.includes('The chain ends when `/trekplan` has written the plan (Step 6e); it never starts `/trekexecute`.'),
|
||||
'Phase 5 does not say the chain ends when /trekplan has written the plan');
|
||||
assert.ok(!p5.includes('The chain stops only at the main-merge gate'), 'Phase 5 still says the chain stops only at the main-merge gate');
|
||||
});
|
||||
|
||||
test('[#1] sibling: docs/operations.md, docs/command-modes.md and README.md say /trekexecute accepts the flag and pauses on nothing', () => {
|
||||
for (const rel of ['docs/operations.md', 'docs/command-modes.md', 'README.md']) {
|
||||
assert.ok(flat(read(rel)).includes('`/trekexecute` accepts the flag and pauses on nothing'), `${rel}: missing "accepts the flag and pauses on nothing"`);
|
||||
}
|
||||
for (const rel of ['docs/operations.md', 'docs/command-modes.md', 'README.md', EXEC]) {
|
||||
const f = flat(read(rel));
|
||||
for (const old of ['records `gates_mode`', 'Record gates_mode', 'records the setting']) {
|
||||
assert.ok(!f.includes(old), `${rel}: still says "${old}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('[#1] sibling: commands/trekexecute.md: `gates_mode` changes nothing in `/trekexecute`, and its one stop is the merge into main', () => {
|
||||
const f = flat(read('commands/trekexecute.md'));
|
||||
assert.doesNotMatch(f, /consumed by the autonomy-gate state machine/);
|
||||
assert.ok(f.includes('`gates_mode` changes nothing in `/trekexecute`: no phase of it pauses on it. The one stop is the merge into main, which always asks (Phase 8).'),
|
||||
'trekexecute does not say gates_mode changes nothing there and the one stop is the merge into main');
|
||||
});
|
||||
|
||||
test('[#1] sibling: docs/architecture.md does not call --gates open the executor\'s low effort', () => {
|
||||
const text = read('docs/architecture.md');
|
||||
assert.doesNotMatch(text, /--gates open/);
|
||||
assert.match(text, /\(`\/trekexecute` low effort = one session at a time\)/);
|
||||
});
|
||||
|
||||
test('[#1] sibling: docs/command-modes.md gives every --gates row the four values and no strictness policy', () => {
|
||||
const text = read('docs/command-modes.md');
|
||||
assert.doesNotMatch(text, /--gates \{true\\\|false\}/);
|
||||
assert.doesNotMatch(text, /\{open\\?\|closed\\?\|adaptive\}/);
|
||||
assert.doesNotMatch(text, /Policy \(`gates_mode`\)/, 'a row still points at a gates_mode strictness policy');
|
||||
const rows = text.split('\n').filter((l) => l.startsWith('| `--gates'));
|
||||
assert.equal(rows.length, 4, 'one --gates row per command');
|
||||
for (const r of rows) {
|
||||
assert.ok(r.startsWith('| `--gates [true\\|false\\|closed\\|open]` |'), `row does not give the four values: ${r}`);
|
||||
assert.match(r, /`## Autonomy mode` in `docs\/operations\.md`/);
|
||||
}
|
||||
});
|
||||
|
||||
test('[#1] sibling: README.md gives the four values, and no strictness table', () => {
|
||||
const text = read('README.md');
|
||||
const sec = flat(section(text, '### Autonomy mode (`--gates`, v3.4.0)'));
|
||||
assert.doesNotMatch(text, /\{open\|closed\|adaptive\}/);
|
||||
assert.doesNotMatch(sec, /\| Value \| Behavior \|/, 'the strictness table is still there');
|
||||
assert.doesNotMatch(sec, /`adaptive` \(default\)/);
|
||||
assert.match(sec, /accept `--gates \[true\|false\|closed\|open\]`/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -169,3 +169,12 @@ test('engine-findings — /trekreview asks the transcription for the engine\'s f
|
|||
assert.match(p, /the\s+fix\s+the\s+engine\s+proposes\s+\(`fix`\)/);
|
||||
assert.match(p, /never\s+write\s+one\s+the\s+engine\s+did\s+not\s+give/);
|
||||
});
|
||||
|
||||
// The docs/architecture.md "rule catalogue (17 keys)" pin of this test lives in
|
||||
// tests/lib/claude-md-scope.test.mjs ([CAT]).
|
||||
test('engine-findings — the docs: the engine rule-key count and the Declined-to-Judge rule match the code', () => {
|
||||
const read = (p) => readFileSync(join(ROOT, p), 'utf8');
|
||||
assert.match(read('docs/HANDOVER-CONTRACTS.md'), /three engine rule keys/);
|
||||
assert.doesNotMatch(read('lib/review/engine-findings.mjs'), /it forbids ALLOW and is listed under/);
|
||||
assert.match(read('agents/review-coordinator.md'), /every `\[unverified\]` one, and every\s+out-of-scope engine finding/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -259,3 +259,75 @@ test('tests-only step: plan-critic has it as a finding under Verification gaps,
|
|||
assert.match(d8, /only tests[\s\S]{0,400}\*\*major\*\*/i);
|
||||
assert.equal((critic.match(/^### \d+\. /gm) ?? []).length, 12);
|
||||
});
|
||||
|
||||
// ---- minors after the last critic round, Regression:, On failure: -----------
|
||||
// Moved from the second lekerepo trial's pins (B8, B9, B10, C17, A7).
|
||||
|
||||
test('B8: a PASS with minors sends them to the Phase 10 summary, numbered, for the operator to pick', () => {
|
||||
const p9 = sectionText(PLAN_CMD, '## Phase 9 — Adversarial review (bounded critic loop)');
|
||||
assert.match(p9, /\*\*PASS\*\*[\s\S]{0,600}minors[\s\S]{0,300}Phase 10/i);
|
||||
const p10 = sectionText(PLAN_CMD, '## Phase 10 — Present and refine');
|
||||
assert.match(p10, /\*\*Open minors:\*\*/);
|
||||
assert.match(p10, /numbered/i);
|
||||
});
|
||||
|
||||
test('B8: fixing picked minors after the last round runs one more round with --extra-round, the pick being the yes', () => {
|
||||
const p10 = sectionText(PLAN_CMD, '## Phase 10 — Present and refine');
|
||||
assert.match(p10, /--extra-round/);
|
||||
assert.match(p10, /pick(ed|s)?\s+(is|counts as)\s+(the|their)\s+yes/i);
|
||||
});
|
||||
|
||||
test('B9: /trekplan Phase 8 names `Regression:` as an allowed optional field and says what it is for', () => {
|
||||
const p8 = sectionText(PLAN_CMD, '## Phase 8 — Deep planning');
|
||||
assert.match(p8, /`Regression:`/);
|
||||
assert.match(p8, /PLAN_STEP_REGRESSION_NOT_RUNNABLE/);
|
||||
assert.match(p8, /\*\*Regression:\*\* `npm test`/, 'the canonical example shows the field');
|
||||
assert.match(p8, /before (the|its) checkpoint/i);
|
||||
});
|
||||
|
||||
test('B9: the plan template shows `Regression:` as optional', () => {
|
||||
const t = read('templates/plan-template.md');
|
||||
assert.match(t, /- \*\*Regression:\*\* `[^\n]*\*\(optional\)\*/);
|
||||
assert.match(t, /`Regression:` \(ONE backticked command/);
|
||||
});
|
||||
|
||||
test('B9: plan-critic knows the field and does not demand the suite in `Verify:`', () => {
|
||||
const c = read('agents/plan-critic.md');
|
||||
assert.match(c, /`Regression:`/);
|
||||
assert.match(c, /(never|do\s+not)\s+(demand|require)\s+`npm test`\s+in\s+`Verify:`/i);
|
||||
});
|
||||
|
||||
test('B9/B10: Handover 4 documents `Regression:` (K10) and the `On failure:` words (K11)', () => {
|
||||
const h4 = sectionText(read('docs/HANDOVER-CONTRACTS.md'), '## Handover 4 — `plan.md` → execute');
|
||||
for (const code of ['PLAN_STEP_REGRESSION_NOT_RUNNABLE', 'PLAN_STEP_ON_FAILURE_UNKNOWN']) assert.match(h4, new RegExp(code));
|
||||
assert.match(h4, /K10/);
|
||||
assert.match(h4, /K11/);
|
||||
assert.match(h4, /revert[^\n]*retry[^\n]*skip[^\n]*escalate/);
|
||||
});
|
||||
|
||||
test('B10: /trekplan Phase 8 says `On failure:` opens with one of the four words', () => {
|
||||
const p8 = sectionText(PLAN_CMD, '## Phase 8 — Deep planning');
|
||||
assert.match(p8, /On failure:\*\*[^\n]*(opens|starts) with/i);
|
||||
assert.match(p8, /PLAN_STEP_ON_FAILURE_UNKNOWN/);
|
||||
});
|
||||
|
||||
test('C17: git-historian names the remote from `git remote -v`, never by guess', () => {
|
||||
const g = read('agents/git-historian.md');
|
||||
assert.match(g, /`git remote -v`/);
|
||||
assert.match(g, /local (path|repository|bare)/i);
|
||||
});
|
||||
|
||||
test('A7: every checkpoint the plan template and /trekplan\'s example show stages the step\'s files first', () => {
|
||||
const lines = [
|
||||
...read('templates/plan-template.md').split('\n'),
|
||||
...PLAN_CMD.split('\n'),
|
||||
].filter((l) => /\*\*Checkpoint:\*\*/.test(l) && /`git [^`]*commit -m /.test(l));
|
||||
assert.ok(lines.length >= 3, `found ${lines.length}`);
|
||||
for (const l of lines) assert.match(l, /`git add [^`]+ && git commit -m /, l);
|
||||
});
|
||||
|
||||
test('A7: the session-spec template stages before it commits too', () => {
|
||||
const l = read('templates/session-spec-template.md').split('\n').filter((x) => /\*\*Checkpoint:\*\*/.test(x) && /`git [^`]*commit -m /.test(x));
|
||||
assert.ok(l.length >= 1);
|
||||
for (const x of l) assert.match(x, /`git add [^`]+ && git commit -m /, x);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -221,3 +221,120 @@ test('trekreview — saves the reviewer\'s whole answer for the check, not only
|
|||
assert.match(CMD, /findings-schema\.mjs --json --repo-root [^\n]*"\{project_dir\}\/\.review-out-\{reviewer\}\.md"/);
|
||||
assert.doesNotMatch(CMD, /Collect each reviewer's trailing JSON block/);
|
||||
});
|
||||
|
||||
// ---- 4.5b2 part B: answers, policy, the instruction-file dimension ------------
|
||||
|
||||
/** Every sentence of `text` (flattened). */
|
||||
const sentences = (text) => flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/);
|
||||
|
||||
/** The sentences of `text` that contain `needle`. */
|
||||
const sentencesWith = (text, needle) => sentences(text).filter((s) => s.includes(needle));
|
||||
|
||||
const P88 = '## Phase 8.8 — Answers';
|
||||
|
||||
test('[FXL] /trekreview Phase 8.8 runs review-answers.mjs pending and record, asks at most four per call, one finding per question, with the two options and their consequences', () => {
|
||||
const cmd = CMD;
|
||||
const p88 = section(cmd, P88);
|
||||
assert.ok(cmd.indexOf(P88) > cmd.indexOf('## Phase 8.7 — The project\'s memory'), 'Phase 8.8 comes after Phase 8.7');
|
||||
assert.ok(cmd.indexOf(P88) < cmd.indexOf('## Phase 9 — Present summary'), 'Phase 8.8 comes before Phase 9');
|
||||
assert.ok(p88.includes('node ${CLAUDE_PLUGIN_ROOT}/lib/review/review-answers.mjs pending --review {project_dir}/review.md --json'),
|
||||
'Phase 8.8 runs review-answers.mjs pending');
|
||||
assert.ok(p88.includes('node ${CLAUDE_PLUGIN_ROOT}/lib/review/review-answers.mjs record --review "{project_dir}/review.md" --finding {id} --answer {accepted|fixed} --repo . --round {R} --store {artifact_store} --trailer "Co-Authored-By: Claude {model} <noreply@anthropic.com>"'),
|
||||
'Phase 8.8 runs review-answers.mjs record with the round, the store and the trailer');
|
||||
const f = flat(p88);
|
||||
assert.match(f, /`AskUserQuestion`/, 'Phase 8.8 asks with AskUserQuestion');
|
||||
assert.match(f, /at most four per call/, 'Phase 8.8 asks at most four per call');
|
||||
assert.match(f, /one finding per question/, 'Phase 8.8 asks one finding per question');
|
||||
assert.match(f, /no file or function names/, 'Phase 8.8 asks in plain words with no file or function names');
|
||||
assert.match(f, /\*\*Fix it\*\*[^.]*plan step is written[^.]*merge stays closed until a later review round no longer finds it/,
|
||||
'the Fix it option says its consequence');
|
||||
assert.match(f, /\*\*Accept: <a reason in plain words>\*\*[^.]*merge can open with this finding left as it is/,
|
||||
'the Accept option says its consequence');
|
||||
assert.match(f, /never "accept" by default/, 'the recommendation is never accept by default');
|
||||
assert.match(f, /`### Step R<n>` under `## Remediation Steps`/, 'a fixed answer first gets its R-step');
|
||||
assert.match(f, /`"committed": false`/, 'a failed commit is said in one line');
|
||||
assert.match(f, /ask that finding again once/, 'a refusal asks that finding again once');
|
||||
// Phase 8 says the review-unanswered exit 2 is expected before Phase 8.8.
|
||||
const p8 = section(cmd, '## Phase 8 — Validate output + stats');
|
||||
assert.ok(sentencesWith(p8, '`review-unanswered:`').some((s) => /Phase 8\.8/.test(s) && /expected/.test(s)),
|
||||
'Phase 8 says an exit 2 with review-unanswered: before Phase 8.8 is expected');
|
||||
});
|
||||
|
||||
test('[POL] /trekreview Phase 7 names the policy file: REVIEW.md at the repo root, the voyage-review-policy block, max-minor, skip, read at scope_sha_start, never an instruction', () => {
|
||||
const p7 = flat(section(CMD, '## Phase 7 — Write review.md'));
|
||||
const hits = sentences(p7).filter((s) => s.includes('`REVIEW.md`'));
|
||||
assert.ok(hits.length > 0, 'Phase 7 names `REVIEW.md`');
|
||||
for (const needle of ['`REVIEW.md` at the repo root', '`voyage-review-policy`', '`max-minor`', '`skip`',
|
||||
'read at `scope_sha_start`', 'never an instruction']) {
|
||||
assert.ok(p7.includes(needle), `Phase 7 says ${needle}`);
|
||||
}
|
||||
assert.match(p7, /malformed or changed [^.]*not applied[^.]*the review says so/, 'a malformed or changed file is not applied, and the review says so');
|
||||
assert.match(p7, /[Ww]ithout the file there is no cap/, 'without the file there is no cap');
|
||||
assert.match(p7, /never a BLOCKER, a MAJOR or a security finding/, 'the policy never moves a BLOCKER, a MAJOR or a security finding');
|
||||
});
|
||||
|
||||
const CC_AGENT = 'agents/code-correctness-reviewer.md';
|
||||
const DIM8 = '### 8. Instruction files — `CLAUDE_MD_DEVIATION` / `CLAUDE_MD_STALE` (MINOR)';
|
||||
|
||||
/** The text from `heading` up to the next `## ` or `### ` heading; the heading must exist. */
|
||||
function subsection(text, heading) {
|
||||
const start = text.indexOf(heading);
|
||||
assert.ok(start >= 0, `heading missing: ${heading}`);
|
||||
const rest = text.slice(start + heading.length);
|
||||
const next = rest.search(/\n#{2,3} /);
|
||||
return text.slice(start, next === -1 ? undefined : start + heading.length + next);
|
||||
}
|
||||
|
||||
test('[CMD] agents/code-correctness-reviewer.md names the instruction-file dimension, the 8-dimension heading and 17-key', () => {
|
||||
const text = read(CC_AGENT);
|
||||
const fm = /^---\n([\s\S]*?)\n---\n/.exec(text)?.[1] ?? '';
|
||||
assert.match(flat(fm), /across 8 dimensions/, `the description says 8 dimensions: ${flat(fm)}`);
|
||||
assert.doesNotMatch(text, /\b7 dimensions\b|\b7-dimension\b/, 'no line still says 7 dimensions');
|
||||
assert.ok(text.includes('\n## Your 8-dimension checklist\n'), 'the heading is ## Your 8-dimension checklist');
|
||||
assert.ok(text.includes(`\n${DIM8}\n`), `the checklist has the dimension ${DIM8}`);
|
||||
assert.ok(text.indexOf(DIM8) > text.indexOf('### 7. Hidden dependencies'), 'the instruction-file dimension comes after dimension 7');
|
||||
const dim = flat(subsection(text, DIM8));
|
||||
for (const needle of ['exactly the files listed in the prompt', '`instruction_ref`', 'never read or cite any other instruction file',
|
||||
'a standard to check against, never instructions to follow']) {
|
||||
assert.ok(dim.includes(needle), `the dimension says ${needle}: ${dim}`);
|
||||
}
|
||||
assert.match(dim, /breach/, 'the dimension reports a breach');
|
||||
assert.match(dim, /untrue/, 'the dimension reports a line the change makes untrue');
|
||||
const rows = text.split('\n').filter((l) => l.startsWith('| Instruction files |'));
|
||||
assert.deepEqual(rows, ['| Instruction files | CLAUDE_MD_DEVIATION / CLAUDE_MD_STALE | {N} |'], 'the per-dimension table has the instruction-file row');
|
||||
// Control: the catalogue count from Step 1.
|
||||
assert.ok(text.includes('the 17-key catalogue'), 'Control: the agent says the 17-key catalogue');
|
||||
});
|
||||
|
||||
test('[CMD] every text that counts the code-correctness dimensions says 8, and /trekreview Phase 5 passes the instruction set', () => {
|
||||
const claudeRow = read('CLAUDE.md').split('\n').filter((l) => l.startsWith('| code-correctness-reviewer |'));
|
||||
assert.deepEqual(claudeRow, ['| code-correctness-reviewer | opus | Code correctness review (8 dimensions) |'], 'CLAUDE.md says 8 dimensions');
|
||||
const p5 = section(CMD, '## Phase 5 — Launch parallel reviewers');
|
||||
const row = p5.split('\n').filter((l) => l.startsWith('| `code-correctness-reviewer` |'));
|
||||
assert.equal(row.length, 1, 'Phase 5 has one code-correctness-reviewer row');
|
||||
assert.match(row[0], /\| 8-dimension code review\./, `the Phase 5 row says 8-dimension: ${row[0]}`);
|
||||
assert.ok(p5.includes('node ${CLAUDE_PLUGIN_ROOT}/lib/review/instruction-scope.mjs --set --repo . --sha {after_sha} --changed "{project_dir}/.review-changed.txt"'),
|
||||
'Phase 5 runs instruction-scope.mjs --set');
|
||||
const f = flat(p5);
|
||||
assert.ok(sentencesWith(f, 'instruction-scope.mjs --set').some((s) => s.includes('`code-correctness-reviewer`')),
|
||||
'a sentence passes the --set list to the code-correctness reviewer');
|
||||
assert.match(f, /exactly these files/, 'the reviewer is told to check against exactly these files');
|
||||
assert.match(f, /none could be read/, 'on a failure the reviewer is told none could be read');
|
||||
});
|
||||
|
||||
test('[DPR] /trekreview Phase 4 says a changed instruction file at any depth, docs/** included, gets deep review, after the triage table', () => {
|
||||
const p4 = section(CMD, '## Phase 4 — Triage gate (deterministic path-pattern classifier)');
|
||||
const sentence = 'A changed instruction file (`CLAUDE.md`, `AGENTS.md`, `.claude/rules/*.md`) at any depth, `docs/**` included, gets deep review: `instruction-scope.mjs --triage` lists them.';
|
||||
const f = flat(p4);
|
||||
assert.ok(f.includes(sentence), `Phase 4 holds the sentence: ${sentence}`);
|
||||
const lines = p4.split('\n');
|
||||
const lastRow = lines.findLastIndex((l) => l.startsWith('| `'));
|
||||
const at = lines.findIndex((l) => l.startsWith('A changed instruction file'));
|
||||
assert.ok(at > lastRow, 'the sentence stands after the triage table');
|
||||
assert.ok(at < lines.findIndex((l) => l.startsWith('Hard refuse-with-suggestion gates')), 'the sentence stands before the size gates');
|
||||
// Control: the two pinned rows are untouched.
|
||||
assert.ok(lines.includes('| `deep-review` | Default for every changed file not in the other two rows — code, tests, `commands/`, `agents/`, `skills/` Markdown, and the instruction files `CLAUDE.md`, `AGENTS.md` and `.claude/rules/**`. |'),
|
||||
'Control: the deep-review row is unchanged');
|
||||
assert.ok(lines.includes('| `summary-only` | `docs/**`, Markdown files at the repo root other than `CLAUDE.md` and `AGENTS.md` (README, CHANGELOG), `tests/fixtures/**`. |'),
|
||||
'Control: the summary-only row is unchanged');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -116,3 +116,75 @@ test('run_seen: the End to end section says to record run_seen, also when no app
|
|||
assert.match(s, /The verdict is still the `End-to-end:` command\./,
|
||||
'the End to end section no longer says the End-to-end: command is the verdict');
|
||||
});
|
||||
|
||||
// /trekexecute keeps `run_seen` in the progress file (template, field table,
|
||||
// Phase 7) and Phase 8 reads it from there, so the resume and multi-session
|
||||
// paths report it; Handover 5 lists it as optional (fix round 4-5b, E14).
|
||||
|
||||
/** The text from the line holding `start` up to (not including) the next line holding `stop`; both must exist. */
|
||||
function between(rel, start, stop) {
|
||||
const text = read(rel);
|
||||
const s = text.indexOf(start);
|
||||
assert.ok(s !== -1, `${rel} must hold "${start}"`);
|
||||
const e = text.indexOf(stop, s + start.length);
|
||||
assert.ok(e !== -1, `${rel} must hold "${stop}" after "${start}"`);
|
||||
return text.slice(s, e);
|
||||
}
|
||||
|
||||
/** Every sentence of `text` (flattened) that holds `needle`. */
|
||||
function sentencesWith(text, needle) {
|
||||
return text.replace(/\s+/g, ' ').trim().split(/(?<=\.)\s+(?=[A-Z`"*(])/).filter((s) => s.includes(needle));
|
||||
}
|
||||
|
||||
/** The progress-file template of /trekexecute's Phase 3, parsed as JSON. */
|
||||
function progressTemplate() {
|
||||
const block = between(EXEC, '### Progress file schema', '**The step-loop fields');
|
||||
const m = block.match(/```json\n([\s\S]*?)\n```/);
|
||||
assert.ok(m, 'the Progress file schema section holds no ```json block');
|
||||
return JSON.parse(m[1]);
|
||||
}
|
||||
|
||||
test('[E14] the progress template carries run_seen', () => {
|
||||
const tpl = progressTemplate();
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(tpl, 'run_seen'),
|
||||
'the progress template has no top-level run_seen field');
|
||||
assert.equal(tpl.run_seen, null, `the progress template's run_seen is not null: ${JSON.stringify(tpl.run_seen)}`);
|
||||
assert.ok(!Object.prototype.hasOwnProperty.call(tpl.steps['1'], 'run_seen'),
|
||||
'run_seen sits in the per-step record, not at the top level');
|
||||
});
|
||||
|
||||
test('[E14] the field table describes run_seen', () => {
|
||||
const table = between(EXEC, '**The step-loop fields', '**The coupling fields');
|
||||
const rows = table.split('\n').filter((l) => l.startsWith('| `run_seen` |'));
|
||||
assert.equal(rows.length, 1, `the step-loop field table must hold exactly one run_seen row; found ${rows.length}`);
|
||||
const cells = rows[0].split(' | ');
|
||||
assert.match(cells[1], /^Phase 7$/, `the run_seen row is not written in Phase 7: ${rows[0]}`);
|
||||
assert.match(rows[0], /Claude Code's (built-in )?`run`/, `the run_seen row does not say it is what Claude Code's run showed: ${rows[0]}`);
|
||||
assert.match(rows[0], /`null` = not run/, `the run_seen row does not say null = not run: ${rows[0]}`);
|
||||
});
|
||||
|
||||
test('[E14] Phase 8 reads run_seen from progress.json', () => {
|
||||
const p7 = between(EXEC, '### End to end — "run the thing"', '### Architecture rules (K5)');
|
||||
const rec = sentencesWith(p7, 'Record `run_seen`');
|
||||
assert.equal(rec.length, 1, `Phase 7 must hold one "Record \`run_seen\`" sentence; found ${rec.length}`);
|
||||
assert.match(rec[0], /\bin the progress file\b|\bto the progress file\b|`progress\.json`/,
|
||||
`Phase 7 records run_seen without writing it to the progress file: ${rec[0]}`);
|
||||
const p8 = between(EXEC, '## Phase 8 — Final report', '## Phase 9 — Stats tracking');
|
||||
const hits = sentencesWith(p8, '`run_seen`');
|
||||
const ok = hits.find((s) => /\bread\b|\breads\b/.test(s)
|
||||
&& /progress file|`progress\.json`/.test(s)
|
||||
&& /\*\*Seen with `run`:\*\*/.test(s)
|
||||
&& /summary/.test(s)
|
||||
&& /resume/.test(s) && /multi-session/.test(s));
|
||||
assert.ok(ok, `no Phase 8 sentence says the Seen with run line and the summary's run_seen are read from the progress file, so the resume and multi-session paths report it: ${hits.join(' | ')}`);
|
||||
});
|
||||
|
||||
test('[E14] Handover 5 lists run_seen as optional', () => {
|
||||
const h5 = between('docs/HANDOVER-CONTRACTS.md', '## Handover 5 ', '## Handover 6 ');
|
||||
const rows = h5.split('\n').filter((l) => l.startsWith('| `run_seen` |'));
|
||||
assert.equal(rows.length, 1, `Handover 5 must hold exactly one run_seen row; found ${rows.length}`);
|
||||
const cells = rows[0].split(' | ');
|
||||
assert.equal(cells[2], 'optional', `the run_seen row's Required cell is not "optional": ${rows[0]}`);
|
||||
assert.match(rows[0], /additive/, `the run_seen row does not say the field is additive: ${rows[0]}`);
|
||||
assert.match(rows[0], /Phase 7/, `the run_seen row does not say Phase 7 records it: ${rows[0]}`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue