Files
msd-core/tests/discuss-phase-power.test.cjs
Tom Boucher 4a0b5e26b4 test(#3338): fold the verify/validate & workflow-text issue-* cluster — Wave 6 (#3383)
* test(#3338): fold the verify/validate & workflow-text issue-* cluster — Wave 6

Folds 10 legacy issue-*.test.cjs regression files (75 test() blocks) into
their module's main suite, per H3 (#3315) of the test-hygiene epic (#3053).
Third of 4 issue-* waves.

- issue-2701-nul-corrupted-validators.test.cjs (9) + issue-429-comment-
  text-gate.test.cjs (31, incl. fast-check property tests): both target
  verify.cjs/validate.cjs via different calling styles (CLI vs direct-
  require) — merged jointly into verify.test.cjs, 0 dropped.
- issue-2762-plan-reviews-chunked.test.cjs (3) merged into
  plan-phase-drift-guard.test.cjs.
- issue-2771-advisor-subagent-type.test.cjs (1) + issue-2772-discuss-
  phase-text-inconsistencies.test.cjs (6) merged jointly into
  discuss-phase-power.test.cjs.
- issue-498-update-backup-runtime-dir.test.cjs (3, rename basis) +
  issue-815-update-next-channel.test.cjs (7, merged in): both concern
  update.md workflow-text contracts, now update-workflow.test.cjs.
- issue-498-update-context.test.cjs (13): pure rename to
  update-context.test.cjs, sole comprehensive suite for its module.
- issue-2765-brace-expansion-lockfile.test.cjs (1, rename basis) +
  issue-3238-js-yaml-lockfile.test.cjs (1, merged in): two distinct CVE
  regression pins against package-lock.json, now lockfile-cve-audit.test.cjs,
  shared ROOT/npmLs helpers deduped instead of double-declared.

Ratchet upkeep to keep this wave's own gates green: pruned 3 stale
allow-test-rule allowlist entries, cited 2 previously-uncited comments
that surfaced in the folded content (#3338), tightened the exemption-file
ceiling 305 -> 297. Fixed one stale filename reference in production code
(src/init.cts) plus one in docs/reference/workflow-fragments.md.

Zero net test-coverage loss. No production code BEHAVIOR changed.

* test(#3338): fix orthogonal-review findings — Wave 6 fold

Standards-axis review found a real structural defect in two files, both
the same root cause and both fixed here:

- tests/update-workflow.test.cjs: the folded:issue-815-update-next-channel
  wrapper's closing brace was placed after the file's pre-existing tail
  instead of before it, making the already-established folded:bug-2470
  and folded:bug-3130 wrappers CHILDREN of issue-815's block in the test
  hierarchy instead of independent siblings — confirmed via an actual
  node --test run showing the mislabeled TAP nesting. Moved the closing
  brace to the correct position; all three fold wrappers are now
  top-level siblings again (verified via node --test, TAP hierarchy
  correct, 10/10 tests, 5 suites, identical count before and after).
- tests/plan-phase-drift-guard.test.cjs: same mistake in the other
  direction — folded:issue-2762-plan-reviews-chunked was spliced inside
  the pre-existing folded:bug-2492-context-coverage-gate wrapper instead
  of after it. Fixed the same way (227/227 tests, 38 suites, identical
  count before and after).
- Reverted unnecessary 815-suffixed local renames (assert815/fs815/etc.)
  introduced by the fold — the wrapper is genuinely block-scoped once
  correctly closed, so no collision existed (same class as Wave 4's
  __foldSetNested finding).

No test() count changed in either file. No production code touched.

---------

Co-authored-by: sim <sim@local>
2026-08-12 00:51:19 -04:00

265 lines
14 KiB
JavaScript

// allow-test-rule: source-text-is-the-product
// 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.
/**
* GSD Tools Tests - discuss-phase power user mode
*
* Validates that the --power flag workflow documentation is present and
* correctly describes the bulk question generation/answering flow.
*
* Closes: #1513
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
describe('discuss-phase power user mode (#1513)', () => {
const commandPath = path.join(__dirname, '..', 'commands', 'gsd', 'discuss-phase.md');
const workflowPath = path.join(__dirname, '..', 'gsd-core', 'workflows', 'discuss-phase.md');
const powerWorkflowPath = path.join(__dirname, '..', 'gsd-core', 'workflows', 'discuss-phase-power.md');
describe('command file (discuss-phase.md)', () => {
test('mentions --power flag in argument-hint or description', () => {
const content = fs.readFileSync(commandPath, 'utf8');
assert.ok(
content.includes('--power'),
'commands/gsd/discuss-phase.md should document the --power flag'
);
});
test('references the power workflow file', () => {
const content = fs.readFileSync(commandPath, 'utf8');
assert.ok(
content.includes('discuss-phase-power'),
'command file should reference discuss-phase-power workflow'
);
});
});
describe('main workflow file (discuss-phase.md)', () => {
test('has power_user_mode section or references discuss-phase-power.md', () => {
// After the discuss-phase/modes split (#717), the power dispatch lives in discuss-phase/modes/power.md and
// the parent references it via the dispatch table.
const parentContent = fs.readFileSync(workflowPath, 'utf8');
const powerModePath = path.join(__dirname, '..', 'gsd-core', 'workflows', 'discuss-phase', 'modes', 'power.md');
const powerMode = fs.existsSync(powerModePath) ? fs.readFileSync(powerModePath, 'utf8') : '';
const content = parentContent + '\n' + powerMode;
const hasPowerSection = content.includes('power_user_mode') || content.includes('power user mode') || content.includes('modes/power.md');
const hasReference = content.includes('discuss-phase-power');
assert.ok(
hasPowerSection || hasReference,
'discuss-phase.md (or modes/power.md after the discuss-phase/modes split) should have power_user_mode section or reference discuss-phase-power.md'
);
});
test('describes --power flag routing', () => {
const content = fs.readFileSync(workflowPath, 'utf8');
assert.ok(
content.includes('--power'),
'discuss-phase.md should describe --power flag handling'
);
});
});
describe('power workflow file (discuss-phase-power.md)', () => {
test('file exists', () => {
assert.ok(
fs.existsSync(powerWorkflowPath),
'gsd-core/workflows/discuss-phase-power.md should exist'
);
});
test('describes the generate step', () => {
const content = fs.readFileSync(powerWorkflowPath, 'utf8');
assert.ok(
content.includes('generate') || content.includes('Generate'),
'power workflow should describe generating questions'
);
});
test('describes the wait/notify step', () => {
const content = fs.readFileSync(powerWorkflowPath, 'utf8');
const hasWait = content.includes('wait') || content.includes('Wait');
const hasNotify = content.includes('notify') || content.includes('Notify') || content.includes('notif');
assert.ok(
hasWait || hasNotify,
'power workflow should describe the wait/notify step after generating files'
);
});
test('describes the refresh step', () => {
const content = fs.readFileSync(powerWorkflowPath, 'utf8');
assert.ok(
content.includes('refresh') || content.includes('Refresh'),
'power workflow should describe the refresh step for processing answers'
);
});
test('describes the finalize step', () => {
const content = fs.readFileSync(powerWorkflowPath, 'utf8');
assert.ok(
content.includes('finalize') || content.includes('Finalize'),
'power workflow should describe the finalize step for generating CONTEXT.md'
);
});
test('QUESTIONS.json structure has required fields', () => {
const content = fs.readFileSync(powerWorkflowPath, 'utf8');
assert.ok(content.includes('QUESTIONS.json'), 'should mention QUESTIONS.json file');
assert.ok(content.includes('"phase"'), 'JSON structure should include phase field');
assert.ok(content.includes('"stats"'), 'JSON structure should include stats field');
assert.ok(content.includes('"sections"'), 'JSON structure should include sections field');
assert.ok(
content.includes('"id"') && content.includes('"title"'),
'JSON structure should include question id and title fields'
);
assert.ok(
content.includes('"options"'),
'JSON structure should include options array'
);
assert.ok(
content.includes('"answer"'),
'JSON structure should include answer field'
);
assert.ok(
content.includes('"status"'),
'JSON structure should include status field'
);
});
test('describes HTML generation step', () => {
const content = fs.readFileSync(powerWorkflowPath, 'utf8');
assert.ok(
content.includes('QUESTIONS.html') || content.includes('.html'),
'power workflow should describe generating the HTML companion file'
);
assert.ok(
content.includes('HTML') || content.includes('html'),
'power workflow should mention HTML output'
);
});
test('QUESTIONS.json file naming uses padded phase number', () => {
const content = fs.readFileSync(powerWorkflowPath, 'utf8');
assert.ok(
content.includes('padded_phase') || content.includes('{padded_phase}') || content.includes('QUESTIONS.json'),
'power workflow should describe file naming with padded phase number'
);
});
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/issue-2771-advisor-subagent-type.test.cjs — H3 Wave 6 (#3338)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe('folded:issue-2771-advisor-subagent-type', () => {
// allow-test-rule: structural-implementation-guard (#2771)
'use strict';
// Regression guard for #2771: the discuss-phase advisor mode must spawn the REGISTERED
// `gsd-advisor-researcher` subagent (auto-loads the agent def), not `general-purpose` —
// which contradicts universal-anti-patterns rule 10 (injected into discuss-phase via
// <required_reading>): "NEVER use non-GSD agent types — ALWAYS use gsd-{agent}".
// Spawning general-purpose + a manual "read the agent def" prompt re-specifies what the
// def already owns (a drift risk; the same shape assumptions's answer_validation hit).
const ADVISOR_MD = path.join(
__dirname, '..', 'gsd-core', 'workflows', 'discuss-phase', 'modes', 'advisor.md'
);
test('advisor mode spawns the registered gsd-advisor-researcher subagent, not general-purpose (#2771)', () => {
const src = fs.readFileSync(ADVISOR_MD, 'utf8');
// Locate the Agent() block that researches gray areas.
const agentIdx = src.indexOf('subagent_type=');
assert.ok(agentIdx !== -1, 'advisor.md must contain an Agent() subagent_type declaration');
assert.ok(
src.includes('subagent_type="gsd-advisor-researcher"'),
'advisor mode must spawn subagent_type="gsd-advisor-researcher" (the registered agent def auto-loads) — not general-purpose (#2771, universal-anti-patterns rule 10)'
);
assert.ok(
!src.includes('subagent_type="general-purpose"'),
'advisor mode must NOT spawn subagent_type="general-purpose" (contradicts universal-anti-patterns rule 10, injected into the same context) (#2771)'
);
// The manual "read @.../gsd-advisor-researcher.md" prompt line must be gone —
// spawning by type auto-loads the def; re-specifying it is a drift risk. Deny the
// full class (any "read @" lead-in, case-insensitive) so a phrasing variant can't
// sneak the drift back in.
assert.ok(
!/read\s+@.*gsd-advisor-researcher\.md/i.test(src),
'advisor mode must not manually instruct reading the agent def — spawning by type auto-loads it (#2771)'
);
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/issue-2772-discuss-phase-text-inconsistencies.test.cjs — H3 Wave 6 (#3338)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe('folded:issue-2772-discuss-phase-text-inconsistencies', () => {
// allow-test-rule: structural-implementation-guard (#2772)
'use strict';
// Regression guard for #2772: four self-contained text inconsistencies in the
// discuss-phase surface, each a literal-instruction hazard. The shipped markdown IS
// the runtime contract, so structural inspection is the correct guard.
const ROOT = path.join(__dirname, '..');
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
test('auto.md does not read the dead MAX_PASSES / max_discuss_passes config (#2772.1)', () => {
const src = read('gsd-core/workflows/discuss-phase/modes/auto.md');
assert.ok(/single pass/i.test(src), 'auto.md must still mandate the single-pass rule');
assert.ok(!/MAX_PASSES=/.test(src), 'auto.md must not read MAX_PASSES (dead config — single-pass rule governs) (#2772)');
assert.ok(!/max_discuss_passes/.test(src), 'auto.md must not reference max_discuss_passes (contradicts the single-pass rule) (#2772)');
});
test('gate-prompts context-handling matches the actual check_existing options (#2772.2)', () => {
const src = read('gsd-core/references/gate-prompts.md');
const ctx = src.slice(src.indexOf('## Pattern: context-handling'), src.indexOf('## Pattern: gray-area-option'));
assert.ok(/Update it \| View it \| Skip/.test(ctx), 'context-handling options must be "Update it | View it | Skip" (the actual check_existing flow) (#2772)');
assert.ok(!/Overwrite \| Append \| Cancel/.test(ctx), 'context-handling must NOT document the obsolete "Overwrite | Append | Cancel" (#2772)');
});
test('gate-prompts gray-area-option does not mandate "Let Claude decide" (#2772.2)', () => {
const src = read('gsd-core/references/gate-prompts.md');
const gray = src.slice(src.indexOf('## Pattern: gray-area-option'));
assert.ok(!/Always include "Let Claude decide"/i.test(gray), 'gray-area-option must NOT mandate "Let Claude decide" — it contradicts discuss-phase.md:353 ("Do NOT include a skip or you decide option") (#2772)');
});
test('discuss-phase auto_advance fallback ends the workflow, not routes back to confirm_creation (#2772.3)', () => {
const src = read('gsd-core/workflows/discuss-phase.md');
const step = src.slice(src.indexOf('<step name="auto_advance">'), src.indexOf('</step>', src.indexOf('<step name="auto_advance">')));
assert.ok(!/route to `confirm_creation`/.test(step), 'auto_advance fallback must not route back to confirm_creation (it already ran earlier in the step order — circular) (#2772)');
assert.ok(/end here|workflow is complete/i.test(step), 'auto_advance fallback must explicitly END the workflow (positive anchor — a re-phrased regression should not slip past) (#2772)');
});
test('discuss-phase-assumptions auto_advance fallback also ends the workflow (sibling of #2772.3)', () => {
const src = read('gsd-core/workflows/discuss-phase-assumptions.md');
const step = src.slice(src.indexOf('<step name="auto_advance">'), src.indexOf('</step>', src.indexOf('<step name="auto_advance">')));
assert.ok(!/Route to confirm_creation step/.test(step), 'assumptions auto_advance fallback must not route back to confirm_creation (same circularity as the parent) (#2772)');
assert.ok(/end here|workflow is complete/i.test(step), 'assumptions auto_advance fallback must explicitly END the workflow (#2772)');
});
test('discuss-phase-assumptions answer_validation matches the parent canonical content (#2772.4)', () => {
const parent = read('gsd-core/workflows/discuss-phase.md');
const assumptions = read('gsd-core/workflows/discuss-phase-assumptions.md');
// The parent's canonical answer_validation includes the "Other" empty-text branch.
const parentBlock = parent.slice(parent.indexOf('<answer_validation>'), parent.indexOf('</answer_validation>') + '</answer_validation>'.length);
const assumptionsBlock = assumptions.slice(assumptions.indexOf('<answer_validation>'), assumptions.indexOf('</answer_validation>') + '</answer_validation>'.length);
assert.ok(/"Other" with empty text/.test(assumptionsBlock), 'assumptions answer_validation must include the "Other" empty-text branch (was drifted) (#2772)');
// The two blocks must now agree on the empty-response handling.
assert.strictEqual(assumptionsBlock, parentBlock, 'discuss-phase-assumptions answer_validation must match the parent canonical block exactly (single source of truth) (#2772)');
});
});
}