test(#1977): consolidate 22 misc + repo-invariant regression tests
Final epic-#1969 batch. Fold 22 issue-named files: the 4 genuine repo-wide invariant scans (551-eslint-bin-lib-coverage, bug-3054 stale /gsd-next, bug-3810 no-gsd-sdk-runtime-refs, feat-3593 cli-negative-universal) into a NEW shared repo-invariants.test.cjs; the other 18 as singletons into their nearest module suite (model-resolver, codex-config, runtime-converters, security, state-transition, worktree-safety, roadmap-parser, etc.). Verbatim block-scoped describe wrappers; 334 subtests conserved 1:1. Host-env pre-check (B2+B6): the 6 CLI folds into GSD_TEST_MODE-setting hosts (model-resolver/ codex-config/runtime-converters) are benign — each origin independently sets GSD_TEST_MODE=1 itself (idempotent), unlike the B6 real-install case. Regenerates regression-name allowlist (222->213), ratchets file-count allowlist (state 17->16), makes 7 relocated allow-test-rule exemptions issue-ref-compliant (ADR-456; prunes stale ids). Repoints 2 tests/ refs in docs/TESTING-SUITES.md. lint:ci green. Part of epic #1969. Closes #1977. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -888,3 +888,643 @@ describe('bug #2950: stale deleted-command references removed from workflow file
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// 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 GSD
|
||||
* 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: structural-IR parser for a docs guide. The .includes() (see #2840)
|
||||
// calls below build a typed record (commandsPresent flags, conceptPairs
|
||||
// flags, nonGoalFlags, safetyFlags); assertions run on those booleans, not
|
||||
// on raw text. This is the documented escape hatch in
|
||||
// scripts/lint-no-source-grep.cjs for doc-shape tests.
|
||||
|
||||
'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
|
||||
// gsd-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
|
||||
// GSD 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 /gsd-* command names are renamed.
|
||||
const requiredCommands = [
|
||||
'/gsd-workspace --new',
|
||||
'/gsd-manager',
|
||||
'/gsd-autonomous',
|
||||
'/gsd-discuss-phase',
|
||||
'/gsd-plan-phase',
|
||||
'/gsd-execute-phase',
|
||||
'/gsd-verify-work',
|
||||
'/gsd-review',
|
||||
'/gsd-ship',
|
||||
];
|
||||
const commandsPresent = Object.fromEntries(
|
||||
requiredCommands.map((c) => [c, content.includes(c)])
|
||||
);
|
||||
|
||||
// Concept-mapping invariants — keys are concept slugs, values are the
|
||||
// GSD 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: /\/gsd-workspace\s+--new/.test(conceptMappingSection),
|
||||
executionCommand:
|
||||
/\/gsd-manager/.test(conceptMappingSection) ||
|
||||
/\/gsd-autonomous/.test(conceptMappingSection),
|
||||
verifyCommand: /\/gsd-verify-work/.test(conceptMappingSection),
|
||||
reviewCommand: /\/gsd-review/.test(conceptMappingSection),
|
||||
shipCommand: /\/gsd-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 GSD 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 GSD 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: '/gsd-workspace --new must appear in the concept mapping',
|
||||
executionCommand:
|
||||
'/gsd-manager or /gsd-autonomous must appear in the concept mapping',
|
||||
verifyCommand: '/gsd-verify-work must appear in the concept mapping',
|
||||
reviewCommand: '/gsd-review must appear in the concept mapping',
|
||||
shipCommand: '/gsd-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 gsd-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 GSD 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, 'gsd-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,
|
||||
explainsHarnessNotGsd: false,
|
||||
mentionsModelProfileInteraction: false,
|
||||
crossLinksContextBudget: false,
|
||||
};
|
||||
}
|
||||
// CR follow-up: strip inline markdown emphasis (`**`, `*`, `~~`) and
|
||||
// backticks before phrase-matching so e.g. "GSD does **not** manage"
|
||||
// is caught by the primary `gsd 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 GSD distinction — must explicitly state GSD doesn't
|
||||
// own this knob and point at the harness
|
||||
const explainsHarnessNotGsd =
|
||||
/(gsd does(?:n[''’]t| not) (own|manage|control)|harness (concern|setting|controlled)|not a gsd (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,
|
||||
explainsHarnessNotGsd,
|
||||
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 GSD does not own MCP enablement — harness concern (#3025 requirement 4)', () => {
|
||||
const section = extractSection(CONTEXT_BUDGET_MD, 'mcp');
|
||||
const parsed = parseMcpBudgetSection(section);
|
||||
assert.equal(parsed.explainsHarnessNotGsd, true,
|
||||
'section must explicitly state GSD 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,
|
||||
explainsHarnessNotGsd: parsed.explainsHarnessNotGsd,
|
||||
mentionsModelProfileInteraction: parsed.mentionsModelProfileInteraction,
|
||||
};
|
||||
assert.deepStrictEqual(contract, {
|
||||
ok: true,
|
||||
explainsMcpAsBudgetConcern: true,
|
||||
namesEnabledMcpjsonServers: true,
|
||||
namesDisabledMcpjsonServers: true,
|
||||
namesClaudeSettingsJson: true,
|
||||
includesPrePhaseAudit: true,
|
||||
auditMentionsBrowserOrPlaywright: true,
|
||||
auditMentionsPlatformSpecific: true,
|
||||
auditMentionsCrossProject: true,
|
||||
explainsHarnessNotGsd: 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)}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user