/** * 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`); } }); }); // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-3097-3099-executor-worktree-path-safety.test.cjs — consolidation epic #1969 (B7 #1976) // ──────────────────────────────────────────────────────────────────────── { const { describe: __foldDescribe } = require('node:test'); __foldDescribe("folded:bug-3097-3099-executor-worktree-path-safety (consolidation epic #1969 B7 #1976)", () => { 'use strict'; // allow-test-rule: reads markdown product files (gsd-executor.md, worktree-path-safety.md) to verify structural protocol — not source-grep (see #3097) // Regression guards for bug #3097 and #3099. // // #3097: gsd-executor's worktree HEAD guard used `if [ -f .git ]` to detect // worktree mode. After a Bash `cd` out of the worktree into the main repo, // `.git` is a DIRECTORY (not a file), so the test is false and the entire // HEAD safety block is silently skipped. Commits then land on whatever branch // the main repo has checked out — not the per-agent worktree branch. // // #3099: Executor agents construct absolute paths from `pwd` captured in the // orchestrator context (main repo root). Edit/Write calls using these paths // resolve to the main repo, not the worktree. git commit from the worktree // sees a clean tree; the work is silently lost or leaks to main. const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const ROOT = path.join(__dirname, '..'); const executorSrc = fs.readFileSync( path.join(ROOT, 'agents', 'gsd-executor.md'), 'utf8', ); const executePhaseSrc = fs.readFileSync( path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md'), 'utf8', ); describe('bug #3097: cwd-drift sentinel in gsd-executor.md', () => { test('task_commit_protocol has cwd-drift assertion step (0a)', () => { const protocolIdx = executorSrc.indexOf(''); const protocolEnd = executorSrc.indexOf(''); assert.ok(protocolIdx !== -1 && protocolEnd !== -1, 'task_commit_protocol block not found'); const protocol = executorSrc.slice(protocolIdx, protocolEnd); assert.ok( protocol.includes('cwd') || protocol.includes('drift') || protocol.includes('gsd-spawn-toplevel'), 'task_commit_protocol missing cwd-drift assertion step — #3097 fix not applied', ); }); test('sentinel uses git rev-parse --git-dir to detect worktree', () => { const protocolIdx = executorSrc.indexOf(''); const protocolEnd = executorSrc.indexOf(''); const protocol = executorSrc.slice(protocolIdx, protocolEnd); assert.ok( protocol.includes('rev-parse --git-dir') || protocol.includes('worktrees/'), 'cwd-drift detection does not use git rev-parse --git-dir or .git/worktrees/ pattern', ); }); test('cwd-drift check precedes HEAD assertion', () => { const protocolIdx = executorSrc.indexOf(''); const protocolEnd = executorSrc.indexOf(''); const protocol = executorSrc.slice(protocolIdx, protocolEnd); const driftIdx = protocol.search(/cwd.drift|gsd-spawn-toplevel|drift.*assertion/i); const headIdx = protocol.indexOf('Pre-commit HEAD safety assertion'); assert.ok(driftIdx !== -1, 'cwd-drift assertion not found'); assert.ok(headIdx !== -1, 'HEAD assertion not found'); assert.ok(driftIdx < headIdx, 'cwd-drift assertion must precede HEAD assertion (step 0a before step 0)'); }); }); describe('bug #3099: absolute-path safety guidance in gsd-executor.md', () => { test('task_commit_protocol documents absolute-path safety', () => { const protocolIdx = executorSrc.indexOf(''); const protocolEnd = executorSrc.indexOf(''); const protocol = executorSrc.slice(protocolIdx, protocolEnd); assert.ok( (protocol.includes('absolute') || protocol.includes('absolute-path')) && (protocol.includes('worktree') || protocol.includes('WT_ROOT')), 'task_commit_protocol missing absolute-path safety guidance — #3099 fix not applied', ); }); test('execute-phase.md parallel_execution block references path safety', () => { const parallelIdx = executePhaseSrc.indexOf(''); assert.ok(parallelIdx !== -1, 'parallel_execution block not found in execute-phase.md'); // Verify the worktree-path-safety.md reference is present in the execution_context // (loaded via @ reference rather than inlined — the safe extract pattern) assert.ok( executePhaseSrc.includes('worktree-path-safety.md'), 'execute-phase.md does not reference worktree-path-safety.md in execution_context', ); }); test('execute-phase prompt anchors subagent file paths to project_root before files_to_read (#280)', () => { const filesIdx = executePhaseSrc.indexOf(''); assert.ok(filesIdx !== -1, 'files_to_read block not found in execute-phase.md'); const dispatchSnippet = executePhaseSrc.slice(filesIdx, filesIdx + 1800); assert.ok( dispatchSnippet.includes('PROJECT_ROOT=$(git rev-parse --show-toplevel'), 'executor dispatch must compute PROJECT_ROOT in the prompt before file reads', ); assert.ok( dispatchSnippet.includes('${PROJECT_ROOT}/'), 'executor files_to_read paths must be anchored to ${PROJECT_ROOT}/', ); }); test('worktree-path-safety.md reference file exists', () => { assert.ok( fs.existsSync(path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md')), 'gsd-core/references/worktree-path-safety.md does not exist', ); }); test('worktree-path-safety.md contains cwd-drift and absolute-path guards', () => { const safetySrc = fs.readFileSync( path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md'), 'utf8', ); assert.ok(safetySrc.includes('gsd-spawn-toplevel') || safetySrc.includes('cwd-drift'), 'worktree-path-safety.md missing cwd-drift sentinel content'); assert.ok(safetySrc.includes('WT_ROOT') || safetySrc.includes('absolute'), 'worktree-path-safety.md missing absolute-path guard content'); }); }); }); }