* test: reproduce Windows SDK not found after fresh npx install (#3211) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: red — docs-parity live-registry tests fail against stub helper (#3049) Adds: - tests/helpers/live-command-registry.cjs (stub: returns empty Set) - tests/docs-parity-live-registry.test.cjs (new polarity-inverted test) - tests/fixtures/live-command-registry/ (fixture .md files) All parity and helper-contract tests fail because the stub returns an empty registry. This is the intentional RED state before GREEN implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(test-helpers): live-command-registry derives canonical tokens from commands/gsd/*.md (#3049) Implements GREEN phase: - tests/helpers/live-command-registry.cjs: walks commands/gsd/*.md, parses YAML frontmatter name: field, emits /gsd-slug, /gsd:slug, $gsd-slug per command. Memoized per process. Fails loud on malformed frontmatter (k302). - tests/docs-parity-live-registry.test.cjs: updated with INTERNAL_COMPONENT_SLUGS exemption for path-component and placeholder tokens (gsd-build from GitHub org URLs, gsd-workspaces from ~/gsd-workspaces/ paths, gsd-tools from bin/gsd-tools.cjs paths, etc.) Docs drift caught and fixed: - ns-* rename: /gsd-ns-workflow→/gsd-workflow etc. in COMMANDS, FEATURES, INVENTORY, USER-GUIDE (6 commands across 4 English files) - /gsd-scan → /gsd-map-codebase --fast (FEATURES, INVENTORY, USER-GUIDE) - /gsd-note → /gsd-capture (FEATURES, issue-driven-orchestration, ja-JP, ko-KR) - /gsd-do → /gsd-fast (FEATURES, ja-JP, ko-KR) - /gsd-from-gsd2 → /gsd-import --from-gsd2 (CLI-TOOLS, FEATURES, INVENTORY) - /gsd-verify-phase → /gsd-validate-phase (STATE-MD-LIFECYCLE) - /gsd-settings-integrations → /gsd-settings or /gsd-config --integrations (CLI-TOOLS) - /gsd-dev-preferences removed from profile-user artifact lists (AGENTS, COMMANDS, FEATURES in English, ja-JP, ko-KR) - /gsd-select-framework removed from gsd-framework-selector spawner list (AGENTS, INVENTORY) All 28 new tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(test): replace deny-list parity tests with polarity-inverted live-registry approach (#3049) - Delete bug-3010-reapply-patches-references.test.cjs (hardcoded deny-list) - Delete bug-3029-3034-stale-command-routes.test.cjs (hardcoded deny-list) - Delete bug-3042-3044-research-flag-and-stale-refs.test.cjs (deny-list + frontmatter checks) - Add tests/skill-frontmatter-contract.test.cjs (frontmatter structural checks extracted from deleted file) - Update tests/commands-doc-parity.test.cjs to derive slug from name: frontmatter field instead of filename, so ns-* commands resolve to their actual deployed tokens Closes #3049 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: annotate commands-doc-parity with source-text-is-the-product exemption (#3049 lint fix) The readFileSync on commands/gsd/*.md reads product markdown whose deployed text IS what the user sees — content.startsWith('---') detects YAML frontmatter in those files, not source-code structure. Add the allow-test-rule exemption matching the same rationale used in docs-parity-live-registry.test.cjs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: walk docs/** recursively to cover nested locale trees (CR finding 7) Replaced the non-recursive listMdFiles() with a hand-rolled DFS walker compatible with Node 20+. Surfaces unreadable-directory errors as stderr warnings (PRED.k302) rather than silently skipping. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: annotate live-command-registry helper and commands-doc-parity with source-text exemptions (CR findings 6, 8) Adds allow-test-rule comments to suppress lint-no-source-grep false positives on YAML frontmatter structure checks in both files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: anchor --research-phase assertions to arg-parsing section and verify combined refresh (CR findings 9, 10) Finding 9: scopes --research-phase check to within 1200 chars of the flag description section header, preventing false positives from prose mentions. Finding 10: tightens the force-refresh assertion to require BOTH --research and force/refresh semantics within the --research-phase description section, verifying the combined-mode contract rather than standalone --research presence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: fix execSync mock to accept opts parameter, forward to saved implementation (CR finding 5) The mock at line 212 dropped the options parameter when delegating to savedExecSync. Updated to (cmd, opts) signature and pass opts through. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: correct routing entrypoint, --fast default, /gsd-review collision, verifying-stage mapping (CR findings 1-4) Finding 1: Change Freeform Routing command from /gsd-fast to /gsd-progress --do. /gsd-fast is the inline trivial-task executor, not the routing entrypoint. Finding 2: Clarify that /gsd-map-codebase --fast REQ-SCAN-02 default (tech+arch) runs as a single combined-focus agent, resolving the contradiction with REQ-SCAN-01. Finding 3: Rename the namespace router /gsd-review to /gsd-quality across all docs, command file, and help.md to eliminate the naming collision with the concrete cross-AI peer-review command (review.md, name: gsd:review). Finding 4: Replace /gsd-validate-phase with /gsd-verify-work in the STATE-MD-LIFECYCLE.md verifying-stage table. /gsd-validate-phase is the retroactive Nyquist-validation flow, not the normal phase-verification step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs+test: fix locale doc drift surfaced by recursive walker (CR finding 7 follow-up) The recursive listMdFiles() walker newly covered docs/**/*.md subdirs. Stale command references in locale docs are now caught and fixed: - docs/zh-CN/references/model-profiles.md: remove /gsd-set-profile (deleted command); config.json is the current mechanism - docs/zh-CN/references/ui-brand.md: remove /gsd-alternative-1/2 template placeholders - docs/{ja-JP,ko-KR,pt-BR}/superpowers/specs/2026-03-20-*: replace /gsd-new-workspace, /gsd-list-workspaces, /gsd-remove-workspace with /gsd-workspace --new / --list / --remove (consolidated in #2790) Also adds smoke- and alternative-{1,2} to INTERNAL_COMPONENT_SLUGS (filesystem path and template placeholder patterns, not slash commands) and introduces listEnglishMdFiles() to scope the English parity check to docs/ excluding locale subdirectories (which have their own per-locale describe blocks). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add bash language tag to fenced code blocks in ja-JP and ko-KR workspace specs (CR round 2) Satisfies MD040 fenced-code-language requirement. These blocks contain shell commands and were missing the language specifier. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
464 lines
20 KiB
JavaScript
464 lines
20 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/gsd/*.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:
|
|
* - get-shit-done/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, 'get-shit-done', '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 /gsd-* 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. "/gsd-command-name [args]"). It is not a registered command.
|
|
'command-name',
|
|
'command',
|
|
|
|
// gsd-tools.cjs — the legacy Node CLI binary (bin/gsd-tools.cjs).
|
|
// Docs reference it as a path component in shell examples, not as a slash command.
|
|
// Example: node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state validate
|
|
'tools',
|
|
|
|
// Hook scripts — internal runtime hooks, not user-invocable slash commands.
|
|
// hooks/gsd-statusline.js — session statusline hook
|
|
// hooks/gsd-context-monitor.js — context-window monitor hook
|
|
// hooks/gsd-update-banner.js — update-available banner hook
|
|
// These appear in docs as file-path references (e.g. "gsd-statusline.js reads
|
|
// the cache"), not as command invocations.
|
|
'statusline',
|
|
'context-monitor',
|
|
'update-banner',
|
|
|
|
// gsd-update-check.json — background update-check CACHE FILE, not a slash command.
|
|
// ARCHITECTURE.md references "~/.cache/gsd/gsd-update-check.json" as a path;
|
|
// the regex captures "/gsd-update-check" from the path component.
|
|
'update-check',
|
|
|
|
// Internal agent names referenced in ARCHITECTURE.md tables of agents.
|
|
// These are spawned agents (gsd-planner, etc.), not user-typable slash commands.
|
|
'planner',
|
|
|
|
// Malformed token from SDK init reference: "/gsd-init-" appears as a truncated
|
|
// prefix in CLI-TOOLS.md describing the gsd-sdk init command family
|
|
// (e.g., "gsd-sdk query init.phase-op 12"). The regex captures "/gsd-init-"
|
|
// without a following slug — this is a documentation formatting artifact, not
|
|
// a real command token.
|
|
'init-',
|
|
|
|
// gsd-build — GitHub organization name: "github.com/gsd-build/get-shit-done".
|
|
// Every occurrence of "/gsd-build" in docs is the path component of a GitHub URL
|
|
// (e.g., "[#2792](https://github.com/gsd-build/get-shit-done/issues/2792)").
|
|
// The regex captures "/gsd-build" from the URL path. Not a slash command.
|
|
'build',
|
|
|
|
// ~/gsd-workspaces/ — filesystem directory path used by /gsd-workspace.
|
|
// Docs reference "~/gsd-workspaces/<name>" as the default workspace directory
|
|
// in shell examples and option tables (e.g. "--path /target (default: ~/gsd-workspaces/<name>)").
|
|
// The regex captures "/gsd-workspaces" from the path component. The LIVE slash
|
|
// command is "/gsd-workspace" (singular) — not "/gsd-workspaces" (plural).
|
|
'workspaces',
|
|
|
|
// Portuguese translation of "command" — pt-BR/ARCHITECTURE.md uses "/gsd-comando"
|
|
// as the localized equivalent of the "/gsd-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',
|
|
|
|
// Smoke-test directory path — locale docs reference "/tmp/gsd-smoke-$(date +%s)"
|
|
// as a temporary directory path in bash code-block examples. The regex captures
|
|
// "/gsd-smoke-" from the filesystem path. Not a slash command.
|
|
'smoke-',
|
|
|
|
// Template placeholders — zh-CN/references/ui-brand.md used "/gsd-alternative-1"
|
|
// and "/gsd-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',
|
|
]);
|
|
|
|
/**
|
|
* 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 /gsd-old-name...").
|
|
*/
|
|
function stripHtmlComments(content) {
|
|
return content.replace(/<!--[\s\S]*?-->/g, '');
|
|
}
|
|
|
|
/**
|
|
* Extract the set of slash-command tokens from markdown content.
|
|
* Three forms per command per runtime:
|
|
* /gsd-slug — Claude / non-Gemini
|
|
* /gsd:slug — Gemini
|
|
* $gsd-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 /gsd- or /gsd: or $gsd- prefix to get the slug
|
|
const slug = token.replace(/^(?:\/gsd[:-]|\$gsd-)/, '');
|
|
// 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;
|
|
}
|
|
|
|
const allSlash = (stripped.match(/\/gsd-[a-z0-9][a-z0-9-]*/g) || []);
|
|
const allColon = (stripped.match(/\/gsd:[a-z0-9][a-z0-9-]*/g) || []);
|
|
const allDollar = (stripped.match(/\$gsd-[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 for Node 20 compat (Node 22+ recursive readdirSync is
|
|
* not available in all CI matrix entries). 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/gsd/ has registered commands)', () => {
|
|
const result = getLiveCommandTokens();
|
|
assert.ok(result.size > 0, 'live registry must contain at least one token');
|
|
});
|
|
|
|
test('contains /gsd-help (from commands/gsd/help.md name: gsd:help)', () => {
|
|
const result = getLiveCommandTokens();
|
|
assert.ok(result.has('/gsd-help'), 'registry must contain /gsd-help');
|
|
});
|
|
|
|
test('contains /gsd:help (Gemini form)', () => {
|
|
const result = getLiveCommandTokens();
|
|
assert.ok(result.has('/gsd:help'), 'registry must contain /gsd:help');
|
|
});
|
|
|
|
test('contains $gsd-help (Codex form)', () => {
|
|
const result = getLiveCommandTokens();
|
|
assert.ok(result.has('$gsd-help'), 'registry must contain $gsd-help');
|
|
});
|
|
|
|
test('contains /gsd-plan-phase (from commands/gsd/plan-phase.md)', () => {
|
|
const result = getLiveCommandTokens();
|
|
assert.ok(result.has('/gsd-plan-phase'), 'registry must contain /gsd-plan-phase');
|
|
});
|
|
|
|
test('contains exactly 3 tokens per slug (slash, colon, dollar)', () => {
|
|
const result = getLiveCommandTokens();
|
|
// Every /gsd-slug should have a matching /gsd:slug and $gsd-slug
|
|
let tokenCount = 0;
|
|
for (const token of result) {
|
|
if (token.startsWith('/gsd-')) tokenCount++;
|
|
}
|
|
const slashTokens = [...result].filter(t => t.startsWith('/gsd-'));
|
|
for (const slash of slashTokens) {
|
|
const slug = slash.slice('/gsd-'.length);
|
|
assert.ok(
|
|
result.has(`/gsd:${slug}`),
|
|
`registry must contain Gemini form /gsd:${slug} for slash form ${slash}`
|
|
);
|
|
assert.ok(
|
|
result.has(`$gsd-${slug}`),
|
|
`registry must contain Codex form $gsd-${slug} for slash form ${slash}`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('does NOT contain removed /gsd-reapply-patches', () => {
|
|
const result = getLiveCommandTokens();
|
|
assert.ok(!result.has('/gsd-reapply-patches'), 'registry must NOT contain removed /gsd-reapply-patches');
|
|
});
|
|
|
|
test('does NOT contain removed /gsd-code-review-fix', () => {
|
|
const result = getLiveCommandTokens();
|
|
assert.ok(!result.has('/gsd-code-review-fix'), 'registry must NOT contain removed /gsd-code-review-fix');
|
|
});
|
|
|
|
test('does NOT contain removed /gsd-status', () => {
|
|
const result = getLiveCommandTokens();
|
|
assert.ok(!result.has('/gsd-status'), 'registry must NOT contain removed /gsd-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 gsd: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/gsd/help.md (name: gsd:help).
|
|
// Fixture file tests are done inline since the helper reads commands/gsd/ only.
|
|
// The canonical token contract:
|
|
// name: gsd:foo → /gsd-foo, /gsd:foo, $gsd-foo
|
|
const registry = getLiveCommandTokens();
|
|
// We know help.md has name: gsd:help
|
|
const slug = 'help';
|
|
assert.ok(registry.has(`/gsd-${slug}`), `must have /gsd-${slug}`);
|
|
assert.ok(registry.has(`/gsd:${slug}`), `must have /gsd:${slug}`);
|
|
assert.ok(registry.has(`$gsd-${slug}`), `must have $gsd-${slug}`);
|
|
});
|
|
|
|
test('parses gsd-slug frontmatter (ns-* commands) and emits 3 tokens', () => {
|
|
// ns-context.md has name: gsd-context (dash-style, no colon)
|
|
const registry = getLiveCommandTokens();
|
|
assert.ok(registry.has('/gsd-context'), 'must have /gsd-context (from ns-context.md)');
|
|
assert.ok(registry.has('/gsd:context'), 'must have /gsd:context (Gemini form)');
|
|
assert.ok(registry.has('$gsd-context'), 'must have $gsd-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 /gsd-progress were renamed to /gsd-status-new, the old /gsd-progress
|
|
// token would not appear in the live registry, and any doc referencing
|
|
// /gsd-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('/gsd-progress'), '/gsd-progress must be live (not renamed to /gsd-status)');
|
|
assert.ok(!registry.has('/gsd-status'), '/gsd-status must be absent (was deleted, replaced by /gsd-progress)');
|
|
});
|
|
|
|
test('freshly-deleted command /gsd-check-todos is absent from registry', () => {
|
|
const registry = getLiveCommandTokens();
|
|
assert.ok(!registry.has('/gsd-check-todos'), '/gsd-check-todos must not be in the live registry');
|
|
});
|
|
|
|
test('freshly-deleted command /gsd-new-workspace is absent from registry', () => {
|
|
const registry = getLiveCommandTokens();
|
|
assert.ok(!registry.has('/gsd-new-workspace'), '/gsd-new-workspace must not be in the live registry');
|
|
});
|
|
|
|
test('freshly-deleted command /gsd-plan-milestone-gaps is absent from registry', () => {
|
|
const registry = getLiveCommandTokens();
|
|
assert.ok(!registry.has('/gsd-plan-milestone-gaps'), '/gsd-plan-milestone-gaps must not be in the live registry');
|
|
});
|
|
|
|
test('freshly-deleted command /gsd-research-phase is absent from registry', () => {
|
|
const registry = getLiveCommandTokens();
|
|
assert.ok(!registry.has('/gsd-research-phase'), '/gsd-research-phase must not be in the live registry');
|
|
});
|
|
});
|