Files
msd-core/tests/bug-3605-stale-research-insert-phase-agent-refs.test.cjs
Tom Boucher 18ffaa86cc fix(3605): scrub retired slash commands from agents/*.md
Six surviving references to /gsd-research-phase (deleted in #3042) and
/gsd-insert-phase (consolidated into /gsd:phase insert in v1.40.0)
remained in five agent contracts because every prior scrub pass (#3029,
#3044, #3131) limited its SEARCH_DIRS to workflows/, references/,
templates/, contexts/, commands/, and hooks/ — agents/ was outside scope.
agents/gsd-executor.md:195 is user-facing: the executor surfaces it
during a package-install failure recovery checkpoint, so a real user
hits "Unknown command" while trying to recover from a stalled phase.

Replacements:
- /gsd-research-phase -> /gsd:plan-phase --research-phase <N>
  (agents/gsd-executor.md:195, agents/gsd-phase-researcher.md:17,
   agents/gsd-planner.md:186, agents/gsd-planner.md:991,
   agents/gsd-research-synthesizer.md:115)
- /gsd-insert-phase -> /gsd:phase insert
  (agents/gsd-roadmapper.md:205)

Adds tests/bug-3605-stale-research-insert-phase-agent-refs.test.cjs as
the regression guard. It scans agents/*.md for any retired command name
(/gsd-research-phase, /gsd-insert-phase, /gsd-add-phase,
/gsd-remove-phase, /gsd-analyze-dependencies) with proper word-boundary
matching so a future consolidation that misses agents/ fails CI.

The guard mirrors tests/bug-2950-stale-command-refs.test.cjs which
covers the same anti-pattern for workflows/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:54:03 -04:00

88 lines
3.0 KiB
JavaScript

// allow-test-rule: source-text-is-the-product
// agents/*.md text IS the deployed contract — Claude Code, Codex, etc. load these
// files at runtime and surface their content to users. Testing for retired slash
// commands in this text is testing what real users will see.
/**
* Bug #3605: Stale slash command references in 5 agent files
*
* After #3042 deleted /gsd-research-phase (replaced by
* /gsd-plan-phase --research-phase <N>) and v1.40.0 consolidated /gsd-insert-phase
* into /gsd-phase insert, six occurrences survived in agents/*.md because none of
* the consolidation passes (#3029, #3044, #3131) included agents/ in their per-name
* scrub scope. scripts/fix-slash-commands.cjs lists agents/ in SEARCH_DIRS but only
* runs the /gsd- → /gsd: namespace transform, not retired-name replacement.
*
* This guard fails when any retired command name reappears in agents/*.md.
*/
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
const RETIRED_COMMANDS = [
'/gsd-research-phase',
'/gsd-insert-phase',
'/gsd-add-phase',
'/gsd-remove-phase',
'/gsd-analyze-dependencies',
];
function listAgentFiles() {
return fs
.readdirSync(AGENTS_DIR)
.filter((name) => name.endsWith('.md'))
.map((name) => path.join(AGENTS_DIR, name));
}
function scanForRetired(filePath) {
const text = fs.readFileSync(filePath, 'utf-8');
const lines = text.split('\n');
const hits = [];
for (let i = 0; i < lines.length; i++) {
for (const cmd of RETIRED_COMMANDS) {
const idx = lines[i].indexOf(cmd);
if (idx === -1) continue;
const next = lines[i].charCodeAt(idx + cmd.length);
// Only count if the match is a real invocation, not a prefix of a longer name.
// The next char must be a non-name char (anything outside [A-Za-z0-9-_]).
const isWordBoundary =
Number.isNaN(next) ||
!((next >= 48 && next <= 57) || // 0-9
(next >= 65 && next <= 90) || // A-Z
(next >= 97 && next <= 122) || // a-z
next === 45 || // -
next === 95); // _
if (!isWordBoundary) continue;
hits.push({ line: i + 1, cmd, text: lines[i].trim() });
}
}
return hits;
}
describe('bug #3605: agent contracts must not reference retired slash commands', () => {
const agentFiles = listAgentFiles();
test('at least one agent file is scanned (smoke)', () => {
assert.ok(agentFiles.length > 0, 'expected agents/*.md to exist');
});
for (const file of agentFiles) {
const rel = path.relative(path.join(__dirname, '..'), file);
test(`${rel} contains no retired slash commands`, () => {
const hits = scanForRetired(file);
assert.deepEqual(
hits,
[],
`${rel} contains retired command references:\n` +
hits.map((h) => ` line ${h.line}: ${h.cmd} — ${h.text}`).join('\n'),
);
});
}
});