Files
msd-core/tests/bug-3683-command-cross-reference-invariant.test.cjs
Tom Boucher 09ab16f9e5 fix(3683): normalize /gsd:<cmd> → /gsd-<cmd> in command, workflow, and reference bodies (#3685)
* fix(3683): normalize /gsd:<cmd> → /gsd-<cmd> in command, workflow, and reference bodies

Extends #3677's agent-body normalizer to all body text staged through
copyWithPathReplacement (commands, workflows, references). The initial
isCommand guard was structurally redundant — normalizeAgentBodyForRuntime
already self-gates on shouldNormalizeHyphenNamespaceInAgentBody(runtime),
so dropping it covers all hyphen-name runtimes (Claude / Qwen / Hermes)
without affecting colon-canonical runtimes (Gemini).

Addresses the user-visible symptom in #3683: workflows like
get-shit-done/workflows/discuss-phase.md (7 colon refs) leaked /gsd:<cmd>
markers to the model context, which the model echoed at the end of
/gsd-discuss-phase runs.

Source-prose drift caught by the new cross-reference invariant test:

  - commands/gsd/plan-phase.md: removed a slash-form mention of the
    deleted /gsd-research-phase command (#3042)
  - commands/gsd/profile-user.md: replaced a slash-form artifact
    reference with a backticked bare name (the referenced item is a
    skill config, not a user-callable slash command)

Tests:

  - tests/bug-3683-command-colon-namespace-leak.test.cjs — runtime-form
    regression for commands/gsd/*.md staging
  - tests/bug-3683-command-cross-reference-invariant.test.cjs — locks
    cross-reference coherence: every /gsd-X / /gsd:X reference in a
    command body must resolve to commands/gsd/X.md (so a future rename
    forces every cross-reference to update)
  - tests/bug-3683-workflow-colon-namespace-leak.test.cjs — runtime-form
    regression for workflows + references; negative test for gemini
    asserting the colon form is preserved

Fixes #3683

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(changeset): remove undefined cycle reference

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:13:59 -04:00

68 lines
2.3 KiB
JavaScript

// allow-test-rule: source-text-is-the-product
// commands/gsd/*.md bodies are the deployed contract — cross-references between
// them must stay coherent. This test inspects .md source to enforce the invariant.
'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 COMMANDS_DIR = path.resolve(__dirname, '..', 'commands', 'gsd');
function readKnownTargets() {
const commandNames = fs.readdirSync(COMMANDS_DIR)
.filter(f => f.endsWith('.md'))
.map(f => f.slice(0, -3));
return { commandNames, knownTargets: new Set(commandNames) };
}
function stripFrontmatter(src) {
return src.replace(/^---\r?\n[\s\S]*?\n---\r?\n/, '');
}
// Word-boundary lookbehind matching fix-slash-commands.cjs buildColonPattern / buildPattern
// Excludes path-y characters (~, ., /) so `~/gsd-workspaces`, `./gsd-foo`, `path/gsd-bar` don't match.
const REF_PATTERN = /(?<![a-zA-Z0-9_~./-])\/gsd[:-]([a-zA-Z0-9_-]+)/g;
describe('bug-3683 command cross-reference invariant', () => {
test('all /gsd:<X> and /gsd-<X> body refs resolve to known command base-names', () => {
const { commandNames, knownTargets: knownSet } = readKnownTargets();
const mdFiles = commandNames.sort().map(n => path.join(COMMANDS_DIR, `${n}.md`));
const failures = [];
for (const filePath of mdFiles) {
const src = fs.readFileSync(filePath, 'utf-8');
const body = stripFrontmatter(src);
const lines = body.split('\n');
const relFile = path.relative(path.resolve(__dirname, '..'), filePath);
lines.forEach((line, idx) => {
REF_PATTERN.lastIndex = 0;
let m;
while ((m = REF_PATTERN.exec(line)) !== null) {
const ref = m[1];
if (!knownSet.has(ref)) {
const sep = m[0].includes(':') ? ':' : '-';
failures.push({
file: relFile,
line: idx + 1,
ref: `/gsd${sep}${ref}`,
excerpt: line.trim(),
});
}
}
});
}
if (failures.length > 0) {
const msg = failures
.map(f => ` ${f.file}:${f.line} — dangling ref "${f.ref}" — ${f.excerpt}`)
.join('\n');
assert.fail(`Dangling command cross-references found:\n${msg}`);
}
});
});