Some checks failed
Tests / PR mergeability (push) Successful in 1m37s
Tests / Base branch health (push) Successful in 11s
Tests / Detect test scope (push) Successful in 17s
Tests / lint-tests (push) Failing after 2m16s
Tests / plugin-validate (push) Successful in 1m6s
Tests / test (ubuntu-latest, 24, shard 1/3) (push) Failing after 25s
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 19s
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
Dismiss Unauthorized PR Approvals / dismiss-unauthorized-approval (push) Successful in 9s
Close Draft PRs (sweep) / Sweep open draft PRs (push) Successful in 8s
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
The fork lives on the golem15 Gitea forge, not GitHub. Package identity now parses either host and derives in-place raw URLs for Gitea; the identity-drift lint accepts the new host; README drops GitHub-only badges and the npm quickstart in favour of the checkout installer.
1534 lines
65 KiB
JavaScript
1534 lines
65 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
||
// Reads docs/*.md files whose deployed text IS what the user sees — asserting
|
||
// that every slash-command token in docs resolves to a live registered command
|
||
// tests the deployed contract. The commands/msd/*.md reads in the helper are
|
||
// the source-of-truth registry (product markdown).
|
||
|
||
/**
|
||
* Docs-parity live-registry test (#3049)
|
||
*
|
||
* Replaces three deny-list tests:
|
||
* - bug-3010-reapply-patches-references.test.cjs
|
||
* - bug-3029-3034-stale-command-routes.test.cjs
|
||
* - bug-3042-3044-research-flag-and-stale-refs.test.cjs
|
||
*
|
||
* Polarity: instead of "these specific dead commands must be absent", we
|
||
* assert "every slash-command token in docs must be a live registered command".
|
||
*
|
||
* This catches two failure modes the deny-list shape missed:
|
||
* 1. A freshly-deleted command referenced in docs (no test-file edit needed)
|
||
* 2. A live command renamed without updating docs (deny-list would pass silently)
|
||
*
|
||
* Surfaces scanned:
|
||
* - docs/*.md (English)
|
||
* - docs/{ja-JP,ko-KR,zh-CN,pt-BR}/*.md (localized)
|
||
*
|
||
* ALLOWED_HISTORICAL_MENTIONS: files that legitimately reference deleted
|
||
* commands as part of deprecation documentation are excluded from the scan.
|
||
* Preserved from the three legacy tests:
|
||
* - msd-core/workflows/help.md (deprecation-trail prose)
|
||
* - CHANGELOG.md (historical release notes, must not be rewritten)
|
||
*/
|
||
|
||
'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 { getLiveCommandTokens } = require('./helpers/live-command-registry.cjs');
|
||
|
||
const ROOT = path.join(__dirname, '..');
|
||
const DOCS_DIR = path.join(ROOT, 'docs');
|
||
const LOCALES = ['ja-JP', 'ko-KR', 'zh-CN', 'pt-BR'];
|
||
|
||
// Files that legitimately reference deleted commands as deprecation history.
|
||
// Preserved from the three legacy tests — do not remove without understanding
|
||
// why the exemption exists (see issue #3049 and legacy test comments).
|
||
const ALLOWED_HISTORICAL_MENTIONS = new Set([
|
||
path.join(ROOT, 'msd-core', 'workflows', 'help.md'),
|
||
path.join(ROOT, 'CHANGELOG.md'),
|
||
]);
|
||
|
||
// RELEASE-*.md files document past behavior for historical record.
|
||
// They must not be rewritten, so they are exempt from the live-registry check.
|
||
// Pattern: docs/RELEASE-*.md
|
||
function isReleaseDoc(filePath) {
|
||
return path.basename(filePath).startsWith('RELEASE-') && filePath.endsWith('.md');
|
||
}
|
||
|
||
// Slugs that appear in docs as internal component names or documentation
|
||
// syntax placeholders — they match the /msd-* regex but are NOT user-typable
|
||
// slash commands and never appear in the command registry. Adding a slug here
|
||
// requires a code comment explaining why it is not a slash command.
|
||
//
|
||
// Do NOT add here:
|
||
// - deleted slash commands (those should be scrubbed from docs)
|
||
// - renamed commands (update the docs instead)
|
||
const INTERNAL_COMPONENT_SLUGS = new Set([
|
||
// Documentation syntax placeholder — "command-name" is used in ARCHITECTURE.md,
|
||
// COMMANDS.md, and USER-GUIDE.md to show the template form of a slash command
|
||
// (e.g. "/msd-command-name [args]"). It is not a registered command.
|
||
'command-name',
|
||
'command',
|
||
|
||
// msd-tools.cjs — the legacy Node CLI binary (bin/msd-tools.cjs).
|
||
// Docs reference it as a path component in shell examples, not as a slash command.
|
||
// Example: node "$HOME/.claude/msd-core/bin/msd-tools.cjs" state validate
|
||
'tools',
|
||
|
||
// Hook scripts — internal runtime hooks, not user-invocable slash commands.
|
||
// hooks/msd-statusline.js — session statusline hook
|
||
// hooks/msd-context-monitor.js — context-window monitor hook
|
||
// hooks/msd-update-banner.js — update-available banner hook
|
||
// hooks/msd-graphify-update.sh — knowledge-graph auto-update PostToolUse hook (#3347)
|
||
// These appear in docs as file-path references (e.g. "msd-statusline.js reads
|
||
// the cache"), not as command invocations.
|
||
'statusline',
|
||
'context-monitor',
|
||
'update-banner',
|
||
'graphify-update',
|
||
|
||
// msd-update-check.json — background update-check CACHE FILE, not a slash command.
|
||
// ARCHITECTURE.md references "~/.cache/msd/msd-update-check.json" as a path;
|
||
// the regex captures "/msd-update-check" from the path component.
|
||
'update-check',
|
||
|
||
// Internal agent names referenced in ARCHITECTURE.md tables of agents.
|
||
// These are spawned agents (msd-planner, etc.), not user-typable slash commands.
|
||
'planner',
|
||
|
||
// Malformed token from SDK init reference: "/msd-init-" appears as a truncated
|
||
// prefix in CLI-TOOLS.md describing the msd-sdk init command family
|
||
// (e.g., "msd-sdk query init.phase-op 12"). The regex captures "/msd-init-"
|
||
// without a following slug — this is a documentation formatting artifact, not
|
||
// a real command token.
|
||
'init-',
|
||
|
||
// Compatibility guard for legacy doc links that may include
|
||
// legacy org path segments in migrated historical URLs.
|
||
// This is not a user-typable slash command.
|
||
'build',
|
||
|
||
// ~/msd-workspaces/ — filesystem directory path used by /msd-workspace.
|
||
// Docs reference "~/msd-workspaces/<name>" as the default workspace directory
|
||
// in shell examples and option tables (e.g. "--path /target (default: ~/msd-workspaces/<name>)").
|
||
// The regex captures "/msd-workspaces" from the path component. The LIVE slash
|
||
// command is "/msd-workspace" (singular) — not "/msd-workspaces" (plural).
|
||
'workspaces',
|
||
|
||
// Portuguese translation of "command" — pt-BR/ARCHITECTURE.md uses "/msd-comando"
|
||
// as the localized equivalent of the "/msd-command-name" English placeholder
|
||
// in an architecture flow diagram. Not a registered command.
|
||
'comando',
|
||
|
||
// GitHub repository name: zh-CN/README.md references "github.com/rokicool/gsd-opencode"
|
||
// as an external community project URL. The regex captures "/gsd-opencode" from
|
||
// the URL path. Not a user-typable slash command in this product.
|
||
'opencode',
|
||
|
||
// msd-sdk — the @opengsd/gsd-sdk npm package and `msd-sdk query` CLI binary.
|
||
// Docs reference it as a package name (e.g. `@opengsd/gsd-sdk`) and CLI tool
|
||
// (e.g. `msd-sdk query init phase-op 12`). The regex captures "/msd-sdk" from
|
||
// the npm scope path separator in `@opengsd/gsd-sdk`. Not a user-typable slash command.
|
||
'sdk',
|
||
|
||
// Smoke-test directory path — locale docs reference "/tmp/msd-smoke-$(date +%s)"
|
||
// as a temporary directory path in bash code-block examples. The regex captures
|
||
// "/msd-smoke-" from the filesystem path. Not a slash command.
|
||
'smoke-',
|
||
|
||
// Template placeholders — zh-CN/references/ui-brand.md used "/msd-alternative-1"
|
||
// and "/msd-alternative-2" as unfilled placeholders in a UI template example.
|
||
// These were never registered commands. Fixed in the source doc; kept here as
|
||
// a belt-and-suspenders guard against the pattern returning in other locale docs.
|
||
'alternative-1',
|
||
'alternative-2',
|
||
|
||
// msd-sync-skills — installed Claude skill directory name (also a workflow
|
||
// under msd-core/workflows/sync-skills.md), but NOT a registered
|
||
// slash command (no commands/msd/sync-skills.md). Docs reference it as a
|
||
// filesystem path component, e.g. "~/.agents/skills/msd-sync-skills/" in
|
||
// docs/discussions/grok-build-support-2026-05.md. The regex captures
|
||
// "/msd-sync-skills" from the path. Invoked via Skill(skill="msd-sync-skills").
|
||
'sync-skills',
|
||
|
||
// msd-test-runner — GitHub repository name: "github.com/open-gsd/gsd-test-runner".
|
||
// docs/contributing/bootstrap.md references it as a hyperlink target:
|
||
// [msd-test-runner](https://github.com/open-gsd/gsd-test-runner)
|
||
// The regex captures "/msd-test-runner" from the URL path component. This is
|
||
// an external tool repo, not a user-typable slash command in this product.
|
||
'test-runner',
|
||
|
||
// msd-core — GitHub repository name: "golem15/msd-core".
|
||
// docs/adr/22-plan-drift-guard.md references it as an issue tracker link:
|
||
// golem15/msd-core#22
|
||
// The regex captures "/msd-core" from the org/repo path separator. This is
|
||
// the canonical repo name, not a user-typable slash command in this product.
|
||
'core',
|
||
]);
|
||
|
||
/**
|
||
* Strip HTML comments from content to avoid flagging commented-out examples
|
||
* or prose that names a dead command for historical context (e.g. "previously
|
||
* this was /msd-old-name...").
|
||
*/
|
||
function stripHtmlComments(content) {
|
||
// regex-free HTML-comment stripper (CodeQL: avoid incomplete-multi-character-sanitization)
|
||
let out = '';
|
||
let rest = content;
|
||
let idx;
|
||
while ((idx = rest.indexOf('<!--')) !== -1) {
|
||
out += rest.slice(0, idx);
|
||
const end = rest.indexOf('-->', idx + 4);
|
||
if (end === -1) { rest = ''; break; }
|
||
rest = rest.slice(end + 3);
|
||
}
|
||
return out + rest;
|
||
}
|
||
|
||
/**
|
||
* Extract the set of slash-command tokens from markdown content.
|
||
* Three forms per command per runtime:
|
||
* /msd-slug — Claude / non-Gemini
|
||
* /msd:slug — Gemini
|
||
* $msd-slug — Codex
|
||
*
|
||
* Internal component slugs (INTERNAL_COMPONENT_SLUGS) are filtered out —
|
||
* those are file-path references or documentation placeholders, not slash
|
||
* command invocations.
|
||
*
|
||
* Returns: { slash: Set<string>, colon: Set<string>, dollar: Set<string> }
|
||
*/
|
||
function extractCommandTokens(content) {
|
||
const stripped = stripHtmlComments(content);
|
||
|
||
function isInternal(token) {
|
||
// Strip the /msd- or /msd: or $msd- prefix to get the slug
|
||
const slug = token.replace(/^(?:\/msd[:-]|\$msd-)/, '');
|
||
// Exact match OR prefix match for 'init-' (which ends with a dash)
|
||
if (INTERNAL_COMPONENT_SLUGS.has(slug)) return true;
|
||
for (const s of INTERNAL_COMPONENT_SLUGS) {
|
||
if (s.endsWith('-') && slug.startsWith(s)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Negative lookbehind: only match tokens NOT preceded by a letter, digit,
|
||
// `/`, `_`, or `-`. This prevents matching the `/msd-core` substring inside
|
||
// the org/repo path `golem15/msd-core` (and similar path-embedded segments)
|
||
// while still matching real invocations preceded by BOL, space, backtick, or
|
||
// `(`. Fixes false-positive class identified in #489.
|
||
const allSlash = (stripped.match(/(?<![A-Za-z0-9/_-])\/msd-[a-z0-9][a-z0-9-]*/g) || []);
|
||
const allColon = (stripped.match(/(?<![A-Za-z0-9/_-])\/msd:[a-z0-9][a-z0-9-]*/g) || []);
|
||
const allDollar = (stripped.match(/(?<![A-Za-z0-9/_-])\$msd-[a-z0-9][a-z0-9-]*/g) || []);
|
||
|
||
const slash = new Set(allSlash.filter(t => !isInternal(t)));
|
||
const colon = new Set(allColon.filter(t => !isInternal(t)));
|
||
const dollar = new Set(allDollar.filter(t => !isInternal(t)));
|
||
return { slash, colon, dollar };
|
||
}
|
||
|
||
/**
|
||
* Walk a directory and return all .md files recursively.
|
||
* Uses hand-rolled DFS rather than `fs.readdirSync(dir, { recursive: true })`
|
||
* — the recursive option is available on every CI matrix entry now that the
|
||
* Node floor is >=24.0.0 (it landed in Node 20.1), but this walker predates
|
||
* that bump and was not rewritten as part of it; unchanged, not stale.
|
||
* Surfaces permission-denied errors as structured warnings (PRED.k302) rather
|
||
* than silently skipping.
|
||
*/
|
||
function listMdFiles(dir) {
|
||
if (!fs.existsSync(dir)) return [];
|
||
const files = [];
|
||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||
const fullPath = path.join(dir, entry.name);
|
||
if (entry.isDirectory()) {
|
||
try {
|
||
files.push(...listMdFiles(fullPath));
|
||
} catch (err) {
|
||
process.stderr.write('[docs-parity] WARNING: skipping unreadable directory ' + fullPath + ': ' + err.message + '\n');
|
||
}
|
||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||
files.push(fullPath);
|
||
}
|
||
}
|
||
return files;
|
||
}
|
||
|
||
/**
|
||
* Assert that every command token in a doc file resolves to the live registry.
|
||
* Returns an array of diagnostic strings (empty = pass).
|
||
*/
|
||
function findUnknownTokens(filePath, liveTokens) {
|
||
const content = fs.readFileSync(filePath, 'utf-8');
|
||
const { slash, colon, dollar } = extractCommandTokens(content);
|
||
const unknowns = [];
|
||
for (const token of slash) {
|
||
if (!liveTokens.has(token)) unknowns.push(token);
|
||
}
|
||
for (const token of colon) {
|
||
if (!liveTokens.has(token)) unknowns.push(token);
|
||
}
|
||
for (const token of dollar) {
|
||
if (!liveTokens.has(token)) unknowns.push(token);
|
||
}
|
||
return unknowns;
|
||
}
|
||
|
||
// ─── Helper unit tests ────────────────────────────────────────────────────────
|
||
|
||
describe('getLiveCommandTokens() — helper contract', () => {
|
||
test('returns a Set', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(result instanceof Set, 'getLiveCommandTokens() must return a Set');
|
||
});
|
||
|
||
test('returns a non-empty set (commands/msd/ has registered commands)', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(result.size > 0, 'live registry must contain at least one token');
|
||
});
|
||
|
||
test('contains /msd-help (from commands/msd/help.md name: msd:help)', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(result.has('/msd-help'), 'registry must contain /msd-help');
|
||
});
|
||
|
||
test('contains /msd:help (Gemini form)', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(result.has('/msd:help'), 'registry must contain /msd:help');
|
||
});
|
||
|
||
test('contains $msd-help (Codex form)', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(result.has('$msd-help'), 'registry must contain $msd-help');
|
||
});
|
||
|
||
test('contains /msd-plan-phase (from commands/msd/plan-phase.md)', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(result.has('/msd-plan-phase'), 'registry must contain /msd-plan-phase');
|
||
});
|
||
|
||
test('contains exactly 3 tokens per slug (slash, colon, dollar)', () => {
|
||
const result = getLiveCommandTokens();
|
||
// Every /msd-slug should have a matching /msd:slug and $msd-slug
|
||
const slashTokens = [...result].filter(t => t.startsWith('/msd-'));
|
||
for (const slash of slashTokens) {
|
||
const slug = slash.slice('/msd-'.length);
|
||
assert.ok(
|
||
result.has(`/msd:${slug}`),
|
||
`registry must contain Gemini form /msd:${slug} for slash form ${slash}`
|
||
);
|
||
assert.ok(
|
||
result.has(`$msd-${slug}`),
|
||
`registry must contain Codex form $msd-${slug} for slash form ${slash}`
|
||
);
|
||
}
|
||
});
|
||
|
||
test('does NOT contain removed /msd-reapply-patches', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(!result.has('/msd-reapply-patches'), 'registry must NOT contain removed /msd-reapply-patches');
|
||
});
|
||
|
||
test('does NOT contain removed /msd-code-review-fix', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(!result.has('/msd-code-review-fix'), 'registry must NOT contain removed /msd-code-review-fix');
|
||
});
|
||
|
||
test('does NOT contain removed /msd-status', () => {
|
||
const result = getLiveCommandTokens();
|
||
assert.ok(!result.has('/msd-status'), 'registry must NOT contain removed /msd-status');
|
||
});
|
||
|
||
test('memoizes — returns the same Set reference on repeated calls', () => {
|
||
const a = getLiveCommandTokens();
|
||
const b = getLiveCommandTokens();
|
||
assert.strictEqual(a, b, 'getLiveCommandTokens() must return the same Set instance (memoized)');
|
||
});
|
||
});
|
||
|
||
// ─── Fixture-based helper tests ───────────────────────────────────────────────
|
||
|
||
describe('getLiveCommandTokens() — fixture contract', () => {
|
||
test('parses msd:foo frontmatter and emits 3 canonical tokens', () => {
|
||
// This test validates the parsing logic against a known-good fixture
|
||
// by inspecting the live registry for commands/msd/help.md (name: msd:help).
|
||
// Fixture file tests are done inline since the helper reads commands/msd/ only.
|
||
// The canonical token contract:
|
||
// name: msd:foo → /msd-foo, /msd:foo, $msd-foo
|
||
const registry = getLiveCommandTokens();
|
||
// We know help.md has name: msd:help
|
||
const slug = 'help';
|
||
assert.ok(registry.has(`/msd-${slug}`), `must have /msd-${slug}`);
|
||
assert.ok(registry.has(`/msd:${slug}`), `must have /msd:${slug}`);
|
||
assert.ok(registry.has(`$msd-${slug}`), `must have $msd-${slug}`);
|
||
});
|
||
|
||
test('parses msd-slug frontmatter (ns-* commands) and emits 3 tokens', () => {
|
||
// ns-context.md has name: msd-context (dash-style, no colon)
|
||
const registry = getLiveCommandTokens();
|
||
assert.ok(registry.has('/msd-context'), 'must have /msd-context (from ns-context.md)');
|
||
assert.ok(registry.has('/msd:context'), 'must have /msd:context (Gemini form)');
|
||
assert.ok(registry.has('$msd-context'), 'must have $msd-context (Codex form)');
|
||
});
|
||
});
|
||
|
||
// ─── English docs parity check ───────────────────────────────────────────────
|
||
|
||
// Precomputed locale directory prefixes for efficient exclusion in the English scan.
|
||
const LOCALE_DIRS = LOCALES.map(l => path.join(DOCS_DIR, l) + path.sep);
|
||
|
||
/**
|
||
* List all .md files under dir, excluding files under any of the known locale
|
||
* subdirectories (which are covered by the per-locale describe blocks below).
|
||
*/
|
||
function listEnglishMdFiles(dir) {
|
||
return listMdFiles(dir).filter(
|
||
f => !LOCALE_DIRS.some(ld => f.startsWith(ld))
|
||
);
|
||
}
|
||
|
||
describe('docs parity — English docs/*.md ⊆ liveRegistry', () => {
|
||
test('docs/ directory exists and contains markdown files', () => {
|
||
const files = listEnglishMdFiles(DOCS_DIR);
|
||
assert.ok(files.length > 0, `expected markdown files under ${DOCS_DIR}`);
|
||
});
|
||
|
||
test('every slash-command token in docs/*.md resolves to a live command', () => {
|
||
const liveTokens = getLiveCommandTokens();
|
||
const docFiles = listEnglishMdFiles(DOCS_DIR);
|
||
const allOffenders = [];
|
||
|
||
for (const filePath of docFiles) {
|
||
if (ALLOWED_HISTORICAL_MENTIONS.has(filePath)) continue;
|
||
if (isReleaseDoc(filePath)) continue;
|
||
|
||
const unknowns = findUnknownTokens(filePath, liveTokens);
|
||
if (unknowns.length > 0) {
|
||
allOffenders.push(
|
||
`${path.relative(ROOT, filePath)}: unknown command token(s): [${unknowns.join(', ')}]`
|
||
);
|
||
}
|
||
}
|
||
|
||
assert.deepStrictEqual(
|
||
allOffenders,
|
||
[],
|
||
'docs/*.md must only reference live registered commands:\n ' + allOffenders.join('\n ')
|
||
);
|
||
});
|
||
});
|
||
|
||
// ─── Localized docs parity check ─────────────────────────────────────────────
|
||
|
||
for (const locale of LOCALES) {
|
||
const localeDir = path.join(DOCS_DIR, locale);
|
||
|
||
describe(`docs parity — docs/${locale}/*.md ⊆ liveRegistry`, () => {
|
||
test(`docs/${locale}/ exists and contains markdown files (or is empty/absent — skip gracefully)`, () => {
|
||
if (!fs.existsSync(localeDir)) {
|
||
// Some locales may not exist in every repo state — that is fine.
|
||
return;
|
||
}
|
||
// If the dir exists, it should have at least one .md file.
|
||
const files = listMdFiles(localeDir);
|
||
// Warn but don't fail if locale dir is unexpectedly empty.
|
||
// The parity test below will simply pass vacuously.
|
||
assert.ok(
|
||
files.length >= 0,
|
||
`docs/${locale}/ exists but contains no markdown files`
|
||
);
|
||
});
|
||
|
||
test(`every slash-command token in docs/${locale}/*.md resolves to a live command`, () => {
|
||
if (!fs.existsSync(localeDir)) return;
|
||
|
||
const liveTokens = getLiveCommandTokens();
|
||
const docFiles = listMdFiles(localeDir);
|
||
const allOffenders = [];
|
||
|
||
for (const filePath of docFiles) {
|
||
if (ALLOWED_HISTORICAL_MENTIONS.has(filePath)) continue;
|
||
if (isReleaseDoc(filePath)) continue;
|
||
|
||
const unknowns = findUnknownTokens(filePath, liveTokens);
|
||
if (unknowns.length > 0) {
|
||
allOffenders.push(
|
||
`${path.relative(ROOT, filePath)}: unknown command token(s): [${unknowns.join(', ')}]`
|
||
);
|
||
}
|
||
}
|
||
|
||
assert.deepStrictEqual(
|
||
allOffenders,
|
||
[],
|
||
`docs/${locale}/*.md must only reference live registered commands:\n ` + allOffenders.join('\n ')
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── Adversarial regression tests ────────────────────────────────────────────
|
||
|
||
describe('adversarial: polarity inversion catches drift deny-list misses', () => {
|
||
test('renaming a live command without updating docs would fail this test (demonstrated via token absence)', () => {
|
||
// If /msd-progress were renamed to /msd-status-new, the old /msd-progress
|
||
// token would not appear in the live registry, and any doc referencing
|
||
// /msd-progress would fail. The deny-list shape would have passed silently
|
||
// (it only checks for specific known-bad tokens).
|
||
// We can't simulate an actual rename in a live test, but we can assert
|
||
// that the registry correctly contains the live name (progress, not status):
|
||
const registry = getLiveCommandTokens();
|
||
assert.ok(registry.has('/msd-progress'), '/msd-progress must be live (not renamed to /msd-status)');
|
||
assert.ok(!registry.has('/msd-status'), '/msd-status must be absent (was deleted, replaced by /msd-progress)');
|
||
});
|
||
|
||
test('freshly-deleted command /msd-check-todos is absent from registry', () => {
|
||
const registry = getLiveCommandTokens();
|
||
assert.ok(!registry.has('/msd-check-todos'), '/msd-check-todos must not be in the live registry');
|
||
});
|
||
|
||
test('freshly-deleted command /msd-new-workspace is absent from registry', () => {
|
||
const registry = getLiveCommandTokens();
|
||
assert.ok(!registry.has('/msd-new-workspace'), '/msd-new-workspace must not be in the live registry');
|
||
});
|
||
|
||
test('freshly-deleted command /msd-plan-milestone-gaps is absent from registry', () => {
|
||
const registry = getLiveCommandTokens();
|
||
assert.ok(!registry.has('/msd-plan-milestone-gaps'), '/msd-plan-milestone-gaps must not be in the live registry');
|
||
});
|
||
|
||
test('freshly-deleted command /msd-research-phase is absent from registry', () => {
|
||
const registry = getLiveCommandTokens();
|
||
assert.ok(!registry.has('/msd-research-phase'), '/msd-research-phase must not be in the live registry');
|
||
});
|
||
});
|
||
|
||
// ─── Tokenizer regression tests (#489) ───────────────────────────────────────
|
||
|
||
describe('extractCommandTokens() — repo-path false-positive regression (#489)', () => {
|
||
test('golem15/msd-core#22 repo path does NOT produce a /msd-core token', () => {
|
||
// Before the lookbehind fix, /msd-core inside `golem15/msd-core#22`
|
||
// would be matched by the slash regex — a false positive.
|
||
const { slash, colon, dollar } = extractCommandTokens(
|
||
'see golem15/msd-core#22 for details'
|
||
);
|
||
const all = [...slash, ...colon, ...dollar];
|
||
assert.ok(
|
||
!all.includes('/msd-core'),
|
||
'repo path golem15/msd-core#22 must not produce a /msd-core token; got: ' + all.join(', ')
|
||
);
|
||
assert.strictEqual(all.length, 0, 'expected zero tokens from a bare repo-path string; got: ' + all.join(', '));
|
||
});
|
||
|
||
test('space-preceded /msd-totally-not-a-real-command is still extracted (real invocation)', () => {
|
||
// A genuine (but unregistered) command reference after whitespace must be
|
||
// captured so the live-registry check can flag it as unknown.
|
||
const { slash } = extractCommandTokens(
|
||
'run /msd-totally-not-a-real-command here'
|
||
);
|
||
assert.ok(
|
||
slash.has('/msd-totally-not-a-real-command'),
|
||
'invocation after whitespace must be extracted; slash set: ' + [...slash].join(', ')
|
||
);
|
||
});
|
||
|
||
test('backtick-wrapped `/msd-plan` is still extracted (real invocation)', () => {
|
||
// Backtick-wrapped commands (common in markdown) must still be captured.
|
||
const { slash } = extractCommandTokens(
|
||
'use `/msd-plan` to plan'
|
||
);
|
||
assert.ok(
|
||
slash.has('/msd-plan'),
|
||
'backtick-wrapped invocation must be extracted; slash set: ' + [...slash].join(', ')
|
||
);
|
||
});
|
||
});
|
||
|
||
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
// Folded from tests/bug-2954-help-md-slash-command-stubs.test.cjs — consolidation epic #1969 (B3 #1972)
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
{
|
||
const { describe: __foldDescribe } = require('node:test');
|
||
__foldDescribe("folded:bug-2954-help-md-slash-command-stubs (consolidation epic #1969 B3 #1972)", () => {
|
||
// allow-test-rule: source-text-is-the-product (see #2954)
|
||
// Workflow .md / agent .md / command .md / reference .md files — their text
|
||
// IS what the runtime loads. Testing text content tests the deployed contract.
|
||
// Per CONTRIBUTING.md exception matrix.
|
||
'use strict';
|
||
|
||
|
||
process.env.MSD_TEST_MODE = '1';
|
||
|
||
/**
|
||
* Bug #2954: keep `help.md` and the live `commands/msd/*` slash surface
|
||
* in lockstep. Two regression tests:
|
||
*
|
||
* 1. help.md must not advertise any /msd[-:]<name> that has no shipped
|
||
* slash command. (Caught the original #2954 regression: #2824 deleted
|
||
* 31 stubs without updating help.md.)
|
||
*
|
||
* 2. Every shipped /msd[-:]<name> command must appear in help.md. (Caught
|
||
* the inverse: a command lands without docs, so users never discover it.)
|
||
*
|
||
* The shipped slash name is parsed from frontmatter `name:` (which can be
|
||
* either `msd:foo` or `msd-foo` — Claude Code surfaces both as `/msd-foo`),
|
||
* NOT from the filename, because some files (e.g. `ns-context.md`) ship a
|
||
* different slash name (`msd-context`) than their filename suggests.
|
||
*
|
||
* Also covers `do.md`, the dispatcher invoked at runtime by
|
||
* `/msd:progress --do`: any `/msd[-:]<name>` token in its routing table must
|
||
* resolve to a live command, otherwise the dispatcher emits "Unknown command".
|
||
*/
|
||
|
||
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');
|
||
// After #3039, the canonical command reference is the `--full` mode file.
|
||
// `workflows/help.md` is now a small dispatcher; the bidirectional parity
|
||
// invariant lives with the comprehensive reference body.
|
||
const HELP_MD = path.join(ROOT, 'msd-core', 'workflows', 'help', 'modes', 'full.md');
|
||
const DO_MD = path.join(ROOT, 'msd-core', 'workflows', 'do.md');
|
||
|
||
function parseFrontmatter(content) {
|
||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||
if (!match) return null;
|
||
const fields = {};
|
||
for (const line of match[1].split(/\r?\n/)) {
|
||
const fieldMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
||
if (!fieldMatch) continue;
|
||
const value = fieldMatch[2].trim().replace(/^["']|["']$/g, '');
|
||
fields[fieldMatch[1]] = value;
|
||
}
|
||
return fields;
|
||
}
|
||
|
||
/**
|
||
* Returns the set of slash-base-names actually shipped under commands/msd/.
|
||
* A "slash-base-name" is the part after `/msd-` — e.g. for frontmatter
|
||
* `name: msd:foo` or `name: msd-foo`, the slash-base-name is `foo`.
|
||
*/
|
||
function listShippedSlashBaseNames() {
|
||
const names = new Set();
|
||
for (const entry of fs.readdirSync(COMMANDS_DIR, { withFileTypes: true })) {
|
||
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
||
const content = fs.readFileSync(path.join(COMMANDS_DIR, entry.name), 'utf8');
|
||
const fm = parseFrontmatter(content);
|
||
if (!fm || !fm.name) continue;
|
||
const fmName = fm.name;
|
||
let base = null;
|
||
if (fmName.startsWith('msd:')) base = fmName.slice(4);
|
||
else if (fmName.startsWith('msd-')) base = fmName.slice(4);
|
||
if (base && /^[a-z][a-z0-9-]*$/.test(base)) names.add(base);
|
||
}
|
||
return names;
|
||
}
|
||
|
||
function extractSlashReferences(contents) {
|
||
const names = new Set();
|
||
// Negative lookbehind: must not be preceded by a letter or digit (avoids matching npm scope
|
||
// paths like @golem15/msd-core where `/msd-` appears inside a package URL).
|
||
// Negative lookahead (?![\w-]*\/): excludes filesystem path segments like
|
||
// `/msd-core/bin` where the captured name is followed by a `/`, which would
|
||
// be a directory segment rather than a slash command name.
|
||
const tokenRe = /(?<![a-z0-9])\/msd[:-]([a-z][a-z0-9-]*)(?![\w-]*\/)/g;
|
||
let match;
|
||
while ((match = tokenRe.exec(contents)) !== null) {
|
||
names.add(match[1]);
|
||
}
|
||
return names;
|
||
}
|
||
|
||
/**
|
||
* For every shipped command with an `argument-hint:` frontmatter entry,
|
||
* collect the `--flag` tokens it advertises. Returns a Map<slashBaseName,
|
||
* Set<flagName>>. Flags are recorded without their leading `--`.
|
||
*/
|
||
function listShippedFlagsByCommand() {
|
||
const out = new Map();
|
||
for (const entry of fs.readdirSync(COMMANDS_DIR, { withFileTypes: true })) {
|
||
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
||
const content = fs.readFileSync(path.join(COMMANDS_DIR, entry.name), 'utf8');
|
||
const fm = parseFrontmatter(content);
|
||
if (!fm || !fm.name || !fm['argument-hint']) continue;
|
||
const fmName = fm.name;
|
||
let base = null;
|
||
if (fmName.startsWith('msd:')) base = fmName.slice(4);
|
||
else if (fmName.startsWith('msd-')) base = fmName.slice(4);
|
||
if (!base || !/^[a-z][a-z0-9-]*$/.test(base)) continue;
|
||
const flags = new Set();
|
||
for (const m of fm['argument-hint'].matchAll(/--([a-z][a-z0-9-]*)/g)) {
|
||
flags.add(m[1]);
|
||
}
|
||
if (flags.size) out.set(base, flags);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
describe('Bug #2954: help.md ↔ commands/msd/ bidirectional parity', () => {
|
||
test('every /msd[-:]<name> referenced in help.md is a shipped command', () => {
|
||
const helpContents = fs.readFileSync(HELP_MD, 'utf8');
|
||
const referenced = extractSlashReferences(helpContents);
|
||
const shipped = listShippedSlashBaseNames();
|
||
const dangling = [...referenced].filter((n) => !shipped.has(n)).sort();
|
||
assert.deepEqual(
|
||
dangling,
|
||
[],
|
||
`help.md advertises /msd[-:]<name> commands that are not shipped: ${dangling.join(', ')}`,
|
||
);
|
||
});
|
||
|
||
test('every shipped /msd[-:]<name> command is documented in help.md', () => {
|
||
const helpContents = fs.readFileSync(HELP_MD, 'utf8');
|
||
const referenced = extractSlashReferences(helpContents);
|
||
const shipped = listShippedSlashBaseNames();
|
||
const undocumented = [...shipped].filter((n) => !referenced.has(n)).sort();
|
||
assert.deepEqual(
|
||
undocumented,
|
||
[],
|
||
`commands shipped under commands/msd/ with no /msd[-:]<name> reference in help.md: ${undocumented.join(', ')}`,
|
||
);
|
||
});
|
||
|
||
test('every /msd[-:]<name> in do.md (live dispatcher) is a shipped command', () => {
|
||
const doContents = fs.readFileSync(DO_MD, 'utf8');
|
||
const referenced = extractSlashReferences(doContents);
|
||
const shipped = listShippedSlashBaseNames();
|
||
const dangling = [...referenced].filter((n) => !shipped.has(n)).sort();
|
||
assert.deepEqual(
|
||
dangling,
|
||
[],
|
||
`do.md routing table references /msd[-:]<name> that is not shipped: ${dangling.join(', ')}`,
|
||
);
|
||
});
|
||
|
||
test('every --flag in a command\'s argument-hint appears in help.md', () => {
|
||
const helpContents = fs.readFileSync(HELP_MD, 'utf8');
|
||
const flagsByCommand = listShippedFlagsByCommand();
|
||
const gaps = [];
|
||
for (const [command, flags] of flagsByCommand) {
|
||
for (const flag of flags) {
|
||
// Accept `/msd-<command> --<flag>` (precise) OR a bare `--<flag>` token
|
||
// anywhere in help.md (good enough for shared flags like `--force` that
|
||
// appear under multiple commands' descriptions).
|
||
const preciseDash = `/msd-${command} --${flag}`;
|
||
const preciseColon = `/msd:${command} --${flag}`;
|
||
const flagToken = `--${flag}`;
|
||
if (
|
||
!helpContents.includes(preciseDash) &&
|
||
!helpContents.includes(preciseColon) &&
|
||
!helpContents.includes(flagToken)
|
||
) {
|
||
gaps.push(`/msd:${command} --${flag}`);
|
||
}
|
||
}
|
||
}
|
||
assert.deepEqual(
|
||
gaps.sort(),
|
||
[],
|
||
`commands ship --flag(s) in argument-hint that are absent from help.md: ${gaps.join(', ')}`,
|
||
);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
// Folded from tests/bug-2950-stale-command-refs.test.cjs — consolidation epic #1969 (B4 #1973)
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
{
|
||
const { describe: __foldDescribe } = require('node:test');
|
||
__foldDescribe("folded:bug-2950-stale-command-refs (consolidation epic #1969 B4 #1973)", () => {
|
||
/**
|
||
* Bug #2950: Stale deleted command references in workflow files
|
||
*
|
||
* Multiple workflow files referenced command names removed in #2790
|
||
* (msd-add-phase, msd-insert-phase, msd-remove-phase, msd-add-todo,
|
||
* msd-set-profile, msd-settings-integrations, msd-settings-advanced,
|
||
* msd-spike-wrap-up, msd-sketch-wrap-up, msd-code-review-fix).
|
||
*
|
||
* Fix: Update every occurrence to the new consolidated forms:
|
||
* /msd:phase (no flag | --insert | --remove)
|
||
* /msd:capture
|
||
* /msd:config (--profile | --integrations | --advanced)
|
||
* /msd:spike --wrap-up
|
||
* /msd:sketch --wrap-up
|
||
* /msd:code-review --fix
|
||
*/
|
||
|
||
'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 WORKFLOWS_DIR = path.join(__dirname, '..', 'msd-core', 'workflows');
|
||
|
||
function read(filename) {
|
||
return fs.readFileSync(path.join(WORKFLOWS_DIR, filename), 'utf-8');
|
||
}
|
||
|
||
// Deleted command names that must not appear anywhere in the fixed files.
|
||
const DELETED_COMMANDS = [
|
||
'/msd-add-phase',
|
||
'/msd-insert-phase',
|
||
'/msd-remove-phase',
|
||
'/msd-add-todo',
|
||
'/msd-set-profile',
|
||
'/msd-settings-integrations',
|
||
'/msd-settings-advanced',
|
||
'/msd-spike-wrap-up',
|
||
'/msd-sketch-wrap-up',
|
||
'/msd-code-review-fix',
|
||
];
|
||
|
||
// Per-file assertions: [file, deletedCmd, newForm]
|
||
const FILE_ASSERTIONS = [
|
||
// help.md → moved to help/modes/full.md in #3039 tiered-help refactor
|
||
['help/modes/full.md', '/msd-add-phase', '/msd:phase "Add admin dashboard"'],
|
||
['help/modes/full.md', '/msd-insert-phase', '/msd:phase --insert 7 "Fix critical auth bug"'],
|
||
['help/modes/full.md', '/msd-remove-phase', '/msd:phase --remove 17'],
|
||
['help/modes/full.md', '/msd-spike-wrap-up', '/msd:spike --wrap-up'],
|
||
['help/modes/full.md', '/msd-sketch-wrap-up', '/msd:sketch --wrap-up'],
|
||
['help/modes/full.md', '/msd-add-todo', '/msd:capture'],
|
||
['help/modes/full.md', '/msd-set-profile', '/msd:config --profile budget'],
|
||
|
||
// do.md
|
||
['do.md', '/msd-spike-wrap-up', '/msd:spike --wrap-up'],
|
||
['do.md', '/msd-sketch-wrap-up', '/msd:sketch --wrap-up'],
|
||
['do.md', '/msd-add-phase', '/msd:phase'],
|
||
['do.md', '/msd-add-todo', '/msd:capture'],
|
||
|
||
// settings.md
|
||
['settings.md', '/msd-code-review-fix', '/msd:code-review --fix'],
|
||
['settings.md', '/msd-settings-integrations', '/msd:config --integrations'],
|
||
['settings.md', '/msd-set-profile', '/msd:config --profile'],
|
||
['settings.md', '/msd-settings-advanced', '/msd:config --advanced'],
|
||
|
||
// discuss-phase.md
|
||
['discuss-phase.md', '/msd-spike-wrap-up', '/msd:spike --wrap-up'],
|
||
['discuss-phase.md', '/msd-sketch-wrap-up', '/msd:sketch --wrap-up'],
|
||
|
||
// new-project.md
|
||
['new-project.md', '/msd-spike-wrap-up', '/msd:spike --wrap-up'],
|
||
['new-project.md', '/msd-sketch-wrap-up', '/msd:sketch --wrap-up'],
|
||
|
||
// plan-phase.md
|
||
['plan-phase.md', '/msd-insert-phase', '/msd:phase --insert'],
|
||
|
||
// spike.md
|
||
['spike.md', '/msd-spike-wrap-up', '/msd:spike --wrap-up'],
|
||
|
||
// sketch.md
|
||
['sketch.md', '/msd-sketch-wrap-up', '/msd:sketch --wrap-up'],
|
||
];
|
||
|
||
describe('bug #2950: stale deleted-command references removed from workflow files', () => {
|
||
// Build a map of file → content to avoid re-reading
|
||
const files = [...new Set(FILE_ASSERTIONS.map(([f]) => f))];
|
||
const contentMap = {};
|
||
for (const f of files) {
|
||
contentMap[f] = read(f);
|
||
}
|
||
|
||
// For each (file, deletedCmd) pair, assert the old name is absent
|
||
for (const [file, deletedCmd] of FILE_ASSERTIONS) {
|
||
test(`${file}: does not contain deleted command "${deletedCmd}"`, () => {
|
||
const content = contentMap[file];
|
||
assert.ok(
|
||
!content.includes(deletedCmd),
|
||
`${file} still contains deleted command "${deletedCmd}" — update to new form`
|
||
);
|
||
});
|
||
}
|
||
|
||
// For each (file, deletedCmd, newForm) triple, assert the new form is present
|
||
for (const [file, , newForm] of FILE_ASSERTIONS) {
|
||
test(`${file}: contains new form "${newForm}"`, () => {
|
||
const content = contentMap[file];
|
||
assert.ok(
|
||
content.includes(newForm),
|
||
`${file} is missing expected new form "${newForm}"`
|
||
);
|
||
});
|
||
}
|
||
|
||
// Blanket check: no affected workflow file contains any of the deleted command names
|
||
// (catches any we might have missed in per-file assertions above)
|
||
const affectedFiles = [
|
||
'help.md',
|
||
'help/modes/full.md',
|
||
'help/modes/default.md',
|
||
'help/modes/brief.md',
|
||
'help/modes/topic.md',
|
||
'do.md',
|
||
'settings.md',
|
||
'discuss-phase.md',
|
||
'new-project.md',
|
||
'plan-phase.md',
|
||
'spike.md',
|
||
'sketch.md',
|
||
];
|
||
|
||
for (const file of affectedFiles) {
|
||
const content = read(file);
|
||
for (const deleted of DELETED_COMMANDS) {
|
||
test(`${file}: blanket check — "${deleted}" not present`, () => {
|
||
assert.ok(
|
||
!content.includes(deleted),
|
||
`${file} contains deleted command "${deleted}"`
|
||
);
|
||
});
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
// Folded from tests/feat-2840-issue-driven-orchestration-guide.test.cjs — consolidation epic #1969 (B8 #1977)
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
{
|
||
const { describe: __foldDescribe } = require('node:test');
|
||
__foldDescribe("folded:feat-2840-issue-driven-orchestration-guide (consolidation epic #1969 B8 #1977)", () => {
|
||
/**
|
||
* Tests for docs/issue-driven-orchestration.md (#2840).
|
||
*
|
||
* Structural-IR assertions per CONTRIBUTING.md "Prohibited: Raw Text Matching
|
||
* on Test Outputs": parse the guide into a typed record and assert on
|
||
* semantic flags, not regex on prose. The guide is rebuildable as long as
|
||
* the structural invariants survive — section-level rewording is fine.
|
||
*
|
||
* Acceptance criteria from issue #2840:
|
||
* - One guide explaining issue-driven orchestration using existing MSD
|
||
* commands.
|
||
* - Concrete end-to-end issue → workspace → plan/execute → verify/review
|
||
* → PR flow.
|
||
* - Explicitly documents safety boundaries: isolated worktrees, explicit
|
||
* human review, no automatic public posting by default.
|
||
* - Adds no runtime dependencies / no new command, daemon, or tracker
|
||
* integration. (Test-enforced via concept-mapping audit.)
|
||
*/
|
||
|
||
// allow-test-rule: source-text-is-the-product (see #2840)
|
||
// docs/issue-driven-orchestration.md's deployed prose IS the product being
|
||
// validated for required-concept coverage. The .includes() calls below build
|
||
// a typed record (commandsPresent flags, conceptPairs flags, nonGoalFlags,
|
||
// safetyFlags); assertions run on those booleans, not on raw text.
|
||
|
||
'use strict';
|
||
|
||
const { test, describe } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
|
||
const GUIDE_PATH = path.join(__dirname, '..', 'docs', 'issue-driven-orchestration.md');
|
||
|
||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Extract a section starting at a given heading. Returns the body up to (but
|
||
* not including) the next heading at the same or shallower depth, or null if
|
||
* the heading isn't found.
|
||
*/
|
||
function extractSection(content, heading) {
|
||
const lines = content.split('\n');
|
||
const headingRe = new RegExp(`^(#+)\\s+${heading.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\s*$`);
|
||
let start = -1;
|
||
let depth = 0;
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const m = lines[i].match(headingRe);
|
||
if (m) {
|
||
start = i + 1;
|
||
depth = m[1].length;
|
||
break;
|
||
}
|
||
}
|
||
if (start < 0) return null;
|
||
let end = lines.length;
|
||
for (let i = start; i < lines.length; i++) {
|
||
const m = lines[i].match(/^(#+)\s+/);
|
||
if (m && m[1].length <= depth) {
|
||
end = i;
|
||
break;
|
||
}
|
||
}
|
||
return lines.slice(start, end).join('\n');
|
||
}
|
||
|
||
/**
|
||
* Parse the guide into a typed record. Returns null when the guide is
|
||
* missing so the file-presence test can name the actual problem instead of
|
||
* cascading TypeErrors.
|
||
*/
|
||
function parseGuide() {
|
||
if (!fs.existsSync(GUIDE_PATH)) return null;
|
||
const content = fs.readFileSync(GUIDE_PATH, 'utf8');
|
||
// Strip inline emphasis but NOT underscores (snake_case identifiers like
|
||
// msd-new-workspace, .planning/, etc. must survive).
|
||
const stripped = content.replace(/\*{1,3}|~{2}/g, '');
|
||
|
||
// Concept-mapping table: rows that pair a Symphony-style concept with a
|
||
// MSD primitive. Test asserts on presence of each required pair, not on
|
||
// exact prose ordering.
|
||
const conceptMappingSection = extractSection(content, 'Concept mapping');
|
||
const endToEndSection = extractSection(content, 'End-to-end flow') ||
|
||
extractSection(content, 'End-to-end issue → PR flow') ||
|
||
extractSection(content, 'End-to-end orchestration loop');
|
||
const safetySection = extractSection(content, 'Safety boundaries') ||
|
||
extractSection(content, 'Safety');
|
||
const nonGoalsSection = extractSection(content, 'Non-goals') ||
|
||
extractSection(content, 'What this guide does not do');
|
||
|
||
// Track which referenced commands appear at least once anywhere in the
|
||
// guide. This prevents drift if /msd-* command names are renamed.
|
||
const requiredCommands = [
|
||
'/msd-workspace --new',
|
||
'/msd-manager',
|
||
'/msd-autonomous',
|
||
'/msd-discuss-phase',
|
||
'/msd-plan-phase',
|
||
'/msd-execute-phase',
|
||
'/msd-verify-work',
|
||
'/msd-review',
|
||
'/msd-ship',
|
||
];
|
||
const commandsPresent = Object.fromEntries(
|
||
requiredCommands.map((c) => [c, content.includes(c)])
|
||
);
|
||
|
||
// Concept-mapping invariants — keys are concept slugs, values are the
|
||
// MSD primitive that must appear in the same paragraph/row of the
|
||
// concept-mapping section.
|
||
const conceptPairs = conceptMappingSection
|
||
? {
|
||
roadmap: /ROADMAP\.md/.test(conceptMappingSection),
|
||
statemd: /STATE\.md/.test(conceptMappingSection),
|
||
contextmd: /CONTEXT\.md/.test(conceptMappingSection),
|
||
planmd: /PLAN\.md/.test(conceptMappingSection),
|
||
workspaceCommand: /\/msd-workspace\s+--new/.test(conceptMappingSection),
|
||
executionCommand:
|
||
/\/msd-manager/.test(conceptMappingSection) ||
|
||
/\/msd-autonomous/.test(conceptMappingSection),
|
||
verifyCommand: /\/msd-verify-work/.test(conceptMappingSection),
|
||
reviewCommand: /\/msd-review/.test(conceptMappingSection),
|
||
shipCommand: /\/msd-ship/.test(conceptMappingSection),
|
||
}
|
||
: null;
|
||
|
||
// Non-goals required by the issue: must explicitly disclaim all four.
|
||
const nonGoalFlags = nonGoalsSection
|
||
? {
|
||
noVendoring: /vendor|copy/i.test(nonGoalsSection),
|
||
noDaemon: /daemon|polling/i.test(nonGoalsSection),
|
||
noTrackerDependency: /tracker.*depend|mandatory.*track/i.test(nonGoalsSection),
|
||
noBypassReview: /bypass|review|verification|human.*decision|human gate/i.test(nonGoalsSection),
|
||
}
|
||
: null;
|
||
|
||
// Safety boundaries — required disclaimers about how the loop stays safe.
|
||
const safetyFlags = safetySection
|
||
? {
|
||
isolatedWorktrees: /worktree|isolated/i.test(safetySection),
|
||
explicitReview: /review|human.*gate|human.*approval/i.test(safetySection),
|
||
noAutoPosting: /not.*automatic|no.*auto|explicit.*confirm|user.*confirm|human.*confirm/i.test(safetySection),
|
||
}
|
||
: null;
|
||
|
||
// End-to-end flow must enumerate at least the seven step sequence the
|
||
// acceptance criteria call out. We assert on numbered list items so the
|
||
// narrative can be reworded freely.
|
||
const numberedSteps = endToEndSection
|
||
? (endToEndSection.match(/^\s*\d+\.\s+/gm) || []).length
|
||
: 0;
|
||
|
||
// Strip markdown emphasis when checking for snake_case-sensitive content
|
||
// in section bodies (per the markdown-aware matching pattern).
|
||
const strippedConceptMapping = conceptMappingSection
|
||
? conceptMappingSection.replace(/\*{1,3}|~{2}/g, '')
|
||
: null;
|
||
|
||
return {
|
||
raw: content,
|
||
stripped,
|
||
conceptMappingSection,
|
||
strippedConceptMapping,
|
||
endToEndSection,
|
||
safetySection,
|
||
nonGoalsSection,
|
||
commandsPresent,
|
||
conceptPairs,
|
||
nonGoalFlags,
|
||
safetyFlags,
|
||
numberedSteps,
|
||
};
|
||
}
|
||
|
||
// ─── Tests ──────────────────────────────────────────────────────────────────
|
||
|
||
describe('issue-driven-orchestration guide (#2840)', () => {
|
||
test('docs/issue-driven-orchestration.md exists', () => {
|
||
assert.ok(
|
||
fs.existsSync(GUIDE_PATH),
|
||
`Guide must live at docs/issue-driven-orchestration.md per #2840`
|
||
);
|
||
});
|
||
|
||
test('every required MSD command is referenced at least once', () => {
|
||
const ir = parseGuide();
|
||
assert.ok(ir, 'parseGuide returned null — guide is missing');
|
||
for (const [cmd, present] of Object.entries(ir.commandsPresent)) {
|
||
assert.ok(present, `guide must reference ${cmd}`);
|
||
}
|
||
});
|
||
|
||
test('concept mapping section exists and pairs Symphony concepts with MSD primitives', () => {
|
||
const ir = parseGuide();
|
||
assert.ok(ir, 'guide must be present');
|
||
assert.ok(
|
||
ir.conceptMappingSection,
|
||
'guide must contain a "Concept mapping" section'
|
||
);
|
||
const expected = {
|
||
roadmap: 'ROADMAP.md must appear in the concept mapping',
|
||
statemd: 'STATE.md must appear in the concept mapping',
|
||
contextmd: 'CONTEXT.md must appear in the concept mapping',
|
||
planmd: 'PLAN.md must appear in the concept mapping',
|
||
workspaceCommand: '/msd-workspace --new must appear in the concept mapping',
|
||
executionCommand:
|
||
'/msd-manager or /msd-autonomous must appear in the concept mapping',
|
||
verifyCommand: '/msd-verify-work must appear in the concept mapping',
|
||
reviewCommand: '/msd-review must appear in the concept mapping',
|
||
shipCommand: '/msd-ship must appear in the concept mapping',
|
||
};
|
||
for (const [flag, msg] of Object.entries(expected)) {
|
||
assert.equal(ir.conceptPairs[flag], true, msg);
|
||
}
|
||
});
|
||
|
||
test('safety boundaries section names isolation, review, and non-auto-posting', () => {
|
||
const ir = parseGuide();
|
||
assert.ok(ir, 'guide must be present');
|
||
assert.ok(
|
||
ir.safetySection,
|
||
'guide must contain a "Safety boundaries" or "Safety" section'
|
||
);
|
||
assert.equal(
|
||
ir.safetyFlags.isolatedWorktrees,
|
||
true,
|
||
'safety section must mention isolated worktrees'
|
||
);
|
||
assert.equal(
|
||
ir.safetyFlags.explicitReview,
|
||
true,
|
||
'safety section must require explicit human review'
|
||
);
|
||
assert.equal(
|
||
ir.safetyFlags.noAutoPosting,
|
||
true,
|
||
'safety section must disclaim automatic public posting'
|
||
);
|
||
});
|
||
|
||
test('non-goals section disclaims vendoring, daemon, tracker dependency, and gate-bypass', () => {
|
||
const ir = parseGuide();
|
||
assert.ok(ir, 'guide must be present');
|
||
assert.ok(
|
||
ir.nonGoalsSection,
|
||
'guide must contain a "Non-goals" section'
|
||
);
|
||
const expected = {
|
||
noVendoring: 'must disclaim copying/vendoring Symphony',
|
||
noDaemon: 'must disclaim a long-running daemon',
|
||
noTrackerDependency: 'must disclaim mandatory tracker dependency',
|
||
noBypassReview: 'must disclaim bypassing review/verification gates',
|
||
};
|
||
for (const [flag, msg] of Object.entries(expected)) {
|
||
assert.equal(ir.nonGoalFlags[flag], true, msg);
|
||
}
|
||
});
|
||
|
||
test('end-to-end flow enumerates at least 7 numbered steps (per acceptance criteria)', () => {
|
||
const ir = parseGuide();
|
||
assert.ok(ir, 'guide must be present');
|
||
assert.ok(
|
||
ir.endToEndSection,
|
||
'guide must contain an "End-to-end flow" (or equivalent) section'
|
||
);
|
||
assert.ok(
|
||
ir.numberedSteps >= 7,
|
||
`end-to-end section must enumerate ≥7 numbered steps; found ${ir.numberedSteps}`
|
||
);
|
||
});
|
||
|
||
test('every fenced code block has a language tag (markdownlint MD040)', () => {
|
||
const ir = parseGuide();
|
||
assert.ok(ir, 'guide must be present');
|
||
// Pair fence opens; flag any opener with no language tag.
|
||
const fences = ir.raw.match(/^```.*$/gm) || [];
|
||
const openers = [];
|
||
for (let i = 0; i < fences.length; i++) {
|
||
// Even index = opener, odd = closer. An opener with empty trailing
|
||
// text is MD040.
|
||
if (i % 2 === 0) openers.push(fences[i]);
|
||
}
|
||
const bare = openers.filter((f) => /^```\s*$/.test(f));
|
||
assert.equal(
|
||
bare.length,
|
||
0,
|
||
`MD040: ${bare.length} fenced block(s) lack a language tag`
|
||
);
|
||
});
|
||
|
||
test('cross-linked from docs/README.md', () => {
|
||
const readme = path.join(__dirname, '..', 'docs', 'README.md');
|
||
if (!fs.existsSync(readme)) {
|
||
// docs/README.md is the discovery surface. Without a cross-link, the
|
||
// guide is invisible to users browsing docs/.
|
||
return; // tolerate absence; test below ensures FEATURES.md anchor.
|
||
}
|
||
const txt = fs.readFileSync(readme, 'utf8');
|
||
assert.ok(
|
||
/issue-driven-orchestration/.test(txt),
|
||
'docs/README.md must link to the new guide'
|
||
);
|
||
});
|
||
|
||
test('cross-linked from docs/USER-GUIDE.md', () => {
|
||
const guide = path.join(__dirname, '..', 'docs', 'USER-GUIDE.md');
|
||
// Mirror the null-guard pattern from the README test above: a missing
|
||
// file must produce a meaningful assertion message, not a cryptic
|
||
// ENOENT stack trace. (CR #3036.)
|
||
assert.ok(
|
||
fs.existsSync(guide),
|
||
'docs/USER-GUIDE.md must exist for cross-link validation'
|
||
);
|
||
const txt = fs.readFileSync(guide, 'utf8');
|
||
assert.ok(
|
||
/issue-driven-orchestration/.test(txt),
|
||
'docs/USER-GUIDE.md must link to the new guide'
|
||
);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
// Folded from tests/feat-3025-mcp-token-budget-docs.test.cjs — consolidation epic #1969 (B8 #1977)
|
||
// ────────────────────────────────────────────────────────────────────────
|
||
{
|
||
const { describe: __foldDescribe } = require('node:test');
|
||
__foldDescribe("folded:feat-3025-mcp-token-budget-docs (consolidation epic #1969 B8 #1977)", () => {
|
||
/**
|
||
* Documentation regression test for issue #3025 — MCP token-budget guidance.
|
||
*
|
||
* Verifies that msd-core/references/context-budget.md contains the
|
||
* structural elements the issue requires:
|
||
*
|
||
* 1. A section explaining MCP/tool schemas as a context-budget concern
|
||
* 2. References to the harness-side toggles (enabledMcpjsonServers,
|
||
* disabledMcpjsonServers in .claude/settings.json)
|
||
* 3. A pre-phase audit checklist (browser/playwright, platform-specific,
|
||
* project-specific)
|
||
* 4. An explicit note that MSD does NOT manage MCP enablement — this is
|
||
* a Claude Code harness concern (with a cross-link)
|
||
* 5. Note the interaction with model_profile (compounding levers)
|
||
*
|
||
* Tests parse the doc into a typed section record (parseMcpSection) and
|
||
* assert on flag booleans, not raw text matches. Adheres to
|
||
* CONTRIBUTING.md "no-source-grep" — describes invariants, not wording,
|
||
* so the prose can be reworded freely as long as the semantics survive.
|
||
*
|
||
* Companion to docs/USER-GUIDE.md task section, which is exercised by the
|
||
* same parser shape (separate test below).
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
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 CONTEXT_BUDGET_MD = path.join(ROOT, 'msd-core', 'references', 'context-budget.md');
|
||
const USER_GUIDE_MD = path.join(ROOT, 'docs', 'USER-GUIDE.md');
|
||
|
||
/**
|
||
* Extract the MCP-budget section from a markdown file by header text.
|
||
* Returns null if the section is missing. Section runs from the matching
|
||
* `## ` header up to the next `## ` header (or EOF).
|
||
*/
|
||
function extractSection(filePath, headerSubstring) {
|
||
const content = fs.readFileSync(filePath, 'utf8');
|
||
const lines = content.split(/\r?\n/);
|
||
let inSection = false;
|
||
let startDepth = 0;
|
||
const collected = [];
|
||
for (const line of lines) {
|
||
const headerMatch = /^(#+)\s/.exec(line);
|
||
if (headerMatch) {
|
||
const depth = headerMatch[1].length;
|
||
if (inSection) {
|
||
// Section ends at a header at the same or shallower depth.
|
||
// Subsections at deeper depth are part of the section.
|
||
if (depth <= startDepth) break;
|
||
} else if (line.toLowerCase().includes(headerSubstring.toLowerCase())) {
|
||
inSection = true;
|
||
startDepth = depth;
|
||
}
|
||
}
|
||
if (inSection) collected.push(line);
|
||
}
|
||
return collected.length > 0 ? collected.join('\n') : null;
|
||
}
|
||
|
||
/**
|
||
* Parse the MCP-budget section into a typed semantic-flag record.
|
||
* Each flag answers a single behavioral question that #3025 requires
|
||
* the prose to encode.
|
||
*/
|
||
function parseMcpBudgetSection(section) {
|
||
if (!section || typeof section !== 'string') {
|
||
return {
|
||
ok: false,
|
||
sectionLength: 0,
|
||
explainsMcpAsBudgetConcern: false,
|
||
namesEnabledMcpjsonServers: false,
|
||
namesDisabledMcpjsonServers: false,
|
||
namesClaudeSettingsJson: false,
|
||
includesPrePhaseAudit: false,
|
||
auditMentionsBrowserOrPlaywright: false,
|
||
auditMentionsPlatformSpecific: false,
|
||
auditMentionsCrossProject: false,
|
||
explainsHarnessNotMsd: false,
|
||
mentionsModelProfileInteraction: false,
|
||
crossLinksContextBudget: false,
|
||
};
|
||
}
|
||
// CR follow-up: strip inline markdown emphasis (`**`, `*`, `~~`) and
|
||
// backticks before phrase-matching so e.g. "MSD does **not** manage"
|
||
// is caught by the primary `msd does not manage` alternative below.
|
||
// WITHOUT this, the markdown-bold breaks the contiguous match and the
|
||
// test only passes via the fallback branch (silent dead code).
|
||
// Underscores are intentionally NOT stripped — `model_profile` and
|
||
// other snake_case identifiers must survive intact so the
|
||
// model_profile interaction check still finds them.
|
||
const stripped = section.replace(/\*{1,3}|~{2}|`/g, '');
|
||
// (1) Explains MCP as budget concern — must mention BOTH "MCP" / "tool
|
||
// schema" AND a token/cost framing.
|
||
const explainsMcpAsBudgetConcern =
|
||
/\bmcp\b|tool schema|tool schemas/i.test(stripped) &&
|
||
/\btoken|context budget|per[- ]turn|cost\b/i.test(stripped);
|
||
// (2) Names the harness keys verbatim
|
||
const namesEnabledMcpjsonServers = /enabledMcpjsonServers/.test(stripped);
|
||
const namesDisabledMcpjsonServers = /disabledMcpjsonServers/.test(stripped);
|
||
// (3) Names the settings file location
|
||
const namesClaudeSettingsJson = /\.claude\/settings\.json/.test(stripped);
|
||
// (4) Audit checklist — must mention all three classes the issue
|
||
// calls out, plus a "before this phase / pre-phase" framing
|
||
const includesPrePhaseAudit =
|
||
/audit|checklist|review (your )?mcp|before (starting|beginning) (a |the )?phase/i.test(stripped);
|
||
const auditMentionsBrowserOrPlaywright = /\bbrowser\b|playwright/i.test(stripped);
|
||
const auditMentionsPlatformSpecific = /platform[- ]specific|mac[- ]?tools|windows[- ]?tools|os[- ]specific/i.test(stripped);
|
||
const auditMentionsCrossProject = /(other|different|cross[- ])\s*project|stale (project )?mcp/i.test(stripped);
|
||
// (5) Harness vs MSD distinction — must explicitly state MSD doesn't
|
||
// own this knob and point at the harness
|
||
const explainsHarnessNotMsd =
|
||
/(msd does(?:n[''’]t| not) (own|manage|control)|harness (concern|setting|controlled)|not a msd (setting|knob))/i.test(stripped);
|
||
// (6) Compounding with model_profile
|
||
const mentionsModelProfileInteraction =
|
||
/model[_ ]profile/i.test(stripped) &&
|
||
/compound|multiplier|stack|every[- ]turn|regardless of (which )?model|in addition/i.test(stripped);
|
||
// (7) Cross-link to the canonical reference doc — task-guide section
|
||
// must point readers at context-budget.md for the full audit. Encoded
|
||
// as a named flag (CR follow-up) so the assertion sits alongside the
|
||
// other parsed invariants rather than as a one-off inline regex.
|
||
const crossLinksContextBudget = /context-budget/i.test(stripped);
|
||
return {
|
||
ok: true,
|
||
sectionLength: section.length,
|
||
explainsMcpAsBudgetConcern,
|
||
namesEnabledMcpjsonServers,
|
||
namesDisabledMcpjsonServers,
|
||
namesClaudeSettingsJson,
|
||
includesPrePhaseAudit,
|
||
auditMentionsBrowserOrPlaywright,
|
||
auditMentionsPlatformSpecific,
|
||
auditMentionsCrossProject,
|
||
explainsHarnessNotMsd,
|
||
mentionsModelProfileInteraction,
|
||
crossLinksContextBudget,
|
||
};
|
||
}
|
||
|
||
// ─── context-budget.md ──────────────────────────────────────────────────────
|
||
|
||
describe('#3025 context-budget.md: MCP token-budget section exists with required content', () => {
|
||
test('the file exists', () => {
|
||
assert.ok(fs.existsSync(CONTEXT_BUDGET_MD), `expected file at ${CONTEXT_BUDGET_MD}`);
|
||
});
|
||
|
||
test('has a section header that mentions MCP', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
assert.ok(section, 'must have a `## ...MCP...` heading; section was not found');
|
||
});
|
||
|
||
test('explains MCP/tool schemas as a context-budget concern (#3025 requirement 1)', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
const parsed = parseMcpBudgetSection(section);
|
||
assert.equal(parsed.explainsMcpAsBudgetConcern, true,
|
||
`must explain MCP/tool schemas as a token/context-budget concern; section was:\n${section}`);
|
||
});
|
||
|
||
test('names enabledMcpjsonServers and disabledMcpjsonServers (#3025 requirement 2)', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
const parsed = parseMcpBudgetSection(section);
|
||
assert.equal(parsed.namesEnabledMcpjsonServers, true,
|
||
'section must reference `enabledMcpjsonServers` so users know the exact key');
|
||
assert.equal(parsed.namesDisabledMcpjsonServers, true,
|
||
'section must reference `disabledMcpjsonServers` for parity');
|
||
assert.equal(parsed.namesClaudeSettingsJson, true,
|
||
'section must name `.claude/settings.json` as the location of the toggle');
|
||
});
|
||
|
||
test('includes a pre-phase audit checklist with all three classes (#3025 requirement 3)', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
const parsed = parseMcpBudgetSection(section);
|
||
assert.equal(parsed.includesPrePhaseAudit, true,
|
||
'section must include audit/checklist framing for pre-phase MCP review');
|
||
assert.equal(parsed.auditMentionsBrowserOrPlaywright, true,
|
||
'audit must mention browser/playwright tools as a candidate for disabling');
|
||
assert.equal(parsed.auditMentionsPlatformSpecific, true,
|
||
'audit must mention platform-specific tools (Mac/Windows/OS-specific)');
|
||
assert.equal(parsed.auditMentionsCrossProject, true,
|
||
'audit must mention stale/cross-project MCPs from other projects');
|
||
});
|
||
|
||
test('explains MSD does not own MCP enablement — harness concern (#3025 requirement 4)', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
const parsed = parseMcpBudgetSection(section);
|
||
assert.equal(parsed.explainsHarnessNotMsd, true,
|
||
'section must explicitly state MSD does not manage MCP enablement (harness concern)');
|
||
});
|
||
|
||
test('notes interaction with model_profile (compounding levers) (#3025 requirement 5)', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
const parsed = parseMcpBudgetSection(section);
|
||
assert.equal(parsed.mentionsModelProfileInteraction, true,
|
||
'section must note that trimming MCPs compounds with model_profile choice');
|
||
});
|
||
|
||
test('full semantic record matches the #3025 contract — typed snapshot', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
const parsed = parseMcpBudgetSection(section);
|
||
const contract = {
|
||
ok: parsed.ok,
|
||
explainsMcpAsBudgetConcern: parsed.explainsMcpAsBudgetConcern,
|
||
namesEnabledMcpjsonServers: parsed.namesEnabledMcpjsonServers,
|
||
namesDisabledMcpjsonServers: parsed.namesDisabledMcpjsonServers,
|
||
namesClaudeSettingsJson: parsed.namesClaudeSettingsJson,
|
||
includesPrePhaseAudit: parsed.includesPrePhaseAudit,
|
||
auditMentionsBrowserOrPlaywright: parsed.auditMentionsBrowserOrPlaywright,
|
||
auditMentionsPlatformSpecific: parsed.auditMentionsPlatformSpecific,
|
||
auditMentionsCrossProject: parsed.auditMentionsCrossProject,
|
||
explainsHarnessNotMsd: parsed.explainsHarnessNotMsd,
|
||
mentionsModelProfileInteraction: parsed.mentionsModelProfileInteraction,
|
||
};
|
||
assert.deepStrictEqual(contract, {
|
||
ok: true,
|
||
explainsMcpAsBudgetConcern: true,
|
||
namesEnabledMcpjsonServers: true,
|
||
namesDisabledMcpjsonServers: true,
|
||
namesClaudeSettingsJson: true,
|
||
includesPrePhaseAudit: true,
|
||
auditMentionsBrowserOrPlaywright: true,
|
||
auditMentionsPlatformSpecific: true,
|
||
auditMentionsCrossProject: true,
|
||
explainsHarnessNotMsd: true,
|
||
mentionsModelProfileInteraction: true,
|
||
}, 'context-budget.md MCP section contract violated');
|
||
});
|
||
});
|
||
|
||
// ─── docs/USER-GUIDE.md task section ────────────────────────────────────────
|
||
|
||
describe('#3025 docs/USER-GUIDE.md: companion task section exists', () => {
|
||
test('USER-GUIDE.md has an MCP-trimming task section', () => {
|
||
const section = extractSection(USER_GUIDE_MD, 'mcp');
|
||
assert.ok(section,
|
||
'USER-GUIDE.md must have a `### ...MCP...` task section so users find it via the guide');
|
||
});
|
||
|
||
test('USER-GUIDE.md task section names the harness key and cross-links the reference', () => {
|
||
const section = extractSection(USER_GUIDE_MD, 'mcp');
|
||
const parsed = parseMcpBudgetSection(section);
|
||
assert.equal(parsed.namesEnabledMcpjsonServers, true,
|
||
'task section must mention the harness key by name');
|
||
// Cross-link to the reference doc — assert on the parsed flag so
|
||
// the invariant lives alongside the other named flags (CR follow-up
|
||
// on the no-source-grep standard).
|
||
assert.equal(parsed.crossLinksContextBudget, true,
|
||
'task section must cross-link to context-budget.md');
|
||
});
|
||
});
|
||
|
||
// ─── markdownlint pre-flight (per bundle-docs-with-code skill) ──────────────
|
||
|
||
describe('#3025 markdownlint pre-flight: MD040 + MD056', () => {
|
||
test('every fenced code block in the new MCP section has a language tag (MD040)', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
// Guard: extractSection returns null when the section is missing.
|
||
// Without this, `section.match(...)` would throw a TypeError instead
|
||
// of producing a meaningful assertion failure (CR follow-up).
|
||
assert.ok(section, 'MCP section not found in context-budget.md — cannot check MD040');
|
||
const fences = (section.match(/^```([a-zA-Z0-9_+-]*)?\s*$/gm) || []);
|
||
// Pairs of fences open/close; odd-indexed ones close blocks. Every
|
||
// OPENING fence must have a language tag. Closing fences are bare ```.
|
||
// Walk pairs: even index = opener, odd = closer.
|
||
const openers = fences.filter((_, i) => i % 2 === 0);
|
||
const missing = openers.filter((line) => /^```\s*$/.test(line));
|
||
assert.deepStrictEqual(missing, [],
|
||
`every fenced code block opener must have a language tag (MD040). Missing: ${JSON.stringify(missing)}`);
|
||
});
|
||
|
||
test('every markdown table row in the new MCP section has the same column count as its header (MD056)', () => {
|
||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||
// Guard: same null-section concern as MD040 above (CR follow-up).
|
||
assert.ok(section, 'MCP section not found in context-budget.md — cannot check MD056');
|
||
const lines = section.split(/\r?\n/);
|
||
// Walk through and detect tables: header row followed by a separator
|
||
// (--- pattern) followed by data rows. Count `|` per line.
|
||
const issues = [];
|
||
for (let i = 0; i < lines.length - 1; i += 1) {
|
||
const header = lines[i];
|
||
const sep = lines[i + 1];
|
||
if (!/^\s*\|.*\|\s*$/.test(header)) continue;
|
||
if (!/^\s*\|[\s\-:|]+\|\s*$/.test(sep)) continue;
|
||
const headerCols = (header.match(/\|/g) || []).length;
|
||
// Walk data rows
|
||
for (let j = i + 2; j < lines.length; j += 1) {
|
||
const row = lines[j];
|
||
if (!/^\s*\|.*\|\s*$/.test(row)) break;
|
||
const rowCols = (row.match(/\|/g) || []).length;
|
||
if (rowCols !== headerCols) {
|
||
issues.push({ line: j, expected: headerCols, actual: rowCols, row });
|
||
}
|
||
}
|
||
}
|
||
assert.deepStrictEqual(issues, [],
|
||
`table rows must match header column count (MD056). Issues: ${JSON.stringify(issues, null, 2)}`);
|
||
});
|
||
});
|
||
});
|
||
}
|