Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
265 lines
14 KiB
JavaScript
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.
|
|
|
|
/**
|
|
* MSD 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', 'msd', 'discuss-phase.md');
|
|
const workflowPath = path.join(__dirname, '..', 'msd-core', 'workflows', 'discuss-phase.md');
|
|
const powerWorkflowPath = path.join(__dirname, '..', 'msd-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/msd/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, '..', 'msd-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),
|
|
'msd-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
|
|
// `msd-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-MSD agent types — ALWAYS use msd-{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, '..', 'msd-core', 'workflows', 'discuss-phase', 'modes', 'advisor.md'
|
|
);
|
|
|
|
test('advisor mode spawns the registered msd-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="msd-advisor-researcher"'),
|
|
'advisor mode must spawn subagent_type="msd-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 @.../msd-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+@.*msd-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('msd-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('msd-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('msd-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('msd-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('msd-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('msd-core/workflows/discuss-phase.md');
|
|
const assumptions = read('msd-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)');
|
|
});
|
|
});
|
|
}
|