/** * 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. ...` 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. \` 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. "..."` 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`); } }); });