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.
169 lines
7.8 KiB
JavaScript
169 lines
7.8 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// plan-phase.md's planner prompt is the deployed runtime contract under assertion
|
|
// plan-phase.md is the deployed planning workflow contract; these checks lock
|
|
// the SPEC path wiring and quality-gate that the edge-probe review (RR-01/02/03)
|
|
// requires — assertions scope to extracted sub-blocks to avoid false positives.
|
|
|
|
'use strict';
|
|
|
|
process.env.MSD_TEST_MODE = '1';
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const PLAN_PHASE_PATH = path.join(__dirname, '..', 'msd-core', 'workflows', 'plan-phase.md');
|
|
const PROMPT_PATH = path.join(__dirname, '..', 'msd-core', 'templates', 'planner-subagent-prompt.md');
|
|
|
|
function readPlanPhase() {
|
|
return fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
|
|
}
|
|
|
|
// Extract the planner <downstream_consumer> block from plan-phase.md. The runtime planner is
|
|
// spawned from plan-phase.md's own inline <planning_context>/<downstream_consumer>, so the
|
|
// load-bearing lift instruction must live here — NOT in templates/planner-subagent-prompt.md,
|
|
// which nothing loads at runtime (no @-import in agents/msd-planner.md, no read in plan-phase.md).
|
|
function extractDownstreamConsumerBlock(content) {
|
|
const start = content.indexOf('<downstream_consumer>');
|
|
if (start === -1) return '';
|
|
const end = content.indexOf('</downstream_consumer>', start);
|
|
if (end === -1) return '';
|
|
return content.slice(start, end + '</downstream_consumer>'.length);
|
|
}
|
|
|
|
// Extract the planner <required_reading> block that contains {UI_SPEC_PATH}
|
|
// There are multiple <required_reading> blocks in plan-phase.md; we need the one
|
|
// at ~line 890-912 inside the planning_context markdown block.
|
|
function extractPlannerFilesBlock(content) {
|
|
let pos = 0;
|
|
while (true) {
|
|
const start = content.indexOf('<required_reading>', pos);
|
|
if (start === -1) return '';
|
|
const end = content.indexOf('</required_reading>', start);
|
|
if (end === -1) return '';
|
|
const block = content.slice(start, end + '</required_reading>'.length);
|
|
if (block.includes('{UI_SPEC_PATH}')) {
|
|
return block;
|
|
}
|
|
pos = end + 1;
|
|
}
|
|
}
|
|
|
|
// Extract the planner <quality_gate> block (the last one in plan-phase.md,
|
|
// inside the planner prompt template)
|
|
function extractQualityGateBlock(content) {
|
|
const start = content.lastIndexOf('<quality_gate>');
|
|
if (start === -1) return '';
|
|
const end = content.indexOf('</quality_gate>', start);
|
|
if (end === -1) return '';
|
|
return content.slice(start, end + '</quality_gate>'.length);
|
|
}
|
|
|
|
// Test A (RR-01): plan-phase.md resolves a phase *-SPEC.md into SPEC_FILE/SPEC_PATH
|
|
// Uses new RegExp to correctly match literal $ and ( characters in bash snippets.
|
|
// This MUST FAIL before the RR-01 fix (no SPEC_PATH resolution exists today)
|
|
test('RR-01: plan-phase.md resolves phase *-SPEC.md (excluding AI/UI variants) into SPEC_FILE/SPEC_PATH', () => {
|
|
const content = readPlanPhase();
|
|
|
|
// Assert the canonical SPEC_FILE resolution form is present.
|
|
// new RegExp used so that \$ and \( are treated as literal dollar-sign and open-paren
|
|
// (JS regex literals interpret \$ as end-anchor and \( as group open).
|
|
assert.match(
|
|
content,
|
|
new RegExp('SPEC_FILE=\\$\\(ls "\\$\\{[A-Z_]*PHASE_DIR[A-Z_]*\\}"[/][*]-SPEC\\.md'),
|
|
'plan-phase.md must resolve SPEC_FILE using ls "${...PHASE_DIR...}"/*-SPEC.md pattern'
|
|
);
|
|
|
|
// Assert {SPEC_PATH} token appears in the planner files_to_read block
|
|
const filesBlock = extractPlannerFilesBlock(content);
|
|
assert.match(
|
|
filesBlock,
|
|
/[{]SPEC_PATH[}]/,
|
|
'The planner <required_reading> block (containing {UI_SPEC_PATH}) must also contain {SPEC_PATH}'
|
|
);
|
|
});
|
|
|
|
// Test B (RR-01): The {SPEC_PATH} entry is labelled as carrying the ## Edge Coverage section
|
|
// This MUST FAIL before the RR-01 fix (no {SPEC_PATH} entry exists today)
|
|
test('RR-01: {SPEC_PATH} entry in files_to_read is labelled with Edge Coverage', () => {
|
|
const content = readPlanPhase();
|
|
const filesBlock = extractPlannerFilesBlock(content);
|
|
|
|
assert.match(
|
|
filesBlock,
|
|
/[{]SPEC_PATH[}][^\n]*Edge Coverage/,
|
|
'{SPEC_PATH} entry in planner files_to_read must be labelled as carrying the ## Edge Coverage section'
|
|
);
|
|
});
|
|
|
|
// Extract a "## " section from its heading until the next "## " heading (or EOF).
|
|
function extractSection(content, heading) {
|
|
const start = content.indexOf(heading);
|
|
if (start === -1) return '';
|
|
const next = content.indexOf('\n## ', start + heading.length);
|
|
return content.slice(start, next === -1 ? content.length : next);
|
|
}
|
|
|
|
// Test E (RR-01 REACHABILITY — the assertion that catches the original no-op):
|
|
// Token presence is not enough. The SPEC resolution must live on an UN-GATED path. §4.5
|
|
// "Check AI-SPEC" is skipped on every non-AI phase (ai_integration_phase_enabled false /
|
|
// --skip-ai-spec), so a resolution placed there leaves SPEC_PATH unbound and the planner
|
|
// never receives the SPEC — exactly the #550 silent no-op. Assert it is NOT in §4.5.
|
|
test('RR-01 reachability: SPEC_FILE resolution is NOT gated inside the AI-SPEC artifact section', () => {
|
|
const content = readPlanPhase();
|
|
const aiSpecSection = extractSection(content, '## 4.5. Resolve AI-SPEC Artifact');
|
|
const specResolution = new RegExp('SPEC_FILE=\\$\\(ls "\\$\\{[A-Z_]*PHASE_DIR[A-Z_]*\\}"[/][*]-SPEC\\.md');
|
|
assert.ok(aiSpecSection.length > 0, 'sanity: the §4.5 AI-SPEC artifact section must exist to scope this test');
|
|
assert.doesNotMatch(
|
|
aiSpecSection,
|
|
specResolution,
|
|
'SPEC_FILE resolution must NOT live inside the §4.5 AI-SPEC artifact block — gating it there silently starves the planner of the SPEC on non-AI phases (the original #550 no-op)'
|
|
);
|
|
assert.match(content, specResolution, 'SPEC_FILE resolution must still exist on an un-gated path elsewhere in plan-phase.md');
|
|
});
|
|
|
|
// Test C (RR-02 consumer end): the lift instruction lives in the RUNTIME planner surface.
|
|
// plan-phase.md spawns the planner from its own inline <planning_context>; the
|
|
// templates/planner-subagent-prompt.md file is orphaned (loaded by nothing), so asserting the
|
|
// contract there is false assurance — the test stays green even if the runtime never consumes it.
|
|
// Pin the contract to the <downstream_consumer> block plan-phase.md actually sends the planner.
|
|
test('RR-02 consumer: plan-phase.md downstream_consumer instructs lifting resolved edges into must_haves.truths', () => {
|
|
const block = extractDownstreamConsumerBlock(readPlanPhase());
|
|
|
|
assert.ok(block.length > 0, 'sanity: plan-phase.md must contain a <downstream_consumer> block to scope this test');
|
|
assert.match(
|
|
block,
|
|
/##\s*Edge Coverage/,
|
|
'plan-phase.md <downstream_consumer> must reference ## Edge Coverage'
|
|
);
|
|
assert.match(
|
|
block,
|
|
/must_haves\.truths/,
|
|
'plan-phase.md <downstream_consumer> must reference must_haves.truths as the lift target'
|
|
);
|
|
|
|
// Regression guard against re-orphaning: the lift instruction must NOT be relocated back into
|
|
// templates/planner-subagent-prompt.md (which nothing loads) and presented as the consumer
|
|
// contract — that is exactly the false-assurance this retarget fixes.
|
|
const prompt = fs.readFileSync(PROMPT_PATH, 'utf8');
|
|
assert.doesNotMatch(
|
|
prompt,
|
|
/Edge Coverage/,
|
|
'planner-subagent-prompt.md is not loaded at runtime — the Edge Coverage lift instruction must not live there'
|
|
);
|
|
});
|
|
|
|
// Test D (RR-03): plan-phase.md <quality_gate> contains a resolved-edge ↔ must_haves item
|
|
// #3132: vocabulary realigned from covered/backstop-as-status to resolved+verification
|
|
test('RR-03: planner quality_gate requires resolved edges represented in must_haves', () => {
|
|
const content = readPlanPhase();
|
|
const qgBlock = extractQualityGateBlock(content);
|
|
|
|
assert.match(
|
|
qgBlock,
|
|
/resolved.*edge.*must_haves/i,
|
|
'planner quality_gate must contain a checklist item tying resolved edges to must_haves'
|
|
);
|
|
});
|