* feat(#3415): ship local/no-unbounded-quantifier, burn down ReDoS class Phase 4 of epic #3212 (ADR-3212 §5/§7, the final phase). New rule flags an unbounded */+/{n,} quantifier over a broad character class ([\s\S], dotAll ., or a 1-2-unit negated class like [^\n]/[^)\n] — the exact #2128-fixed shape) applied to a regex whose match target is data-flow-traced to readFileSync content. eslint-rules/lib/readfilesync-trace.cjs extracts the data-flow tracer shared with no-crlf-fragile-split (Phase 2) rather than a second copy — no-crlf-fragile-split refactored onto it with zero behavior change, parity-tested. Real triage, not 798 mechanical edits: the ADR's census (2026-08-08) screened every unbounded quantifier in the tree unscoped. Correctly scoped to readFileSync-derived content (matching Phase 2's own G2/G3 scoping), the rule found 162 real hits across two detection waves — the second wave (93) surfaced only after a genuine off-by-one bug in this rule's own first draft was caught while writing its RuleTester tests and fixed (the bug silently missed every directly-quantified [\s\S]* with no gap before the quantifier — exactly the class this rule exists to catch). 3 hits landed in production src/ (commands.cts, milestone.cts, roadmap.cts) and were each empirically timed against adversarial input (matching #2128's own measured-not-assumed precedent) — all confirmed linear-time/benign, left unbounded with a measured-evidence comment rather than mechanically bounded. The remaining 159 are test-file fixture parsing (test-author-controlled, fixed-size content, not adversarial input) — each suppressed with a specific, non-generic reason. Zero functional behavior changed anywhere in this diff. tests/no-pending-3212-markers.test.cjs locks the epic's own closing invariant (ADR §7: "assert zero pending #3212 markers remain") — ground truth confirmed trivially true today (no phase left any such marker behind), now regression-locked going forward. Design: .gsd/phase/chore-3415-prohibition-with-teeth/40-design.md Test matrix: .gsd/phase/chore-3415-prohibition-with-teeth/50-test-matrix.md Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): correct rule category mislabel, add CI test-scope entry An orthogonal Standards-axis review found eslint-rules/no-unbounded-quantifier.cjs mistakenly carried meta.docs.category: 'Portability', copied from a sibling rule without realizing what that implied: docs/contributing/cross-platform- portability-rules.md governs an ADR-1703 rule family under a hard "zero escape hatches" contract (tests/portability-rule-disable-ban.test.cjs's PROTECTED_RULES bans eslint-disable for those rules entirely). This rule is not part of that family — it's ADR-3212 (ReDoS/CWE-1333), a different epic — and its eslint-disable-next-line suppressions (159 of them, added earlier this same phase after empirical benign-verification) are an intentional, correct design, not a bypass. Corrected to category: 'Best Practices', matching the actual precedent (no-adhoc-regex-escape.cjs, Phase 1 of the same epic, which is also correctly outside PROTECTED_RULES), and the rule's own docstring now states this explicitly so a future reader doesn't have to re-derive it. Also registers a new scripts/ci-test-scope.cjs bucket so editing this rule or the shared eslint-rules/lib/readfilesync-trace.cjs helper re-runs their own test suites under targeted CI selection — was previously unregistered and invisible to that fast-path (this PR's own gsd-test checkpoint runs the full suite regardless, so this only affects future narrowly-scoped PRs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): bound no-unbounded-quantifier's own scanner (CWE-1333, ironic) Security review found the rule meant to catch algorithmic-complexity bugs had one of its own: hasUnboundedBroadQuantifier's negated-class inner scan walked from each `[^` occurrence to the next `]` (or EOF) with no bound, while the outer loop only ever advanced by one character — O(n²) total work on a pattern with many unclosed `[^` runs. Runs unconditionally inside checkPattern on any `new RegExp('literal string')` argument in any linted file, before the (cheap) readFileSync data-flow gate — so a single crafted string literal, no valid regex syntax required, could make `npm run lint` / CI hang. Empirically confirmed both the bug and the fix: pre-fix, n=4000/8000/ 16000/32000 chars took 30.8/115.6/463.8/1874.3ms (~4x work per 2x n, quadratic); extrapolated, the 300000-char repro from the finding would run ~165s. Post-fix (bail the inner scan once units exceeds the rule's own 1-2-unit scope, rather than continuing to hunt for a closing `]`), the same 300000-char input runs in 8.7ms via the real rule module, independently reconfirmed at 18ms via a fresh Linter.verify() call. New regression row in tests/no-unbounded-quantifier.rule.test.cjs asserts the RuleTester run on a 50000-char adversarial pattern completes and returns a defined result — no wall-clock assertion (CLAUDE.md Clock Seams / local/no-elapsed-assertion). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3415): triage 3 new sites, re-raise ceiling after upstream batch next merged 12 more PRs during this PR's review. Two consequences: - tests/edit-phase.test.cjs (fix #3262, unrelated) added 3 new content.match(/<tag>([\s\S]*?)<\/tag>/) reads of this repo's own workflow .md content — the same Class A pattern as the ~159 sites already triaged elsewhere in this PR. Suppressed with the same established reason. - lint-allow-test-rule-refs' ratchet ceiling needed re-raising again (301 -> 303) for the same reason as the two prior bumps: organic growth from unrelated, already-reviewed PRs landing concurrently, not a defect in this branch's own diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
564 lines
22 KiB
JavaScript
564 lines
22 KiB
JavaScript
// allow-test-rule: docs-parity
|
|
// allow-test-rule: source-text-is-the-product — settings-advanced.md prompt text is the deployed contract (#1216)
|
|
// Extracts CONFIG_DEFAULTS keys from config-loader.cjs source to verify planning-config.md
|
|
// stays in sync. The canonical list of defaults lives in source; there is no runtime
|
|
// API to enumerate them. Source inspection is the only practical parity check here.
|
|
// CONFIG_DEFAULTS was extracted from core.cjs into config-loader.cjs by ADR-857 phase 2e.
|
|
|
|
/**
|
|
* Verify planning-config.md documents all config fields from source code.
|
|
*/
|
|
|
|
const { describe, test, before } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const REFERENCE_PATH = path.join(__dirname, '..', 'gsd-core', 'references', 'planning-config.md');
|
|
const CORE_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'config-loader.cjs');
|
|
|
|
describe('config-field-docs', () => {
|
|
let content;
|
|
|
|
before(() => {
|
|
content = fs.readFileSync(REFERENCE_PATH, 'utf-8');
|
|
});
|
|
|
|
test('contains Complete Field Reference section', () => {
|
|
assert.ok(
|
|
content.includes('## Complete Field Reference'),
|
|
'planning-config.md must contain a "Complete Field Reference" heading'
|
|
);
|
|
});
|
|
|
|
test('documents at least 15 config fields in tables', () => {
|
|
// Count table rows that start with | `<key>` (field rows, not header/separator)
|
|
const fieldRows = content.match(/^\| `[a-z_][a-z0-9_.]*` \|/gm);
|
|
assert.ok(fieldRows, 'Expected markdown table rows with backtick-quoted keys');
|
|
assert.ok(
|
|
fieldRows.length >= 15,
|
|
`Expected at least 15 documented fields, found ${fieldRows.length}`
|
|
);
|
|
});
|
|
|
|
test('contains example configurations', () => {
|
|
assert.ok(
|
|
content.includes('## Example Configurations'),
|
|
'planning-config.md must contain an "Example Configurations" section'
|
|
);
|
|
// Verify at least one JSON code block with a model_profile key
|
|
assert.ok(
|
|
content.includes('"model_profile"'),
|
|
'Example configurations must include model_profile'
|
|
);
|
|
});
|
|
|
|
test('contains field interactions section', () => {
|
|
assert.ok(
|
|
content.includes('## Field Interactions'),
|
|
'planning-config.md must contain a "Field Interactions" section'
|
|
);
|
|
});
|
|
|
|
test('every CONFIG_DEFAULTS key appears in the doc', () => {
|
|
// Extract CONFIG_DEFAULTS keys from config-loader.cjs source (moved from core.cjs by ADR-857 phase 2e)
|
|
const coreSource = fs.readFileSync(CORE_PATH, 'utf-8');
|
|
const defaultsMatch = coreSource.match(
|
|
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own config-loader.cjs source, fixed-size author-controlled content
|
|
/const CONFIG_DEFAULTS\s*=\s*\{([\s\S]*?)\r?\n\};/
|
|
);
|
|
assert.ok(defaultsMatch, 'Could not find CONFIG_DEFAULTS in config-loader.cjs');
|
|
|
|
const body = defaultsMatch[1];
|
|
// Match property keys (word characters before the colon)
|
|
const keys = [...body.matchAll(/^\s*(\w+)\s*:/gm)].map(m => m[1]);
|
|
assert.ok(keys.length > 0, 'Could not extract any keys from CONFIG_DEFAULTS');
|
|
|
|
// CONFIG_DEFAULTS uses flat keys; the doc may use namespaced equivalents.
|
|
// Map flat keys to the namespace forms used in config.json and the doc.
|
|
const NAMESPACE_MAP = {
|
|
research: 'workflow.research',
|
|
plan_checker: 'workflow.plan_check',
|
|
verifier: 'workflow.verifier',
|
|
nyquist_validation: 'workflow.nyquist_validation',
|
|
ai_integration_phase: 'workflow.ai_integration_phase',
|
|
api_coverage_gate: 'workflow.api_coverage_gate',
|
|
text_mode: 'workflow.text_mode',
|
|
subagent_timeout: 'workflow.subagent_timeout',
|
|
branching_strategy: 'git.branching_strategy',
|
|
phase_branch_template: 'git.phase_branch_template',
|
|
milestone_branch_template: 'git.milestone_branch_template',
|
|
quick_branch_template: 'git.quick_branch_template',
|
|
security_enforcement: 'workflow.security_enforcement',
|
|
security_asvs_level: 'workflow.security_asvs_level',
|
|
security_block_on: 'workflow.security_block_on',
|
|
};
|
|
|
|
const missing = keys.filter(k => {
|
|
// Check both bare key and namespaced form
|
|
if (content.includes(`\`${k}\``)) return false;
|
|
const ns = NAMESPACE_MAP[k];
|
|
if (ns && content.includes(`\`${ns}\``)) return false;
|
|
return true;
|
|
});
|
|
assert.deepStrictEqual(
|
|
missing,
|
|
[],
|
|
`CONFIG_DEFAULTS keys missing from planning-config.md: ${missing.join(', ')}`
|
|
);
|
|
});
|
|
|
|
test('documents workflow namespace fields', () => {
|
|
const workflowFields = [
|
|
'workflow.research',
|
|
'workflow.plan_check',
|
|
'workflow.verifier',
|
|
'workflow.nyquist_validation',
|
|
'workflow.use_worktrees',
|
|
'workflow.subagent_timeout',
|
|
'workflow.text_mode',
|
|
];
|
|
const missing = workflowFields.filter(f => !content.includes(`\`${f}\``));
|
|
assert.deepStrictEqual(
|
|
missing,
|
|
[],
|
|
`Workflow fields missing from planning-config.md: ${missing.join(', ')}`
|
|
);
|
|
});
|
|
|
|
test('documents git namespace fields', () => {
|
|
const gitFields = [
|
|
'git.branching_strategy',
|
|
'git.base_branch',
|
|
'git.phase_branch_template',
|
|
'git.milestone_branch_template',
|
|
];
|
|
const missing = gitFields.filter(f => !content.includes(`\`${f}\``));
|
|
assert.deepStrictEqual(
|
|
missing,
|
|
[],
|
|
`Git fields missing from planning-config.md: ${missing.join(', ')}`
|
|
);
|
|
});
|
|
|
|
test('documents KNOWN_TOP_LEVEL internal fields not in CONFIG_DEFAULTS', () => {
|
|
// These fields are in KNOWN_TOP_LEVEL (core.cjs) and read by loadConfig()
|
|
// but not in CONFIG_DEFAULTS, so the CONFIG_DEFAULTS test doesn't cover them.
|
|
const internalFields = [
|
|
'model_overrides',
|
|
'agent_skills',
|
|
];
|
|
const missing = internalFields.filter(f => !content.includes(`\`${f}\``));
|
|
assert.deepStrictEqual(
|
|
missing,
|
|
[],
|
|
`KNOWN_TOP_LEVEL internal fields missing from planning-config.md: ${missing.join(', ')}`
|
|
);
|
|
});
|
|
|
|
test('documents sub_repos field (CONFIG_DEFAULTS, no namespace form)', () => {
|
|
// sub_repos is in CONFIG_DEFAULTS but has no NAMESPACE_MAP entry
|
|
// (it uses a planning.sub_repos nested lookup but is documented as a
|
|
// top-level field). Verify it explicitly since the NAMESPACE_MAP path
|
|
// would silently skip it.
|
|
assert.ok(
|
|
content.includes('`sub_repos`'),
|
|
'planning-config.md must document the sub_repos field'
|
|
);
|
|
});
|
|
|
|
test('documents features.thinking_partner field', () => {
|
|
// features.thinking_partner is in VALID_CONFIG_KEYS (config.cjs) and
|
|
// used by discuss-phase.md and plan-phase.md for conditional extended
|
|
// thinking at workflow decision points.
|
|
assert.ok(
|
|
content.includes('`features.thinking_partner`'),
|
|
'planning-config.md must document the features.thinking_partner field'
|
|
);
|
|
});
|
|
|
|
test('mode field documents correct allowed values', () => {
|
|
// mode values are "interactive" and "yolo" per templates/config.json
|
|
// and workflows/new-project.md — NOT "code-first"/"plan-first"/"hybrid"
|
|
assert.ok(
|
|
content.includes('"interactive"') && content.includes('"yolo"'),
|
|
'mode field must document "interactive" and "yolo" as allowed values'
|
|
);
|
|
assert.ok(
|
|
!content.includes('"code-first"'),
|
|
'mode field must NOT document non-existent "code-first" value'
|
|
);
|
|
});
|
|
|
|
test('discuss_mode field documents correct allowed values', () => {
|
|
// discuss_mode values are "discuss" and "assumptions" per workflows/settings.md
|
|
// NOT "auto" or "analyze" (those are CLI flags, not config values)
|
|
assert.ok(
|
|
content.includes('"assumptions"'),
|
|
'discuss_mode must document "assumptions" as an allowed value'
|
|
);
|
|
});
|
|
|
|
test('documents plan_checker alias for workflow.plan_check', () => {
|
|
// plan_checker is the flat-key form in CONFIG_DEFAULTS; workflow.plan_check
|
|
// is the canonical namespaced form. The doc should mention the alias.
|
|
assert.ok(
|
|
content.includes('`workflow.plan_check`'),
|
|
'planning-config.md must document workflow.plan_check'
|
|
);
|
|
assert.ok(
|
|
content.includes('plan_checker'),
|
|
'planning-config.md must mention the plan_checker flat-key alias'
|
|
);
|
|
});
|
|
|
|
test('workflow.test_command is documented in planning-config.md (#1216)', () => {
|
|
assert.ok(
|
|
content.includes('`workflow.test_command`'),
|
|
'planning-config.md must document workflow.test_command'
|
|
);
|
|
// Must appear specifically in the Complete Field Reference section
|
|
const completeRefSection = content.slice(content.indexOf('## Complete Field Reference'));
|
|
assert.ok(
|
|
completeRefSection.includes('`workflow.test_command`'),
|
|
'planning-config.md Complete Field Reference must include workflow.test_command'
|
|
);
|
|
});
|
|
|
|
test('workflow.build_command is documented in planning-config.md (#1216)', () => {
|
|
assert.ok(
|
|
content.includes('`workflow.build_command`'),
|
|
'planning-config.md must document workflow.build_command'
|
|
);
|
|
// Must appear specifically in the Complete Field Reference section
|
|
const completeRefSection = content.slice(content.indexOf('## Complete Field Reference'));
|
|
assert.ok(
|
|
completeRefSection.includes('`workflow.build_command`'),
|
|
'planning-config.md Complete Field Reference must include workflow.build_command'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── Capability-enum doc parity (#3303) ─────────────────────────────────────
|
|
|
|
describe('capability-enum doc parity (#3303)', () => {
|
|
const CAPABILITY_PATH = path.join(
|
|
__dirname, '..', 'capabilities', 'code-review', 'capability.json'
|
|
);
|
|
|
|
let docContent;
|
|
let capability;
|
|
|
|
before(() => {
|
|
docContent = fs.readFileSync(REFERENCE_PATH, 'utf-8');
|
|
capability = JSON.parse(fs.readFileSync(CAPABILITY_PATH, 'utf-8'));
|
|
});
|
|
|
|
/**
|
|
* Extract a single Complete-Field-Reference table row by key and split it into
|
|
* trimmed cells: ['', '`key`', type, default, allowedValues, description, ''].
|
|
* Row-scoping keeps unrelated wording elsewhere in the doc from satisfying
|
|
* (or breaking) the parity assertions.
|
|
*/
|
|
function docRow(key) {
|
|
const line = docContent
|
|
.split(/\r?\n/)
|
|
.find(l => l.startsWith(`| \`${key}\` |`));
|
|
assert.ok(line, `planning-config.md must have a table row for \`${key}\``);
|
|
return line.split('|').map(c => c.trim());
|
|
}
|
|
|
|
test('workflow.code_review_depth allowed values match the capability registry (#3303)', () => {
|
|
const spec = capability.config && capability.config['workflow.code_review_depth'];
|
|
assert.ok(
|
|
spec && Array.isArray(spec.values),
|
|
'capabilities/code-review/capability.json must declare workflow.code_review_depth values'
|
|
);
|
|
|
|
const allowedCell = docRow('workflow.code_review_depth')[4];
|
|
const docValues = [...allowedCell.matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
|
assert.ok(
|
|
docValues.length > 0,
|
|
`workflow.code_review_depth Allowed-Values cell must list quoted values, got: ${allowedCell}`
|
|
);
|
|
assert.deepStrictEqual(
|
|
[...docValues].sort(),
|
|
[...spec.values].sort(),
|
|
`planning-config.md workflow.code_review_depth allowed values (${docValues.join(', ')}) ` +
|
|
`must exactly match what config-set accepts per capability.json (${spec.values.join(', ')})`
|
|
);
|
|
});
|
|
|
|
test('workflow.code_review_depth default matches the capability registry (#3303)', () => {
|
|
const spec = capability.config && capability.config['workflow.code_review_depth'];
|
|
assert.ok(spec, 'capability.json must declare workflow.code_review_depth');
|
|
const defaultCell = docRow('workflow.code_review_depth')[3];
|
|
const [docDefault] = [...defaultCell.matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
|
assert.strictEqual(
|
|
docDefault,
|
|
spec.default,
|
|
`planning-config.md workflow.code_review_depth default must match capability.json default (${spec.default})`
|
|
);
|
|
});
|
|
|
|
test('agent_skills row documents the array-of-strings form (#3303)', () => {
|
|
const cells = docRow('agent_skills');
|
|
const allowedCell = cells[4];
|
|
assert.ok(
|
|
/\[\s*"<skill-set>"\s*,/.test(allowedCell),
|
|
`agent_skills Allowed-Values cell must show the array-of-strings form, got: ${allowedCell}`
|
|
);
|
|
assert.ok(
|
|
/array/i.test(cells[5]),
|
|
'agent_skills description must explain the array form assigns multiple skill sets to one agent type'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── CONFIGURATION.md parity (#1216) ────────────────────────────────────────
|
|
|
|
describe('CONFIGURATION.md parity (#1216)', () => {
|
|
const DOCS_CONFIG_PATH = path.join(__dirname, '..', 'docs', 'CONFIGURATION.md');
|
|
const SETTINGS_ADVANCED_PATH = path.join(
|
|
__dirname,
|
|
'..',
|
|
'gsd-core',
|
|
'workflows',
|
|
'settings-advanced.md',
|
|
);
|
|
|
|
let docsContent;
|
|
let settingsAdvancedContent;
|
|
|
|
before(() => {
|
|
docsContent = fs.readFileSync(DOCS_CONFIG_PATH, 'utf-8');
|
|
settingsAdvancedContent = fs.readFileSync(SETTINGS_ADVANCED_PATH, 'utf-8');
|
|
});
|
|
|
|
test('CONFIGURATION.md workflow.subagent_timeout describes milliseconds, not seconds (#1216)', () => {
|
|
assert.ok(
|
|
docsContent.includes('millisecond') || docsContent.includes('milliseconds'),
|
|
'CONFIGURATION.md workflow.subagent_timeout must use the word "millisecond(s)"'
|
|
);
|
|
assert.ok(
|
|
!docsContent.match(/\|\s*`workflow\.subagent_timeout`[^|]*\|\s*`?600`?\s*\|/),
|
|
'CONFIGURATION.md workflow.subagent_timeout must NOT have default 600 (that was the seconds default)'
|
|
);
|
|
});
|
|
|
|
test('CONFIGURATION.md workflow.subagent_timeout default is 300000 (#1216)', () => {
|
|
// Row-scoped: the actual table row for workflow.subagent_timeout must contain 300000
|
|
assert.ok(
|
|
/\|\s*`workflow\.subagent_timeout`\s*\|[^|]*\|\s*`?300000`?\s*\|/.test(docsContent),
|
|
'CONFIGURATION.md workflow.subagent_timeout table row must have default 300000'
|
|
);
|
|
});
|
|
|
|
test('settings-advanced.md subagent_timeout prompt says milliseconds, not seconds (#1216)', () => {
|
|
assert.ok(
|
|
settingsAdvancedContent.includes('millisecond') ||
|
|
settingsAdvancedContent.includes('milliseconds'),
|
|
'settings-advanced.md subagent_timeout prompt must use "millisecond(s)"'
|
|
);
|
|
assert.ok(
|
|
!settingsAdvancedContent.includes('Integer number of seconds'),
|
|
'settings-advanced.md must NOT say "Integer number of seconds" for subagent_timeout'
|
|
);
|
|
});
|
|
|
|
test('settings-advanced.md subagent_timeout prompt default is 300000 not 600 (#1216)', () => {
|
|
assert.ok(
|
|
!settingsAdvancedContent.match(/value or 600/),
|
|
'settings-advanced.md must NOT show 600 as the subagent_timeout default'
|
|
);
|
|
assert.ok(
|
|
settingsAdvancedContent.includes('300000'),
|
|
'settings-advanced.md must show 300000 as the subagent_timeout default'
|
|
);
|
|
});
|
|
|
|
test('settings-advanced.md parse-default list must NOT show subagent_timeout default 600 (#1216)', () => {
|
|
// Line 53 regression: the parse-default list item must use 300000, not 600
|
|
assert.ok(
|
|
!(/`workflow\.subagent_timeout`[^\r\n]*default:[^\n]*`?600`?/.test(settingsAdvancedContent)),
|
|
'settings-advanced.md must NOT list subagent_timeout default as 600 (stale seconds default)'
|
|
);
|
|
});
|
|
|
|
test('settings-advanced.md confirmation table must NOT label subagent_timeout as {seconds} (#1216)', () => {
|
|
// Line 754 regression: the confirmation table row must say {milliseconds}, not {seconds}
|
|
assert.ok(
|
|
!(/workflow\.subagent_timeout\s*\|\s*\{seconds\}/.test(settingsAdvancedContent)),
|
|
'settings-advanced.md confirmation table must NOT label subagent_timeout as {seconds}'
|
|
);
|
|
});
|
|
|
|
test('settings-advanced.md bash example must NOT use subagent_timeout 900 (#1216)', () => {
|
|
// Line 501 regression: the bash example must not show the stale 900 value
|
|
assert.ok(
|
|
!(/subagent_timeout 900\b/.test(settingsAdvancedContent)),
|
|
'settings-advanced.md bash example must NOT set subagent_timeout to 900 (stale seconds value)'
|
|
);
|
|
});
|
|
|
|
test('CONFIGURATION.md review.models rows do not show shell command examples (#1216)', () => {
|
|
// The Integration Settings section (around line 195-202) used to have
|
|
// shell-command examples like "codex exec --model gpt-5". After the fix
|
|
// those rows must describe model ids, not full commands.
|
|
assert.ok(
|
|
!docsContent.includes('"codex exec --model'),
|
|
'CONFIGURATION.md must NOT contain "codex exec --model" shell command example'
|
|
);
|
|
assert.ok(
|
|
!docsContent.includes('"opencode run --model'),
|
|
'CONFIGURATION.md must NOT contain "opencode run --model" shell command example'
|
|
);
|
|
assert.ok(
|
|
!docsContent.includes('"gemini -m gemini'),
|
|
'CONFIGURATION.md must NOT contain "gemini -m gemini..." shell command example'
|
|
);
|
|
});
|
|
|
|
test('workflow.test_command is documented in CONFIGURATION.md (#1216)', () => {
|
|
assert.ok(
|
|
docsContent.includes('`workflow.test_command`'),
|
|
'CONFIGURATION.md must document workflow.test_command'
|
|
);
|
|
});
|
|
|
|
test('workflow.build_command is documented in CONFIGURATION.md (#1216)', () => {
|
|
assert.ok(
|
|
docsContent.includes('`workflow.build_command`'),
|
|
'CONFIGURATION.md must document workflow.build_command'
|
|
);
|
|
});
|
|
});
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
// Folded from tests/enh-1494-workflow-config-key-docs.test.cjs — consolidation epic #1969 (B3 #1972)
|
|
// ────────────────────────────────────────────────────────────────────────
|
|
{
|
|
const { describe: __foldDescribe } = require('node:test');
|
|
__foldDescribe("folded:enh-1494-workflow-config-key-docs (consolidation epic #1969 B3 #1972)", () => {
|
|
'use strict';
|
|
|
|
/**
|
|
* Parity assertions for #1494: workflow config keys that are consumed by
|
|
* planning-pipeline code must be (a) accepted by VALID_CONFIG_KEYS and
|
|
* (b) documented in references/planning-config.md.
|
|
*
|
|
* Per DEFECT.GENERATIVE-FIX: a shared constant / key-list that spans two
|
|
* surfaces requires a parity assertion that fails when the surfaces diverge.
|
|
*/
|
|
|
|
const { describe, test, before, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
|
|
|
|
const CONFIG_SCHEMA_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'config-schema.cjs');
|
|
const PLANNING_CONFIG_PATH = path.join(__dirname, '..', 'gsd-core', 'references', 'planning-config.md');
|
|
|
|
describe('VALID_CONFIG_KEYS parity — #1494 orphan-undocumented keys', () => {
|
|
const { VALID_CONFIG_KEYS } = require(CONFIG_SCHEMA_PATH);
|
|
|
|
test('workflow.mvp_mode is in VALID_CONFIG_KEYS', () => {
|
|
assert.ok(
|
|
VALID_CONFIG_KEYS.has('workflow.mvp_mode'),
|
|
'workflow.mvp_mode is read by config-loader.cts and plan-phase.md but was missing from VALID_CONFIG_KEYS (#1494)'
|
|
);
|
|
});
|
|
|
|
test('workflow.code_review_command is in VALID_CONFIG_KEYS', () => {
|
|
assert.ok(
|
|
VALID_CONFIG_KEYS.has('workflow.code_review_command'),
|
|
'workflow.code_review_command must be in VALID_CONFIG_KEYS'
|
|
);
|
|
});
|
|
|
|
test('workflow.plan_chunked is in VALID_CONFIG_KEYS', () => {
|
|
assert.ok(
|
|
VALID_CONFIG_KEYS.has('workflow.plan_chunked'),
|
|
'workflow.plan_chunked must be in VALID_CONFIG_KEYS'
|
|
);
|
|
});
|
|
|
|
test('workflow.test_command is in VALID_CONFIG_KEYS', () => {
|
|
assert.ok(
|
|
VALID_CONFIG_KEYS.has('workflow.test_command'),
|
|
'workflow.test_command must be in VALID_CONFIG_KEYS'
|
|
);
|
|
});
|
|
|
|
test('workflow.build_command is in VALID_CONFIG_KEYS', () => {
|
|
assert.ok(
|
|
VALID_CONFIG_KEYS.has('workflow.build_command'),
|
|
'workflow.build_command must be in VALID_CONFIG_KEYS'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('config-set accepts workflow.mvp_mode (#1494)', () => {
|
|
let tmpDir;
|
|
afterEach(() => { if (tmpDir) cleanup(tmpDir); });
|
|
|
|
test('config-set workflow.mvp_mode true succeeds and stores the value', () => {
|
|
tmpDir = createTempProject();
|
|
const result = runGsdTools(['config-set', 'workflow.mvp_mode', 'true'], tmpDir);
|
|
assert.ok(
|
|
result.success,
|
|
`config-set workflow.mvp_mode must succeed; got:\nstdout: ${result.output}\nstderr: ${result.error}`
|
|
);
|
|
const parsed = JSON.parse(result.output);
|
|
assert.strictEqual(parsed.updated, true, 'response must have updated:true');
|
|
assert.strictEqual(parsed.key, 'workflow.mvp_mode', 'response must echo the key');
|
|
});
|
|
|
|
test('config-set workflow.mvp_mode false succeeds', () => {
|
|
tmpDir = createTempProject();
|
|
const result = runGsdTools(['config-set', 'workflow.mvp_mode', 'false'], tmpDir);
|
|
assert.ok(
|
|
result.success,
|
|
`config-set workflow.mvp_mode false must succeed; got:\nstdout: ${result.output}\nstderr: ${result.error}`
|
|
);
|
|
const parsed = JSON.parse(result.output);
|
|
assert.strictEqual(parsed.updated, true);
|
|
});
|
|
});
|
|
|
|
// allow-test-rule: source-text-is-the-product — planning-config.md is the deployed reference contract (#1494)
|
|
describe('planning-config.md documents #1494 keys', () => {
|
|
let content;
|
|
before(() => { content = fs.readFileSync(PLANNING_CONFIG_PATH, 'utf-8'); });
|
|
|
|
const KEYS = [
|
|
'workflow.mvp_mode',
|
|
'workflow.code_review_command',
|
|
'workflow.plan_chunked',
|
|
'workflow.test_command',
|
|
'workflow.build_command',
|
|
];
|
|
|
|
for (const key of KEYS) {
|
|
test(`planning-config.md documents \`${key}\``, () => {
|
|
assert.ok(
|
|
content.includes(`\`${key}\``),
|
|
`planning-config.md must document \`${key}\` (#1494)`
|
|
);
|
|
});
|
|
|
|
test(`\`${key}\` appears in the Complete Field Reference section`, () => {
|
|
const refSection = content.slice(content.indexOf('## Complete Field Reference'));
|
|
assert.ok(
|
|
refSection.includes(`\`${key}\``),
|
|
`planning-config.md Complete Field Reference must include \`${key}\` (#1494)`
|
|
);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|