An allow-test-rule annotation citing a category that does not apply is worse than no annotation, because it reads as reviewed. Eight were confirmed by reading the assertions each one covered, and auditing the rest found five more plus one refutation — a converter test whose wording described the wrong mechanism while the covered assertion genuinely was deployed-text. The instructive one used the CANONICAL string for the same mistake: STATE.md command output labelled as a deployed artifact. A canonical string is not evidence the category fits, which is why normalising strings alone would have laundered the problem rather than fixed it. Every mapping the audit had inferred rather than code-verified was spot-checked before rewriting, and the ones that turned out not to fit were re-annotated rather than relabelled. Fourteen STATE.md assertions had a typed extractor available all along and now use it; their annotations came out because nothing needs exempting. Eight assertions genuinely need a production change first — CLI stdout and stderr with no structured mode — and are tagged pending-migration-to-typed-ir citing #3090, which is what that category is for. It had zero real uses before this, while one file carried a real citation to migration issue #2974 under a non-canonical tag. Six annotations covered assertions that do no text matching at all. An exemption for a violation that does not exist is noise that makes the real ones harder to audit; those are removed. atomic-write-coverage gains the annotation it always warranted — its own docstring describes a structural-regression-guard while the file carried none. Fifty-nine non-canonical strings across roughly thirty files are normalised, and the allow-test-rule allowlist is regenerated to match. 472 annotations became 463: every one now uses a canonical category, and the two remaining non-canonical strings are ESLint RuleTester fixtures, not annotations. Refs #3057 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
11 KiB
JavaScript
228 lines
11 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`);
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
// 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: source-text-is-the-product (see #3097)
|
|
// Reads markdown product files (gsd-executor.md, worktree-path-safety.md) to
|
|
// verify structural protocol.
|
|
|
|
// 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('<task_commit_protocol>');
|
|
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
|
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('<task_commit_protocol>');
|
|
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
|
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('<task_commit_protocol>');
|
|
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
|
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('<task_commit_protocol>');
|
|
const protocolEnd = executorSrc.indexOf('</task_commit_protocol>');
|
|
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('<parallel_execution>');
|
|
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('<files_to_read>');
|
|
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');
|
|
});
|
|
});
|
|
});
|
|
}
|