Files
msd-core/tests/slash-command-namespace.test.cjs
Jakub Zych fe3ed06691
Some checks failed
Tests / PR mergeability (push) Successful in 18s
Tests / Base branch health (push) Successful in 9s
Tests / Detect test scope (push) Successful in 16s
Tests / lint-tests (push) Failing after 1m43s
Tests / plugin-validate (push) Successful in 58s
Tests / test (ubuntu-latest, 24, shard 1/3) (push) Failing after 19s
Tests / test (ubuntu-latest, 24, shard 2/3) (push) Failing after 20s
Tests / test (ubuntu-latest, 24, shard 3/3) (push) Failing after 20s
Tests / test (ubuntu-latest, 24) (push) Failing after 18s
Tests / test (inert CI) (push) Has been skipped
Tests / QA loop walk (smell ratchet) (push) Failing after 19s
Tests / Coverage gate (merged shards) (push) Has been skipped
Tests / Publish emitted-baseline artifact (push) Has been skipped
Duplicate auto-close sweep / sweep (push) Successful in 19s
CI timeout budget report / report (push) Failing after 14s
Close Draft PRs (sweep) / Sweep open draft PRs (push) Successful in 9s
Dismiss Unauthorized PR Approvals / dismiss-unauthorized-approval (push) Successful in 9s
Tests / conformance test (macos-latest, 24) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 1/3) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 2/3) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 3/3) (push) Has been cancelled
Tests / Required tests (push) Has been cancelled
chore: clear dead test and allowlist leftovers of dropped runtimes
2026-10-06 20:35:12 +02:00

1457 lines
66 KiB
JavaScript

