* fix(#1863): use named flags for state.* calls in executor + workflows The named-only state-command router (parseNamedArgs) silently drops positional args, so state.cjs threw its required-arg error and metrics/decisions/blockers/session continuity were never recorded. Convert record-metric / add-decision / add-blocker / record-session in agents/gsd-executor.md to the named-flag form (mirroring execute-plan.md), and fix the two remaining positional record-session calls in gsd-core/workflows/milestone-summary.md and forensics.md. Recapture the golden-install-parity fixtures and size baselines for the edited files. Also fix a pre-existing detached-rebuild handle leak in tests/graphify-auto-update.slow.test.cjs: three dispatch tests returned after observing only the synchronous "running" status without awaiting the detached rebuild's terminal state. That leak was latent until the new #1863 regression block's added runtime shifted --test-force-exit timing and surfaced it as a non-zero chunk exit. The three tests now await terminal status via the file's existing waitForBuildStatus helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#1863): add changeset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
99 lines
4.7 KiB
JavaScript
99 lines
4.7 KiB
JavaScript
/**
|
|
* gsd-executor agent — MVP+TDD gate section contract
|
|
* Verifies the agent definition contains a section instructing the executor
|
|
* to halt and report when the runtime gate trips.
|
|
*/
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const AGENT = path.join(__dirname, '..', 'agents', 'gsd-executor.md');
|
|
const REF = path.join(__dirname, '..', 'gsd-core', 'references', 'execute-mvp-tdd.md');
|
|
|
|
describe('gsd-executor — MVP+TDD gate section', () => {
|
|
const content = fs.readFileSync(AGENT, 'utf-8');
|
|
|
|
test('agent defines an MVP+TDD Gate section', () => {
|
|
assert.match(content, /MVP\+TDD\s*Gate|MVP[\s-]?TDD[\s-]?gate/i, 'must label the gate');
|
|
});
|
|
|
|
test('agent instructs halt-and-report when gate trips', () => {
|
|
assert.match(content, /halt|stop[^\n]*gate|gate[^\n]*halt/i, 'must instruct halt');
|
|
assert.match(content, /report|surface|emit/i, 'must instruct report');
|
|
});
|
|
|
|
test('agent references execute-mvp-tdd.md', () => {
|
|
assert.match(content, /execute-mvp-tdd\.md/, 'must reference the gate semantics file');
|
|
});
|
|
|
|
test('referenced file exists on disk', () => {
|
|
assert.ok(fs.existsSync(REF), `${REF} must exist`);
|
|
});
|
|
});
|
|
|
|
describe('gsd-executor — state.* calls use the named-only router form (#1863 regression)', () => {
|
|
// The runtime state-command router (gsd-core/bin/lib/state-command-router.cjs)
|
|
// parses record-metric / add-decision / add-blocker / record-session named-only
|
|
// via parseNamedArgs. Positional values are silently dropped, so state.cjs then
|
|
// throws its required-arg error and metrics/decisions/blockers/session continuity
|
|
// are never recorded. Each invocation in the executor agent must therefore pass
|
|
// the named flags the router expects (mirrors gsd-core/workflows/execute-plan.md).
|
|
const content = fs.readFileSync(AGENT, 'utf-8');
|
|
|
|
// Capture a `gsd_run query state.<cmd> ...` invocation, including backslash-continued lines.
|
|
function invocation(cmd) {
|
|
const re = new RegExp(String.raw`gsd_run query state\.${cmd}\b(?:[^\r\n]*\\\r?\n)*[^\r\n]*`);
|
|
const m = content.match(re);
|
|
assert.ok(m, `executor must invoke state.${cmd}`);
|
|
return m[0];
|
|
}
|
|
|
|
test('record-metric passes --phase/--plan/--duration/--tasks/--files', () => {
|
|
const call = invocation('record-metric');
|
|
for (const flag of ['--phase', '--plan', '--duration', '--tasks', '--files']) {
|
|
assert.ok(call.includes(flag), `record-metric must pass ${flag}, got:\n${call}`);
|
|
}
|
|
});
|
|
|
|
test('add-decision passes --summary (or --summary-file)', () => {
|
|
assert.match(invocation('add-decision'), /--summary(?:-file)?\b/);
|
|
});
|
|
|
|
test('add-blocker passes --text (or --text-file)', () => {
|
|
assert.match(invocation('add-blocker'), /--text(?:-file)?\b/);
|
|
});
|
|
|
|
test('record-session passes --stopped-at and --resume-file', () => {
|
|
const call = invocation('record-session');
|
|
assert.ok(call.includes('--stopped-at'), 'record-session must pass --stopped-at');
|
|
assert.ok(call.includes('--resume-file'), 'record-session must pass --resume-file');
|
|
});
|
|
|
|
test('no state.* call leads with a bare positional (quoted) value — the #1863 bug', () => {
|
|
// Buggy multi-line form: `state.<cmd> \` then a line whose first token is a quote.
|
|
const continued = /state\.(?:record-metric|add-decision|add-blocker|record-session)\b[^\r\n]*\\\r?\n\s*"/;
|
|
assert.ok(!continued.test(content),
|
|
'state.* calls must lead with --flags, not a positional quoted value on the next line');
|
|
// Buggy same-line form: `state.<cmd> "..."`
|
|
const inline = /state\.(?:record-metric|add-decision|add-blocker|record-session)\s+"/;
|
|
assert.ok(!inline.test(content),
|
|
'state.* calls must not pass a positional value immediately after the command');
|
|
});
|
|
|
|
test('sibling workflow record-session calls also use named flags (#1863 completeness)', () => {
|
|
// The same named-only router backs milestone-summary.md and forensics.md; both
|
|
// previously passed record-session positionally (`"" "stopped-at" "resume-file"`),
|
|
// silently dropping the values. Guard them alongside the executor.
|
|
for (const rel of ['gsd-core/workflows/milestone-summary.md', 'gsd-core/workflows/forensics.md']) {
|
|
const wf = fs.readFileSync(path.join(__dirname, '..', rel), 'utf-8');
|
|
const m = wf.match(/gsd_run query state\.record-session\b(?:[^\r\n]*\\\r?\n)*[^\r\n]*/);
|
|
assert.ok(m, `${rel} must invoke state.record-session`);
|
|
assert.ok(m[0].includes('--stopped-at') && m[0].includes('--resume-file'),
|
|
`${rel} record-session must use --stopped-at/--resume-file, got:\n${m[0]}`);
|
|
assert.ok(!/state\.record-session\s+"/.test(wf),
|
|
`${rel} record-session must not lead with a positional value`);
|
|
}
|
|
});
|
|
});
|