Files
msd-core/tests/executor-mvp-tdd-section.test.cjs
Behruz Nassre Esfahani 7bef6a6496 fix(#1863): use named flags for state.* calls in executor + workflows (#1873)
* 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>
2026-07-02 13:01:10 -04:00

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`);
}
});
});