'use strict';
// Slash / colon command-namespace invariant tests.
//
// Consolidated home for the namespace-leak regression suites (epic #1969, batch
// B6 #1975). Each block below is folded verbatim from its origin issue-named
// file and carries its origin issue number for provenance. These tests share no
// production module — they assert the cross-surface invariant that MSD command,
// agent, and workflow bodies use the hyphen form (`msd-<cmd>`) and never leak the
// deprecated colon/slash namespace after install/conversion.
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-2543-msd-slash-namespace.test.cjs — consolidation epic #1969 (B6 #1975)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-2543-msd-slash-namespace (consolidation epic #1969 B6 #1975)", () => {
'use strict';
// allow-test-rule: structural-regression-guard (see #2543)
/**
* Slash-command namespace invariant (#3443) — SCOPED ACTIVE VARIANT.
*
* History:
* #3443 re-establishes `/msd:<cmd>` as canonical in Claude-facing source text.
* The source repo is authored for Claude command registration under
* `.claude/commands/msd/` (namespaced slash commands), while non-Claude runtimes
* perform install-time conversion (for example `/msd:<cmd>` -> `/msd-<cmd>`).
*
* Two-tier model (current — see CONTEXT.md § "Slash-command form: directory-level matrix"):
* • Claude-facing SOURCE TEXT (commands/, agents/, workflows/, references/,
* templates/, hooks/, .clinerules): uses `/msd:<cmd>` (colon).
* THIS test enforces the colon invariant over those directories.
* • Runtime-emitter contexts (runtime-slash.cjs, phase-lifecycle-policy.ts,
* *.generated.cjs, bug-3584 test file): use `/msd-<cmd>` (hyphen) per
* bug-3584's contract. Those files are EXCLUDED from this scan.
*
* Scoped invariant enforced here:
* No `/msd-<cmd>` pattern in Claude-facing source files, EXCLUDING the
* runtime-emitter contexts listed in RUNTIME_EMITTER_EXCLUDES below.
*
* Canonical reference for the runtime-emitter (hyphen-form) contract:
* tests/bug-3584-runtime-slash-emitters.test.cjs
*
* DO NOT expand RUNTIME_EMITTER_EXCLUDES without also updating the bug-3584
* test and CONTEXT.md § "Slash-command form: directory-level matrix".
*
* See also: PR #154 first-pass incident (agent applied outdated invariant,
* broke bug-3584 contract); PR #164 Codex adversarial review (surfaced the
* need to re-activate this test with explicit exclusions).
*/
const { test, describe } = 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 COMMANDS_DIR = path.join(ROOT, 'commands', 'msd');
// Runtime-emitter contexts: these files intentionally emit `/msd-<cmd>` (hyphen)
// as part of the bug-3584 runtime contract. They must NOT be scanned by this
// invariant — doing so caused PR #154 first-pass to revert correct hyphen form
// to colon form, breaking bug-3584-runtime-slash-emitters.test.cjs.
//
// Expand this list only if a new runtime-emitter module is introduced AND the
// bug-3584 test is updated to cover it.
const SEARCH_DIRS = [
// NOTE: msd-core/bin/lib is intentionally EXCLUDED from SEARCH_DIRS.
// runtime-slash.cjs and *.generated.cjs live there and use the hyphen form
// per bug-3584's runtime-emitter contract. The full bin/lib tree is
// runtime-emitter territory — scanning it would cause false positives.
path.join(ROOT, 'msd-core', 'workflows'),
path.join(ROOT, 'msd-core', 'references'),
path.join(ROOT, 'msd-core', 'templates'),
COMMANDS_DIR,
path.join(ROOT, 'agents'),
path.join(ROOT, 'hooks'),
];
const TOP_LEVEL_FILES = [
path.join(ROOT, '.clinerules'),
];
// Re-use SKIP_DIRS from the production script so the test's directory walker
// stays in lockstep with the fixer's. EXTENSIONS legitimately diverges (the
// guard scans only `.md`/`.cjs`/`.js` per the no-source-grep standard, while
// the fixer also rewrites `.ts`/`.tsx`), so it is not shared.
const { SKIP_DIRS } = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
const EXTENSIONS = new Set(['.md', '.cjs', '.js']);
function collectFiles(dir, results = []) {
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) {
if (SKIP_DIRS.has(e.name)) continue;
collectFiles(full, results);
}
else if (EXTENSIONS.has(path.extname(e.name))) results.push(full);
}
return results;
}
const cmdNames = fs.readdirSync(COMMANDS_DIR)
.filter(f => f.endsWith('.md'))
.map(f => f.replace(/\.md$/, ''))
.sort((a, b) => b.length - a.length);
const retiredPattern = new RegExp(`/msd-(${cmdNames.join('|')})(?=[^a-zA-Z0-9_-]|$)`);
const allFiles = SEARCH_DIRS.flatMap(d => collectFiles(d));
const topLevelFiles = TOP_LEVEL_FILES.filter((file) => fs.existsSync(file));
const allUserFacingFiles = allFiles.concat(topLevelFiles);
describe('slash-command namespace invariant (#3443)', () => {
test('commands/msd/ directory contains known command files', () => {
assert.ok(cmdNames.length > 0, 'commands/msd/ must contain .md files');
assert.ok(cmdNames.includes('plan-phase'), 'plan-phase must be a known command');
assert.ok(cmdNames.includes('execute-phase'), 'execute-phase must be a known command');
});
// SCOPED ACTIVE INVARIANT (2026-05-23 re-activation after Codex adversarial review of PR #164).
//
// Scan is scoped to Claude-facing source directories only (SEARCH_DIRS above).
// msd-core/bin/lib/ is excluded entirely — runtime-slash.cjs and
// *.generated.cjs there use hyphen form per bug-3584's runtime-emitter contract.
//
// If this test fails: check CONTEXT.md § "Slash-command form: directory-level matrix"
// before deciding whether to update the file or add to RUNTIME_EMITTER_EXCLUDES.
test('no /msd-<cmd> retired syntax in Claude-facing source files (scoped — excludes runtime-emitter contexts)', () => {
const violations = [];
for (const file of allUserFacingFiles) {
const src = fs.readFileSync(file, 'utf-8');
const lines = src.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
if (retiredPattern.test(lines[i])) {
violations.push(`${path.relative(ROOT, file)}:${i + 1}: ${lines[i].trim().slice(0, 80)}`);
}
}
}
assert.strictEqual(
violations.length,
0,
`Found ${violations.length} retired /msd-<cmd> reference(s) — use /msd:<cmd> instead:\n${violations.slice(0, 10).join('\n')}`,
);
});
test('command filenames use canonical hyphenated command slugs', () => {
const underscoreFiles = fs.readdirSync(COMMANDS_DIR)
.filter((f) => f.endsWith('.md') && f.includes('_'));
assert.deepStrictEqual(
underscoreFiles,
[],
'command filenames feed generated skill/autocomplete names and must not contain underscores',
);
});
describe('fix-slash-commands transformer behavior', () => {
const { transformContent } = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
// Use the live command names so the transformer matches the same surface
// the production CLI rewrites.
const liveCmdNames = cmdNames;
test('rewrites /msd-<cmd> to /msd:<cmd>', () => {
const out = transformContent('See /msd-plan-phase for details.', liveCmdNames);
assert.ok(out.includes('/msd:plan-phase'), `expected /msd:plan-phase, got: ${out}`);
assert.ok(!out.includes('/msd-plan-phase'), `dash form must not survive, got: ${out}`);
});
test('rewrites multiple occurrences in one pass', () => {
const out = transformContent('Run /msd-plan-phase then /msd-execute-phase.', liveCmdNames);
assert.ok(out.includes('/msd:plan-phase'));
assert.ok(out.includes('/msd:execute-phase'));
assert.ok(!out.match(/\/msd-[a-z]/), `no dash form may remain, got: ${out}`);
});
test('does not rewrite canonical colon form (idempotent)', () => {
const input = '/msd:plan-phase is the canonical name.';
assert.strictEqual(transformContent(input, liveCmdNames), input,
'transformer must be a no-op when input is already canonical');
});
test('does not rewrite msd-sdk or msd-tools (not slash commands)', () => {
const input = 'Run /msd-sdk query and /msd-tools init.';
assert.strictEqual(transformContent(input, liveCmdNames), input,
'transformer must leave non-command identifiers alone');
});
test('respects word boundary — does not rewrite /msd-plan-phase-extra', () => {
const out = transformContent('/msd-plan-phase-extra', liveCmdNames);
assert.strictEqual(out, '/msd-plan-phase-extra',
'word-boundary lookahead must prevent partial matches');
});
});
test('transformer leaves non-command identifiers untouched', () => {
const { transformContent } = require(path.join(ROOT, 'scripts', 'fix-slash-commands.cjs'));
const sample = 'Use /msd-sdk query and node bin/msd-tools.cjs';
assert.strictEqual(
transformContent(sample, cmdNames),
sample,
'msd-sdk and msd-tools are not slash commands and must remain untouched'
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3677-agent-colon-namespace-leak.test.cjs — consolidation epic #1969 (B6 #1975)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3677-agent-colon-namespace-leak (consolidation epic #1969 B6 #1975)", () => {
// allow-test-rule: source-text-is-the-product (see #3677)
// Tests A1/A2/B inspect agent / installed `.md` bodies whose deployed text IS
// the runtime contract. Tests C exercises the install.js exported pure helper
// `shouldNormalizeHyphenNamespaceInAgentBody` directly — purely behavioral.
/**
* Regression for #3677 — installed agent bodies leak `/msd:<cmd>` colon refs
* for Claude (unroutable since #2808).
*
* Root cause: `bin/install.js` agent install loop (around line 8350-8447)
* reads each agent .md, runs runtime-specific transforms via
* `convertClaudeAgentToXAgent()`, then writes the result. For:
* - Self-converting runtimes (Codex/Cursor/Antigravity/Opencode): their converters handle
* namespace themselves.
* - Gemini: intentionally uses colon namespace.
* - Claude-default: registers hyphen-form `name:` (#2808)
* but copies bodies verbatim (Claude does no namespace work). The retired `/msd:<cmd>` colon refs leak.
*
* Sibling fixes #3583 (SKILL.md, via #3629) and #3584 (runtime emissions, via
* #3606) covered the other two surfaces. This is the agent-body surface.
*
* Fix surface:
* 1. `bin/install.js` exports a pure predicate
* `shouldNormalizeHyphenNamespaceInAgentBody(runtime)` plus a helper
* `normalizeAgentBodyForRuntime(content, runtime, cmdNames)` that
* conditionally applies `transformContentToHyphen` from
* scripts/fix-slash-commands.cjs.
* 2. The agent install loop calls the helper after all runtime-specific
* conversions but before writeFileSync.
* 3. This regression test guards both the predicate and the integration.
*/
'use strict';
process.env.MSD_TEST_MODE = '1';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
// Single `..` traversal matches the existing tests/helpers.cjs convention
// (TOOLS_PATH at tests/helpers.cjs:21). Avoids `..` chains per CLAUDE.md and
// works in the docker mirror at /work/tests (which has no `.git` to anchor on).
const REPO_ROOT = path.resolve(__dirname, '..');
const install = require(path.join(REPO_ROOT, 'bin', 'install.js'));
const { transformContentToHyphen } = require(path.join(REPO_ROOT, 'scripts', 'fix-slash-commands.cjs'));
// Snapshot of all runtime IDs in the layout table at the time of this fix.
// Keep these two sets covering: any runtime listed in
// runtime-artifact-layout.cjs MUST appear in exactly one bucket.
const HYPHEN_NAME_AGENT_RUNTIMES = ['claude'];
const SELF_CONVERTING_OR_COLON_RUNTIMES = [
'codex', 'antigravity', 'cursor', 'opencode',
];
describe('bug #3677 — agent body colon-namespace leak (Claude)', () => {
describe('A — install.js exports the pure predicate + helper', () => {
test('A1: shouldNormalizeHyphenNamespaceInAgentBody is an exported function', () => {
assert.strictEqual(
typeof install.shouldNormalizeHyphenNamespaceInAgentBody,
'function',
'bin/install.js must export shouldNormalizeHyphenNamespaceInAgentBody as the runtime predicate (regression seam for #3677)',
);
});
test('A2: normalizeAgentBodyForRuntime is an exported function', () => {
assert.strictEqual(
typeof install.normalizeAgentBodyForRuntime,
'function',
'bin/install.js must export normalizeAgentBodyForRuntime as the wired helper called by the agent install loop',
);
});
});
describe('B — predicate returns true for hyphen-`name:` runtimes and false otherwise', () => {
const { shouldNormalizeHyphenNamespaceInAgentBody } = install;
for (const runtime of HYPHEN_NAME_AGENT_RUNTIMES) {
test(`B+ '${runtime}': normalize hyphen namespace (true)`, () => {
assert.strictEqual(
shouldNormalizeHyphenNamespaceInAgentBody(runtime),
true,
`${runtime} registers hyphen-form 'name:' (#2808) and copies agent bodies verbatim — must normalize`,
);
});
}
for (const runtime of SELF_CONVERTING_OR_COLON_RUNTIMES) {
test(`B- '${runtime}': skip normalization (false)`, () => {
assert.strictEqual(
shouldNormalizeHyphenNamespaceInAgentBody(runtime),
false,
`${runtime} either self-converts via convertClaudeAgentToXAgent or intentionally uses colon — must NOT re-rewrite`,
);
});
}
test('B?: unknown runtime defaults to false (conservative)', () => {
assert.strictEqual(
shouldNormalizeHyphenNamespaceInAgentBody('bogus-runtime-id'),
false,
'unknown runtimes must not be normalized — better to leak than to mangle',
);
});
});
describe('C — normalizeAgentBodyForRuntime applies transformContentToHyphen iff predicate is true', () => {
const { normalizeAgentBodyForRuntime } = install;
// Sample agent body with colon refs that #2808 retired.
const inputBody = [
'# Agent prose',
'',
'Run `/msd:execute-phase 1 --tdd` to execute the phase.',
'Then `/msd:verify-work 1` to verify.',
'Reference unchanged: `msd-sdk query commit` (this is a CLI binary, not a slash command).',
].join('\n');
// Only known commands from commands/msd/*.md should be rewritten; msd-sdk
// (a binary) must stay untouched.
const cmdNames = ['execute-phase', 'verify-work', 'plan-phase'];
test('C1: claude — rewrites both colon refs to hyphen', () => {
const out = normalizeAgentBodyForRuntime(inputBody, 'claude', cmdNames);
assert.ok(out.includes('/msd-execute-phase'), 'execute-phase must be rewritten to hyphen form');
assert.ok(out.includes('/msd-verify-work'), 'verify-work must be rewritten to hyphen form');
assert.ok(!out.includes('/msd:execute-phase'), 'colon form for execute-phase must be gone');
assert.ok(!out.includes('/msd:verify-work'), 'colon form for verify-work must be gone');
assert.ok(out.includes('msd-sdk query commit'), 'msd-sdk (CLI binary) must not be touched');
});
test('C5: self-converting runtime (codex) — body returned unchanged at this layer', () => {
const out = normalizeAgentBodyForRuntime(inputBody, 'codex', cmdNames);
// Codex has its own agent converter that handles namespace — the
// normalize layer is a no-op for it.
assert.strictEqual(out, inputBody);
});
});
describe('D — sanity check: the underlying transform actually works against real cmd names', () => {
test('D1: transformContentToHyphen rewrites /msd:<cmd> to /msd-<cmd> for known cmds only', () => {
const out = transformContentToHyphen(
'A /msd:execute-phase B /msd:unknown-cmd C /msd-sdk D',
['execute-phase'],
);
assert.ok(out.includes('/msd-execute-phase'), 'known cmd rewritten');
assert.ok(out.includes('/msd:unknown-cmd'), 'unknown cmd preserved (longest-first matcher only rewrites registered names)');
assert.ok(out.includes('/msd-sdk'), 'msd-sdk (binary, not slash command) preserved');
});
});
// ---------------------------------------------------------------------------
// E — Behavioral coverage ported from PR #3681 (johnzilla / John Turner).
//
// #3681 proposed the same allow-list fix independently and was closed by its
// author in favor of this PR. Its test file contributed two coverage angles
// worth keeping: real-source efficacy against every `agents/msd-*.md` (the
// shape of bug that pure-function tests miss) and idempotence-via-fixpoint
// (guards against double-rewrite on reinstall). Credit: johnzilla.
// ---------------------------------------------------------------------------
describe('E — real-source efficacy + idempotence (ported from #3681, credit: johnzilla)', () => {
const fs = require('node:fs');
const { readCmdNames } = require(path.join(REPO_ROOT, 'scripts', 'fix-slash-commands.cjs'));
const cmdNames = readCmdNames();
// Roster regex matches any registered command in `msd:<cmd>` form with a
// negative lookbehind (so `mymsd:foo` is ignored) and a non-word lookahead
// (so `plan-phase-extra` is not a false match for `plan-phase`).
const roster = () => new RegExp(
`(?<![a-zA-Z0-9_-])msd:(${[...cmdNames].sort((a, b) => b.length - a.length).join('|')})(?=[^a-zA-Z0-9_-]|$)`,
);
test('E0: command roster is populated and contains the symptom commands', () => {
assert.ok(cmdNames.length > 0, 'command roster must be populated');
assert.ok(cmdNames.includes('execute-phase'));
assert.ok(cmdNames.includes('plan-phase'));
});
test('E1: every agents/msd-*.md transforms clean — no roster colon refs survive', () => {
const agentsDir = path.join(REPO_ROOT, 'agents');
const offenders = [];
// Not the shared listAgentFiles() helper: this needs full `.md` filenames
// (not stripped basenames) to readFileSync + transform each agent body.
for (const f of fs.readdirSync(agentsDir)) {
if (!f.startsWith('msd-') || !f.endsWith('.md')) continue;
const src = fs.readFileSync(path.join(agentsDir, f), 'utf-8');
const out = transformContentToHyphen(src, cmdNames);
if (roster().test(out)) offenders.push(f);
}
assert.deepEqual(
offenders,
[],
`agents still carry roster colon refs after transform: ${offenders.join(', ')}`,
);
});
test('E2: idempotent — transform of already-hyphenated input is a no-op', () => {
const input = 'use /msd-plan-phase next, then /msd-execute-phase';
assert.strictEqual(
transformContentToHyphen(input, cmdNames),
input,
'reinstalls re-run the transform; double application must not mangle the body',
);
});
test('E3: word boundary — /msd:plan-phase-extra is not a roster match', () => {
assert.strictEqual(
transformContentToHyphen('/msd:plan-phase-extra', cmdNames),
'/msd:plan-phase-extra',
);
});
test('E4: rewrites bare `msd:<cmd>` shorthand (no leading slash)', () => {
const out = transformContentToHyphen(
'Spawned by the msd:execute-phase orchestrator.',
cmdNames,
);
assert.strictEqual(out, 'Spawned by the msd-execute-phase orchestrator.');
});
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3683-command-colon-namespace-leak.test.cjs — consolidation epic #1969 (B6 #1975)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3683-command-colon-namespace-leak (consolidation epic #1969 B6 #1975)", () => {
// allow-test-rule: source-text-is-the-product (see #3683)
// Command `.md` files — their staged text IS the runtime contract loaded by
// Claude Code. Asserting that staged bodies lack `/msd:<cmd>` colon refs is
// a behavioral test of the install transform, not source-grep theater.
/**
* Regression for #3683 — installed command bodies leak `/msd:<cmd>` colon refs
* for Claude Code local installs.
*
* Root cause: `bin/install.js` command install path (`copyWithPathReplacement`,
* around line 8296 in the `else` branch) copies each command `.md` body without
* applying the hyphen-namespace normalizer that the agent install loop gained in
* PR #3677. Static prose in `commands/msd/*.md` (e.g. plan-phase.md referencing
* `/msd:execute-phase`) therefore reaches the model verbatim, causing the model
* to echo the retired colon form at workflow boundaries.
*
* Fix surface:
* Call `normalizeAgentBodyForRuntime` (or an equivalent helper) in the command
* staging path after all other rewrites but before writeFileSync, mirroring
* the agent install loop fix from #3677.
*
* This test guards the behavioral integration: run a real local claude install
* into a temp dir, then assert that no staged command body contains a
* `/msd:<known-cmd>` colon ref.
*/
'use strict';
process.env.MSD_TEST_MODE = '1';
const { describe, test, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const { runNode } = require('./helpers/process-seam.cjs');
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const REPO_ROOT = path.resolve(__dirname, '..');
const INSTALL_PATH = path.join(REPO_ROOT, 'bin', 'install.js');
const install = require(INSTALL_PATH);
const { readCmdNames } = require(path.join(REPO_ROOT, 'scripts', 'fix-slash-commands.cjs'));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Run `node install.js --claude --local --no-sdk` in tmpDir.
* MSD_TEST_MODE must be cleared so the install() main block executes.
*/
function runClaudeLocalInstall(cwd) {
const env = { ...process.env };
delete env.MSD_TEST_MODE;
const r = runNode([INSTALL_PATH, '--claude', '--local', '--no-sdk'], {
cwd,
env,
timeoutMs: INSTALL_TIMEOUT_MS,
});
throwIfFailed(r, `node ${INSTALL_PATH} --claude --local --no-sdk`);
}
/**
* Build the roster regex that matches `/msd:<known-cmd>` or `msd:<known-cmd>`
* (with appropriate word boundaries). Mirrors the pattern used in bug-3677.
*/
function buildRosterRegex(cmdNames) {
const sorted = [...cmdNames].sort((a, b) => b.length - a.length);
return new RegExp(
`(?<![a-zA-Z0-9_-])msd:(${sorted.join('|')})(?=[^a-zA-Z0-9_-]|$)`,
);
}
// ---------------------------------------------------------------------------
// Suite A — export surface: normalizeAgentBodyForRuntime must be exported
// (same seam used for command bodies)
// ---------------------------------------------------------------------------
describe('bug #3683 — command body colon-namespace leak (Claude local install)', () => {
describe('A — install.js exports the normalizer seam', () => {
test('A1: normalizeAgentBodyForRuntime is exported (reused for command bodies)', () => {
assert.strictEqual(
typeof install.normalizeAgentBodyForRuntime,
'function',
'bin/install.js must export normalizeAgentBodyForRuntime — the seam used for both agent and command body normalization',
);
});
test('A2: shouldNormalizeHyphenNamespaceInAgentBody is exported and true for claude', () => {
assert.strictEqual(
typeof install.shouldNormalizeHyphenNamespaceInAgentBody,
'function',
);
assert.strictEqual(
install.shouldNormalizeHyphenNamespaceInAgentBody('claude'),
true,
'claude must normalize hyphen namespace — it is in the allow-list from #2808',
);
});
});
// ---------------------------------------------------------------------------
// B — pure-function coverage: normalizer rewrites command body colon refs
// ---------------------------------------------------------------------------
describe('B — normalizeAgentBodyForRuntime rewrites colon refs in command-body prose', () => {
const { normalizeAgentBodyForRuntime } = install;
const cmdNames = readCmdNames();
test('B0: command roster is populated and includes symptom commands', () => {
assert.ok(cmdNames.length > 0, 'readCmdNames() must return a non-empty list');
assert.ok(cmdNames.includes('execute-phase'), 'roster must include execute-phase');
assert.ok(cmdNames.includes('plan-phase'), 'roster must include plan-phase');
});
test('B1: claude — rewrites /msd:<cmd> colon refs in command-body prose to hyphen form', () => {
const input = [
'## After planning',
'',
'Run `/msd:execute-phase 1 --tdd` to begin execution.',
'Then use `/msd:verify-work 1` when done.',
].join('\n');
const out = normalizeAgentBodyForRuntime(input, 'claude', cmdNames);
assert.ok(out.includes('/msd-execute-phase'), 'execute-phase must be rewritten to hyphen form');
assert.ok(out.includes('/msd-verify-work'), 'verify-work must be rewritten to hyphen form');
assert.ok(!out.includes('/msd:execute-phase'), 'colon form for execute-phase must be absent');
assert.ok(!out.includes('/msd:verify-work'), 'colon form for verify-work must be absent');
});
test('B2: gemini — colon refs preserved (Gemini intentionally uses colon namespace)', () => {
const input = 'Run `/msd:execute-phase 1` to begin.';
const out = normalizeAgentBodyForRuntime(input, 'gemini', cmdNames);
assert.ok(out.includes('/msd:execute-phase'), 'Gemini must keep colon form');
assert.ok(!out.includes('/msd-execute-phase'), 'Gemini must not have hyphen form injected');
});
});
// ---------------------------------------------------------------------------
// E — Integration: real local claude install produces clean command bodies
// ---------------------------------------------------------------------------
// E — integration: flat msd-*.md layout + clean bodies (#1367 fix)
//
// Prior to #1367: commands wrote to commands/msd/<cmd>.md (bare names in a
// subdir), causing Claude Code to namespace them as /msd:<cmd> (colon form).
// After #1367: commands write flat msd-<cmd>.md at commands/ level so Claude
// Code registers them as /msd-<cmd> (hyphen form, matching all framework refs).
// ---------------------------------------------------------------------------
describe('E — integration: staged msd-*.md flat commands contain no colon-namespace refs', () => {
let tmpDir;
const cmdNames = readCmdNames();
const rosterRegex = buildRosterRegex(cmdNames);
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3683-'));
runClaudeLocalInstall(tmpDir);
});
after(() => {
cleanup(tmpDir);
});
test('E0: staged commands/ directory has flat msd-*.md files after install (#1367)', () => {
// After #1367 fix: commands land at .claude/commands/msd-<cmd>.md (flat,
// hyphen-prefixed). The old .claude/commands/msd/<cmd>.md subdirectory
// layout must NOT be created.
const commandsDir = path.join(tmpDir, '.claude', 'commands');
assert.ok(
fs.existsSync(commandsDir),
`commands/ must be created by local claude install at ${commandsDir}`,
);
const flatFiles = fs.readdirSync(commandsDir).filter(f => f.startsWith('msd-') && f.endsWith('.md'));
assert.ok(
flatFiles.length > 0,
`commands/ must contain flat msd-*.md files (e.g. msd-help.md). ` +
`Found none — install may still be using the old commands/msd/<cmd>.md subdirectory layout.`,
);
// The old subdirectory must NOT exist (it caused /msd:<cmd> colon namespace)
const oldSubdir = path.join(commandsDir, 'msd');
assert.ok(
!fs.existsSync(oldSubdir),
`commands/msd/ subdir must NOT exist after install (it causes /msd:<cmd> colon namespace in Claude Code). ` +
`#1367 fix: use flat msd-<cmd>.md at commands/ level instead.`,
);
});
test('E1: no staged command body contains /msd:<known-cmd> colon refs', () => {
const commandsDir = path.join(tmpDir, '.claude', 'commands');
assert.ok(fs.existsSync(commandsDir), 'commands/ must exist for this check to be meaningful');
const offenders = [];
for (const entry of fs.readdirSync(commandsDir, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
if (!entry.name.startsWith('msd-')) continue;
const fullPath = path.join(commandsDir, entry.name);
const content = fs.readFileSync(fullPath, 'utf-8');
if (rosterRegex.test(content)) {
offenders.push(path.relative(tmpDir, fullPath));
}
}
assert.deepEqual(
offenders,
[],
`Staged command bodies still contain roster colon refs (e.g. /msd:execute-phase). ` +
`Install must normalize these to /msd-<cmd> for claude runtime. Offenders: ${offenders.join(', ')}`,
);
});
test('E2: idempotent — re-running install does not double-mangle already-hyphenated refs', () => {
// Run install a second time; if the normalizer double-applies it would
// produce garbled output like /msd--execute-phase. Verify the commands
// still pass the same cleanliness check after a second install.
runClaudeLocalInstall(tmpDir);
const commandsDir = path.join(tmpDir, '.claude', 'commands');
const doubleRewriteRegex = /\/msd--[a-z]/;
const garbled = [];
for (const entry of fs.readdirSync(commandsDir, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
if (!entry.name.startsWith('msd-')) continue;
const content = fs.readFileSync(path.join(commandsDir, entry.name), 'utf-8');
if (doubleRewriteRegex.test(content)) {
garbled.push(entry.name);
}
}
assert.deepEqual(
garbled,
[],
`Re-install produced double-hyphen artifacts (/msd--cmd) — normalizer is not idempotent. Garbled files: ${garbled.join(', ')}`,
);
});
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3683-workflow-colon-namespace-leak.test.cjs — consolidation epic #1969 (B6 #1975)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3683-workflow-colon-namespace-leak (consolidation epic #1969 B6 #1975)", () => {
// allow-test-rule: source-text-is-the-product (see #3683)
// Workflow and reference `.md` files are deployed verbatim as part of the
// msd-core skill payload — their staged text IS the runtime contract
// loaded by Claude Code. Asserting that staged bodies lack `/msd:<cmd>`
// colon refs is a behavioral test of the install transform, not
// source-grep theater.
/**
* Regression for #3683 — installed workflow/reference bodies leak `/msd:<cmd>`
* colon refs for Claude Code local installs.
*
* Root cause: `copyWithPathReplacement` in `bin/install.js` guarded the
* `normalizeAgentBodyForRuntime` call behind `if (isCommand)`, so the
* `msd-core/` directory (workflows, references — all `isCommand=false`)
* was copied without applying the hyphen-namespace normalizer. Static prose
* in `msd-core/workflows/*.md` and `msd-core/references/*.md`
* (e.g. discuss-phase.md referencing `/msd:plan-phase`) therefore reached
* the model verbatim, causing it to echo the retired colon form.
*
* Fix surface:
* Remove the `if (isCommand)` guard so `normalizeAgentBodyForRuntime` is
* called unconditionally in `copyWithPathReplacement`. The function
* self-gates on `shouldNormalizeHyphenNamespaceInAgentBody(runtime)` and
* is a no-op for colon-canonical runtimes (Gemini, Codex, etc.).
*
* User repro path: `/msd-discuss-phase` output ends with `/msd:nextcommand`
* because discuss-phase.md (7 colon refs) is not normalized at install time.
*/
'use strict';
process.env.MSD_TEST_MODE = '1';
const { describe, test, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const { runNode } = require('./helpers/process-seam.cjs');
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const REPO_ROOT = path.resolve(__dirname, '..');
const INSTALL_PATH = path.join(REPO_ROOT, 'bin', 'install.js');
require(INSTALL_PATH);
const { readCmdNames } = require(path.join(REPO_ROOT, 'scripts', 'fix-slash-commands.cjs'));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Run `node install.js --claude --local --no-sdk` in tmpDir.
* MSD_TEST_MODE must be cleared so the install() main block executes.
*/
function runClaudeLocalInstall(cwd) {
const env = { ...process.env };
delete env.MSD_TEST_MODE;
const r = runNode([INSTALL_PATH, '--claude', '--local', '--no-sdk'], {
cwd,
env,
timeoutMs: INSTALL_TIMEOUT_MS,
});
throwIfFailed(r, `node ${INSTALL_PATH} --claude --local --no-sdk`);
}
/**
* Build the roster regex that matches `msd:<known-cmd>` references.
* Mirrors the pattern used by the Cycle 1 command test.
*/
function buildRosterRegex(cmdNames) {
const sorted = [...cmdNames].sort((a, b) => b.length - a.length);
return new RegExp(
`(?<![a-zA-Z0-9_-])msd:(${sorted.join('|')})(?=[^a-zA-Z0-9_-]|$)`,
);
}
/**
* Walk a directory recursively and collect .md files whose body matches regex.
*/
function collectOffenders(dir, regex) {
const offenders = [];
const walk = (d) => {
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
const fullPath = path.join(d, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
} else if (entry.name.endsWith('.md')) {
const content = fs.readFileSync(fullPath, 'utf-8');
if (regex.test(content)) {
offenders.push(fullPath);
}
}
}
};
walk(dir);
return offenders;
}
// ---------------------------------------------------------------------------
// Suite — integration: staged msd-core/workflows/ and references/ must
// have no colon-namespace refs for claude.
// ---------------------------------------------------------------------------
describe('bug #3683 — workflow/reference colon-namespace leak (Claude local install)', () => {
// Shared Claude local install used by W and R suites.
// Consolidating to a single install halves disk I/O for this file and
// reduces concurrent load on CI runners — preventing timing interference
// with concurrently-running tests (e.g. the TOCTOU barrier tests in
// locking-bugs-1909-1916-1925-1927.test.cjs).
let claudeTmpDir;
const cmdNames = readCmdNames();
const rosterRegex = buildRosterRegex(cmdNames);
// Shared claude local install — used by W (workflow/reference clean-slate) and
// R (routing-block positive assertion) sub-suites.
before(() => {
claudeTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3683-claude-'));
runClaudeLocalInstall(claudeTmpDir);
});
after(() => {
cleanup(claudeTmpDir);
});
// -------------------------------------------------------------------------
// W — real local claude install: workflow + reference bodies are clean
// -------------------------------------------------------------------------
describe('W — integration: staged workflows and references contain no colon-namespace refs', () => {
test('W0: staged msd-core/workflows/ directory exists after install', () => {
const workflowsDir = path.join(claudeTmpDir, '.claude', 'msd-core', 'workflows');
assert.ok(
fs.existsSync(workflowsDir),
`msd-core/workflows/ must be created by local claude install at ${workflowsDir}`,
);
});
test('W1: staged msd-core/references/ directory exists after install', () => {
const refsDir = path.join(claudeTmpDir, '.claude', 'msd-core', 'references');
assert.ok(
fs.existsSync(refsDir),
`msd-core/references/ must be created by local claude install at ${refsDir}`,
);
});
test('W2: focused repro — staged discuss-phase.md has zero /msd: colon refs', () => {
// User-reported repro: /msd-discuss-phase output ends with /msd:nextcommand
// because discuss-phase.md ships 7 colon refs that were not normalized.
const stagedFile = path.join(
claudeTmpDir, '.claude', 'msd-core', 'workflows', 'discuss-phase.md',
);
assert.ok(
fs.existsSync(stagedFile),
`discuss-phase.md must exist in staged msd-core/workflows/`,
);
const content = fs.readFileSync(stagedFile, 'utf-8');
const colonMatches = content.match(/msd:[a-z][a-z0-9-]*/g) || [];
// Filter to known-command refs only
const knownColonRefs = colonMatches.filter(m => {
const cmd = m.slice(4); // strip 'msd:'
return cmdNames.includes(cmd);
});
assert.deepEqual(
knownColonRefs,
[],
`discuss-phase.md still contains colon-namespace refs that install must normalize: ${knownColonRefs.join(', ')}`,
);
});
test('W3: no staged workflow body contains /msd:<known-cmd> colon refs', () => {
const workflowsDir = path.join(claudeTmpDir, '.claude', 'msd-core', 'workflows');
assert.ok(fs.existsSync(workflowsDir), 'workflows/ must exist for this check to be meaningful');
const offenders = collectOffenders(workflowsDir, rosterRegex);
const relOffenders = offenders.map(f => path.relative(claudeTmpDir, f));
assert.deepEqual(
relOffenders,
[],
`Staged workflow bodies still contain roster colon refs (e.g. /msd:plan-phase). ` +
`Install must normalize these to /msd-<cmd> for claude runtime. Offenders: ${relOffenders.join(', ')}`,
);
});
test('W4: no staged reference body contains /msd:<known-cmd> colon refs', () => {
const refsDir = path.join(claudeTmpDir, '.claude', 'msd-core', 'references');
assert.ok(fs.existsSync(refsDir), 'references/ must exist for this check to be meaningful');
const offenders = collectOffenders(refsDir, rosterRegex);
const relOffenders = offenders.map(f => path.relative(claudeTmpDir, f));
assert.deepEqual(
relOffenders,
[],
`Staged reference bodies still contain roster colon refs. ` +
`Install must normalize these to /msd-<cmd> for claude runtime. Offenders: ${relOffenders.join(', ')}`,
);
});
});
// -------------------------------------------------------------------------
// R — #3646 routing-block positive assertion: ▶-prefixed lines use hyphen
//
// User repro: workflow output ends with "▶ /msd:validate-phase {N}" (colon
// form) which does not resolve in Claude Code — the installed skill is
// /msd-validate-phase (hyphen). Workflows emit routing blocks verbatim, so
// the colon form reaches the model and is echoed to the user unchanged.
//
// This suite checks the POSITIVE invariant: lines starting with ▶ that
// reference a MSD slash command must use /msd-<cmd> (hyphen) in the staged
// output. This is a stricter assertion than W3 (which only checks absence
// of colon globally) because it confirms the routing-position strings were
// NOT omitted — they must be present AND use the correct form.
//
// Source files with known ▶-prefixed routing-block colon refs (#3646):
// - msd-core/workflows/validate-phase.md:151 ▶ Next: /msd:audit-milestone
// - msd-core/workflows/validate-phase.md:158 ▶ Retry: /msd:validate-phase
// - msd-core/workflows/secure-phase.md:140 ▶ Fix mitigations: /msd:secure-phase
// - msd-core/workflows/secure-phase.md:158 ▶ /msd:validate-phase
// - msd-core/workflows/secure-phase.md:159 ▶ /msd:verify-work
// -------------------------------------------------------------------------
describe('R — #3646 routing-block: ▶-prefixed lines use hyphen form in staged claude install', () => {
// Uses the shared claudeTmpDir from the parent describe block — no separate install needed.
/**
* Collect all lines starting with the ▶ routing marker from a file.
* Returns an array of { lineNo, text } objects.
*/
function collectRoutingLines(filePath) {
if (!fs.existsSync(filePath)) return [];
return fs.readFileSync(filePath, 'utf-8')
.split(/\r?\n/)
.map((text, i) => ({ lineNo: i + 1, text }))
.filter(({ text }) => text.startsWith('▶'));
}
test('R1: staged validate-phase.md routing block uses /msd-<cmd> hyphen form', () => {
const stagedFile = path.join(
claudeTmpDir, '.claude', 'msd-core', 'workflows', 'validate-phase.md',
);
assert.ok(
fs.existsSync(stagedFile),
`validate-phase.md must exist in staged msd-core/workflows/`,
);
const routingLines = collectRoutingLines(stagedFile);
// Exactly two known routing lines (▶ Next / ▶ Retry).
const msdRoutingLines = routingLines.filter(({ text }) => /\/msd[-:]/.test(text));
assert.strictEqual(
msdRoutingLines.length,
2,
`validate-phase.md must have exactly 2 ▶-routing lines referencing a /msd- command — ` +
`found ${msdRoutingLines.length}: ${JSON.stringify(msdRoutingLines)}`,
);
// Positive: every routing line that references msd must use the hyphen form.
for (const { lineNo, text } of msdRoutingLines) {
assert.ok(
/\/msd-[a-z]/.test(text),
`validate-phase.md line ${lineNo}: ▶-routing line must use /msd-<cmd> hyphen form, got: ${JSON.stringify(text)}`,
);
// Negative: must not contain the colon form.
assert.ok(
!/\/msd:[a-z]/.test(text),
`validate-phase.md line ${lineNo}: ▶-routing line must not contain /msd:<cmd> colon form, got: ${JSON.stringify(text)}`,
);
// Token-level: extract real command tokens (/msd-<cmd> starting with a
// lowercase letter) and assert none contain an embedded colon.
// Skips documentation placeholder tokens like /msd-[command].
const rawTokens = text.match(/\/msd[^\s]*/g) || [];
for (const token of rawTokens) {
assert.ok(
!token.includes(':'),
`validate-phase.md line ${lineNo}: /msd token "${token}" must not contain a colon — embedded colon detected (e.g. /msd-validate:phase), got: ${JSON.stringify(text)}`,
);
}
}
});
test('R2: staged secure-phase.md routing block uses /msd-<cmd> hyphen form', () => {
const stagedFile = path.join(
claudeTmpDir, '.claude', 'msd-core', 'workflows', 'secure-phase.md',
);
assert.ok(
fs.existsSync(stagedFile),
`secure-phase.md must exist in staged msd-core/workflows/`,
);
const routingLines = collectRoutingLines(stagedFile);
// Exactly three known routing lines (fix-mitigations, validate-phase, verify-work).
const msdRoutingLines = routingLines.filter(({ text }) => /\/msd[-:]/.test(text));
assert.strictEqual(
msdRoutingLines.length,
3,
`secure-phase.md must have exactly 3 ▶-routing lines referencing a /msd- command — ` +
`found ${msdRoutingLines.length}: ${JSON.stringify(msdRoutingLines)}`,
);
for (const { lineNo, text } of msdRoutingLines) {
assert.ok(
/\/msd-[a-z]/.test(text),
`secure-phase.md line ${lineNo}: ▶-routing line must use /msd-<cmd> hyphen form, got: ${JSON.stringify(text)}`,
);
assert.ok(
!/\/msd:[a-z]/.test(text),
`secure-phase.md line ${lineNo}: ▶-routing line must not contain /msd:<cmd> colon form, got: ${JSON.stringify(text)}`,
);
// Token-level: extract all /msd... tokens and assert none contain an
// embedded colon (catches /msd-validate:phase etc).
// Skips documentation placeholder tokens like /msd-[command].
const rawTokens = text.match(/\/msd[^\s]*/g) || [];
for (const token of rawTokens) {
assert.ok(
!token.includes(':'),
`secure-phase.md line ${lineNo}: /msd token "${token}" must not contain a colon — embedded colon detected (e.g. /msd-validate:phase), got: ${JSON.stringify(text)}`,
);
}
}
});
test('R3: all staged workflow routing blocks use hyphen form (cross-file sweep)', () => {
// R3 unique value vs W3:
// W3 catches overt /msd:<cmd> at file level (any line).
// R3 adds:
// (a) ▶-line-scoped assertion (catches drift specifically in routing-block context)
// (b) embedded-colon token check (e.g. /msd-validate:phase partial-conversion artifacts)
// not detectable by W3's file-level regex
// Sweeps both workflows/ and references/ so routing blocks in reference files
// are covered alongside workflow files.
const msdDir = path.join(claudeTmpDir, '.claude', 'msd-core');
const workflowsDir = path.join(msdDir, 'workflows');
assert.ok(fs.existsSync(workflowsDir), 'workflows/ must exist for R3 to be meaningful');
const colonOffenders = [];
const embeddedColonOffenders = [];
const walk = (d) => {
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
const fullPath = path.join(d, entry.name);
if (entry.isDirectory()) { walk(fullPath); continue; }
if (!entry.name.endsWith('.md')) continue;
const lines = fs.readFileSync(fullPath, 'utf-8').split(/\r?\n/);
const rel = path.relative(claudeTmpDir, fullPath);
lines.forEach((text, i) => {
if (!text.startsWith('▶')) return;
// Negative: must not contain overt /msd:<cmd> colon form.
if (/\/msd:[a-z]/.test(text)) {
colonOffenders.push(`${rel}:${i + 1}: ${text.trim()}`);
}
// Token-level: check each /msd... token for an embedded colon.
// Catches cases like /msd-validate:phase where normalizer half-converted.
// Documentation placeholder tokens like /msd-[command] are skipped
// because their tokens will not contain a colon.
const tokens = text.match(/\/msd[^\s]*/g) || [];
for (const token of tokens) {
if (token.includes(':')) {
embeddedColonOffenders.push(`${rel}:${i + 1}: token "${token}" in "${text.trim()}"`);
}
}
});
}
};
// Walk both workflows/ and references/ — routing blocks can appear in either.
walk(workflowsDir);
const refsDir = path.join(msdDir, 'references');
if (fs.existsSync(refsDir)) walk(refsDir);
assert.deepEqual(
colonOffenders,
[],
`Staged workflows contain ▶-routing lines with /msd:<cmd> colon form — ` +
`these must resolve to /msd-<cmd> for Claude Code skills-based install. ` +
`Offenders:\n ${colonOffenders.join('\n ')}`,
);
assert.deepEqual(
embeddedColonOffenders,
[],
`Staged workflows contain ▶-routing lines with /msd tokens that have an embedded ` +
`colon (e.g. /msd-validate:phase) — normalizer may have partially converted a token. ` +
`Offenders:\n ${embeddedColonOffenders.join('\n ')}`,
);
});
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// #4324 — colon tokens the install transform CANNOT convert leak to users
// ────────────────────────────────────────────────────────────────────────
//
// Companion to the `#3443` invariant above, and deliberately its mirror image.
// That one asserts the source stays COLON. This one asserts every colon token
// in the source is one the installer can actually turn into hyphen form.
//
// The install rewrite (`transformContentToHyphen`) is gated on an exact match
// against the `commands/msd/*.md` stem list, so a `/msd:<token>` whose token is
// not a registered stem survives the install untouched and reaches the user as
// the deprecated colon form. #4324 reported this as "all auto-suggestions are
// still using the outdated /msd:".
//
// That gate is load-bearing and must NOT be widened: it is the only thing
// protecting the workflow DSL marker family (`msd:section`, `msd:protected`,
// `msd:loop-host`, `msd:guard`, `msd:dispatch`, `msd:plan-revision-conflicts`),
// which is parsed as a literal — `src/workflow-fragments.cts` pins
// `CLOSE_TAG = '/msd:section'`. The fix therefore belongs in the shipped text,
// and this suite is what keeps it there.
{
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 FIXER = path.join(ROOT, 'scripts', 'fix-slash-commands.cjs');
// Drive the REAL production transform with the REAL roster. A roster invented
// here could only confirm what this test's author already believed about the
// gate (fixture-provenance rule, #2371).
const {
transformContentToHyphen,
buildColonPattern,
readCmdNames,
SKIP_DIRS,
} = require(FIXER);
const cmdNames = readCmdNames();
// Shipped surfaces whose text the runtime loads and shows the user.
//
// `skills/` is included even though the #3443 colon-invariant scan omits it —
// but not for the reason that scan omits it. skills/<name>/SKILL.md is
// GENERATED by scripts/gen-plugin-skills.cjs and is already emitted in hyphen
// form, so it is runtime-emitter output rather than colon source. Scanning it
// is a cheap belt-and-braces check that the generator never emits a colon
// token the installer could not convert; it currently contributes zero
// tokens, and that is the expected steady state.
const SCAN_DIRS = [
path.join(ROOT, 'commands', 'msd'),
path.join(ROOT, 'agents'),
path.join(ROOT, 'msd-core', 'workflows'),
path.join(ROOT, 'msd-core', 'references'),
path.join(ROOT, 'msd-core', 'templates'),
path.join(ROOT, 'skills'),
];
// Structural markers that legitimately use `msd:` and are NOT slash commands.
// Enumerated BY FAMILY, never by "it sits in a comment": a blanket
// comment-context waiver would also wave through a genuinely broken command
// reference that merely happens to be commented out, e.g.
// `<!-- see /msd:typo-cmd -->`, which is exactly the leak this guard exists
// to catch.
const COMMENT_MARKER_TOKENS = new Set([
'section', // <!-- msd:section id="…" when="…" --> / <!-- /msd:section -->
'protected', // <!-- msd:protected:start --> / :end
'loop-host', // <!-- msd:loop-host … -->
'plan-revision-conflicts', // <!-- msd:plan-revision-conflicts:begin --> / :end
'live-dom-families', // <!-- msd:live-dom-families -->
'write-continue', // <!-- msd:write-continue … -->
'ui-interaction-capture', // <!-- msd:ui-interaction-capture --> / <!-- /msd:ui-interaction-capture -->
]);
const BARE_MARKER_TOKENS = new Set([
'guard', // `# msd:guard=orchestrator-cwd-drift`
'dispatch', // `[msd:dispatch phase="…" plan="…"]`
]);
const IN_HTML_COMMENT = /<!--[^>]*msd:/;
// `<!-- msd: no compact sibling … -->` — the compact-content disclosure
// banner. Its token is empty, so it is matched by shape rather than by name.
const COMPACT_DISCLOSURE = /<!--\s*msd:\s/;
function collect(dir, out = []) {
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) {
if (SKIP_DIRS.has(e.name)) continue;
collect(full, out);
} else if (e.name.endsWith('.md')) {
out.push(full);
}
}
return out;
}
const shippedFiles = SCAN_DIRS.flatMap((d) => collect(d));
// Same lookbehind as buildColonPattern, so match indices from the two regexes
// are directly comparable.
const ANY_COLON_TOKEN = /(?<![a-zA-Z0-9_-])msd:([a-zA-Z0-9_-]*)/g;
describe('install-convertibility of colon tokens (#4324)', () => {
test('the scan corpus and the real roster are both populated', () => {
assert.ok(cmdNames.length > 0, 'commands/msd/ must yield a non-empty roster');
assert.ok(shippedFiles.length > 0, 'SCAN_DIRS must yield shipped .md files');
assert.ok(cmdNames.includes('quick'), 'quick must be a registered command stem');
});
// ROW 1 — the failing-first regression test for #4324.
test('every msd: token in user-facing source is convertible or a declared structural marker', () => {
const colonPattern = buildColonPattern(cmdNames);
assert.ok(colonPattern, 'buildColonPattern must compile for a non-empty roster');
const violations = [];
for (const file of shippedFiles) {
// allow-test-rule: source-text-is-the-product (#4324)
// These are shipped command/agent/workflow/skill bodies — their text IS
// what the runtime loads and renders, so the deployed text is the contract.
const src = fs.readFileSync(file, 'utf-8');
if (!src.includes('msd:')) continue;
const convertibleAt = new Set(
[...src.matchAll(new RegExp(colonPattern.source, 'g'))].map((m) => m.index),
);
const lines = src.split(/\r?\n/);
for (const m of src.matchAll(ANY_COLON_TOKEN)) {
if (convertibleAt.has(m.index)) continue; // installer handles it
const lineNo = src.slice(0, m.index).split(/\r?\n/).length;
const line = lines[lineNo - 1] || '';
if (BARE_MARKER_TOKENS.has(m[1])) continue; // structural marker
const inComment = IN_HTML_COMMENT.test(line);
if (inComment && COMMENT_MARKER_TOKENS.has(m[1])) continue;
if (inComment && m[1] === '' && COMPACT_DISCLOSURE.test(line)) continue;
violations.push(
`${path.relative(ROOT, file)}:${lineNo}: "${m[0]}" in "${line.trim().slice(0, 90)}"`,
);
}
}
assert.deepEqual(
violations,
[],
`Found ${violations.length} colon token(s) the install transform cannot convert — ` +
`they reach the user as the deprecated /msd: form (#4324).\n` +
`Fix the SHIPPED TEXT, not the transform: the command-stem gate protects the ` +
`msd:section / msd:protected / msd:loop-host marker family.\n` +
`Either close the command token at a boundary ("\`/msd:quick\`-shaped", not ` +
`"/msd:quick-shaped"), stop rendering a non-command as a slash command, or ` +
`declare a new structural marker in BARE_MARKER_TOKENS.\n` +
`Offenders:\n ${violations.join('\n ')}`,
);
});
// ROW 2 — the reported symptom, asserted at the surface the user sees AND
// through the code that actually produces it.
//
// The first version of this test called `transformContentToHyphen` on the
// description line directly and passed — while a real install still shipped
// the colon form, because both hyphen-namespace skill converters rebuild
// `description:` from the raw frontmatter field and never run the transform
// on it. That is the difference between asserting the identity and asserting
// a proxy for it: drive the converter, or the next converter that forgets to
// normalise a field ships the same bug again (#4324).
test('installed skill descriptions carry no colon form', () => {
// allow-test-rule: integration-test-input (#4324)
// The command files are passed to the converters as DATA — real fixture
// input to the transformation under test — and the assertion is on the
// converter's emitted frontmatter, which is the text the host's skill
// picker renders and therefore the deployed contract itself.
const {
convertClaudeCommandToClaudeSkill,
} = require(path.join(ROOT, 'bin', 'install.js'));
const converters = [
['claude', convertClaudeCommandToClaudeSkill],
];
const offenders = [];
let checked = 0;
for (const stem of cmdNames) {
const file = path.join(ROOT, 'commands', 'msd', `${stem}.md`);
if (!fs.existsSync(file)) continue;
const src = fs.readFileSync(file, 'utf-8');
for (const [label, convert] of converters) {
const emitted = convert(src, `msd-${stem}`, null, cmdNames);
const descLine = emitted
.split(/\r?\n/)
.find((l) => l.startsWith('description:'));
if (!descLine) continue;
checked += 1;
if (/msd:/.test(descLine)) {
offenders.push(
`${label} · commands/msd/${stem}.md → ${descLine.trim().slice(0, 110)}`,
);
}
}
}
// Guard against the whole loop silently doing nothing.
assert.ok(
checked >= cmdNames.length,
`expected at least one emitted description per command per converter; checked ${checked}`,
);
assert.deepEqual(
offenders,
[],
`A converter emitted a skill description still carrying the /msd: colon ` +
`form. This is the picker text #4324 reported, and it reaches the user on ` +
`every install.\n ${offenders.join('\n ')}`,
);
});
// ROW 3 — cross-surface parity, across BOTH served reference variants.
// topic.md tells the model which bold signature line to extract AND where
// that line's one-line summary sits. Two things can go wrong, and both did:
// 1. a literal prefix baked into the rule can never match the installed
// reference, which is converted to hyphen form at install time; and
// 2. the two served variants put the summary in DIFFERENT places —
// full.md on the following line, full.compact.md trailing an em-dash on
// the signature line itself. A rule that knows only one shape emits the
// following `Usage:` line as if it were the summary.
test('help topic-mode rule covers every served reference variant', () => {
const modes = path.join(ROOT, 'msd-core', 'workflows', 'help', 'modes');
const topicPath = path.join(modes, 'topic.md');
assert.ok(fs.existsSync(topicPath), 'help/modes/topic.md must exist');
const variants = ['full.md', 'full.compact.md']
.map((name) => ({ name, file: path.join(modes, name) }))
.filter((v) => fs.existsSync(v.file));
assert.equal(variants.length, 2, 'both full.md and full.compact.md must ship');
const shapes = new Set();
for (const variant of variants) {
// allow-test-rule: source-text-is-the-product (#4324)
// The installed reference text IS what the model reads; its rendered
// shape is the contract topic.md's extraction rule is written against.
const converted = transformContentToHyphen(
fs.readFileSync(variant.file, 'utf-8'), cmdNames,
);
const lines = converted.split(/\r?\n/);
const signatureLines = lines.filter((l) => /^\*\*`\/msd[-:]/.test(l));
assert.ok(
signatureLines.length > 0,
`${variant.name} must contain command-signature bold lines`,
);
const prefixes = new Set(
signatureLines.map((l) => l.match(/^\*\*`(\/msd[-:])/)[1]),
);
assert.deepEqual(
[...prefixes], ['/msd-'],
`${variant.name} must ship only the hyphen signature prefix after install conversion`,
);
for (const l of signatureLines) {
shapes.add(/\*\*\s+—\s+\S/.test(l) ? 'same-line' : 'next-line');
}
}
// The variants genuinely disagree. This is WHY topic.md's rule must branch,
// and it is asserted rather than assumed: if the corpus ever collapses to a
// single placement, the two-case rule can be simplified — deliberately.
assert.deepEqual(
[...shapes].sort(), ['next-line', 'same-line'],
'the served variants must between them use both summary placements',
);
// allow-test-rule: source-text-is-the-product (#4324)
// topic.md is shipped workflow text the runtime loads; its wording is the
// deployed contract, so the wording is what must be asserted.
const topic = fs.readFileSync(topicPath, 'utf-8');
const topicConverted = transformContentToHyphen(topic, cmdNames);
// (a) no literal command prefix baked into a bold-signature instruction —
// such a prefix can never match the converted reference.
const instructed = [...topicConverted.matchAll(/\*\*`(\/msd[-:])/g)].map((m) => m[1]);
assert.deepEqual(
instructed, [],
`topic.md bakes the literal prefix(es) ${JSON.stringify(instructed)} into a ` +
`bold-signature instruction, but the installed reference ships '/msd-' after ` +
`conversion — the match can never succeed and --brief silently falls back to ` +
`"heading + first paragraph" on every topic (#4324).`,
);
// (b) the rule must actually COVER both placements. Without this, (a) is
// vacuous: any rewording that merely avoids spelling a literal prefix
// would pass, including one that handles only a single variant.
for (const [needle, why] of [
[/Locating the summary/, 'a named sub-rule that says where the summary sits'],
[/em-dash/, 'the full.compact.md case — summary trailing an em-dash on the signature line'],
[/single non-blank line immediately after/, 'the full.md case — summary on the following line'],
[/`Usage:` line is never a summary/, 'the guard against emitting a Usage: line as the summary'],
]) {
assert.match(
topic, needle,
`topic.md must retain ${why}; without it the compact variant emits the ` +
`following "Usage:" line as if it were the one-line summary (#4324).`,
);
}
});
});
// The gate's own boundary behaviour. These characterize WHY the residuals above
// escape, so a future editor cannot "fix" #4324 by widening the gate without
// reding the marker guard directly below.
describe('colon-gate boundary behaviour (#4324)', () => {
test('a command stem at a token boundary converts', () => {
for (const input of ['/msd:quick ', '`/msd:quick`', '/msd:quick.', '/msd:quick']) {
assert.match(
transformContentToHyphen(input, cmdNames),
/msd-quick/,
`expected ${JSON.stringify(input)} to convert`,
);
}
});
test('a command stem followed by a hyphen is not a command token', () => {
// `quick` is a stem, but `quick-shaped` is not, and the right-hand lookahead
// rejects the following `-`. Nothing matches — this is the #4324 mechanism.
assert.equal(transformContentToHyphen('/msd:quick-shaped', cmdNames), '/msd:quick-shaped');
});
test('longest-stem-first ordering is preserved', () => {
assert.equal(transformContentToHyphen('/msd:quick-batch', cmdNames), '/msd-quick-batch');
});
test('an empty roster converts nothing', () => {
assert.equal(buildColonPattern([]), null);
assert.equal(transformContentToHyphen('/msd:quick', []), '/msd:quick');
});
test('non-command msd- identifiers are left alone', () => {
for (const input of ['/msd-sdk', '/msd-tools']) {
assert.equal(transformContentToHyphen(input, cmdNames), input);
}
});
// NEGATIVE SPACE — the guard that makes "just un-gate the regex" fail loudly.
test('structural markers survive the install transform unchanged', () => {
const markers = [
'<!-- msd:section id="converge-loop" when="state:plan-strategy-converge" -->',
'<!-- /msd:section -->',
'<!-- msd:protected:start -->',
'<!-- msd:protected:end -->',
'<!-- msd:loop-host',
'<!-- msd:plan-revision-conflicts:begin -->',
'<!-- msd:plan-revision-conflicts:end -->',
'# msd:guard=orchestrator-cwd-drift',
'[msd:dispatch phase="{phase_number}" plan="{plan_id}"]',
'<!-- msd:live-dom-families -->',
];
for (const marker of markers) {
assert.equal(
transformContentToHyphen(marker, cmdNames),
marker,
`structural marker must survive byte-identical: ${marker}`,
);
// CRLF variant — same verdict.
assert.equal(
transformContentToHyphen(`${marker}\r\n`, cmdNames),
`${marker}\r\n`,
`structural marker must survive byte-identical under CRLF: ${marker}`,
);
}
});
});
}