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.
119 lines
6.7 KiB
JavaScript
119 lines
6.7 KiB
JavaScript
// allow-test-rule: source-text-is-the-product (see #1963)
|
|
// Agent .md + reference .md files — their text IS what the runtime loads.
|
|
// Testing text content tests the deployed prevention/postmortem contract.
|
|
// Per CONTRIBUTING.md exception matrix. Covers epic #1957 Phase 3B (#1963).
|
|
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const ROOT = process.cwd();
|
|
const AGENT = path.join(ROOT, 'agents/msd-debugger.md');
|
|
const SESSION_MGR = path.join(ROOT, 'agents/msd-debug-session-manager.md');
|
|
const REFERENCE = path.join(ROOT, 'msd-core/references/debugger-prevention.md');
|
|
|
|
describe('prevention / blameless-postmortem output (#1963, epic #1957 Phase 3B)', () => {
|
|
describe('reference extract exists and is wired into archive_session', () => {
|
|
test('msd-core/references/debugger-prevention.md exists', () => {
|
|
assert.ok(fs.existsSync(REFERENCE), 'debugger-prevention.md reference must exist');
|
|
});
|
|
|
|
test('msd-debugger.md @-includes the prevention reference', () => {
|
|
const content = fs.readFileSync(AGENT, 'utf8');
|
|
assert.ok(
|
|
content.includes('@~/.claude/msd-core/references/debugger-prevention.md'),
|
|
'msd-debugger.md must @-include the prevention reference from archive_session'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('the Prevention block has three blame-free components (criterion 1)', () => {
|
|
test('reference documents a blameless 5-Whys causal chain', () => {
|
|
const content = fs.readFileSync(REFERENCE, 'utf8');
|
|
assert.ok(/5-?\s*whys|five whys|causal chain/i.test(content), 'must document a 5-Whys causal chain');
|
|
assert.ok(/blame|blameless|blame-free/i.test(content), 'must state the chain is blame-free/blameless');
|
|
});
|
|
|
|
test('the 5-Whys branches per RCA (Phase 2A), not a single-cause chain', () => {
|
|
const content = fs.readFileSync(REFERENCE, 'utf8');
|
|
assert.ok(/branch/i.test(content), 'the postmortem must branch (not chain) per the Phase 2A RCA discipline');
|
|
assert.ok(/single[\s-]?cause|agent error|why was that (?:error|possible)/i.test(content),
|
|
'must treat "agent error" as a prompt for "why was that error possible?" (blame-free), not a terminal cause');
|
|
});
|
|
|
|
test('reference documents the "why wasn\'t this caught?" question', () => {
|
|
const content = fs.readFileSync(REFERENCE, 'utf8');
|
|
assert.ok(/why (?:wasn'?t|was not|didn'?t).*(?:caught|prevent)/i.test(content) || /why[\s-]?not[\s-]?caught/i.test(content),
|
|
'must document the "why wasn\'t this caught?" question (which existing gate missed it)');
|
|
});
|
|
|
|
test('reference documents the recurrence-guard taxonomy', () => {
|
|
const content = fs.readFileSync(REFERENCE, 'utf8');
|
|
assert.ok(/recurrence guard|recurrence-guard/i.test(content), 'must name the recurrence guard');
|
|
// The concrete artifact options from the issue
|
|
assert.ok(/regression test/i.test(content), 'guard option: regression test');
|
|
assert.ok(/lint rule|assertion|precondition/i.test(content), 'guard option: lint rule / assertion / precondition');
|
|
});
|
|
});
|
|
|
|
describe('knowledge-base entry stores the prevention fields (criterion 2)', () => {
|
|
test('KB entry format in msd-debugger.md includes why_not_caught + recurrence_guard', () => {
|
|
const content = fs.readFileSync(AGENT, 'utf8');
|
|
assert.ok(/why.not.caught/i.test(content), 'KB entry format must include a why_not_caught field');
|
|
assert.ok(/recurrence.guard/i.test(content), 'KB entry format must include a recurrence_guard field');
|
|
});
|
|
|
|
test('reference documents backward compatibility (old entries without the fields still load)', () => {
|
|
const content = fs.readFileSync(REFERENCE, 'utf8');
|
|
assert.ok(/backward|back.compat|old entries|without.*fields.*load|absent.*field/i.test(content),
|
|
'must document that old KB entries without the new fields still load (additive, no format break)');
|
|
});
|
|
});
|
|
|
|
describe('session-manager compact return surfaces the prevention summary (criterion 3)', () => {
|
|
test('msd-debug-session-manager.md compact summary includes a prevention line', () => {
|
|
const content = fs.readFileSync(SESSION_MGR, 'utf8');
|
|
assert.ok(/prevention|recurrence guard|why not caught/i.test(content),
|
|
'session-manager compact summary must surface the prevention summary (recurrence guard / why-not-caught)');
|
|
});
|
|
});
|
|
|
|
describe('scope boundary (Zawinski) — a block, not an incident-management subsystem', () => {
|
|
test('reference states the prevention block reuses existing structures and adds no subsystem', () => {
|
|
const content = fs.readFileSync(REFERENCE, 'utf8');
|
|
assert.ok(/reuse|existing|no(?:t| new).*(?:subsystem|framework|command)|block, not/i.test(content),
|
|
'must state the prevention block reuses the existing debug file + knowledge base (no new subsystem)');
|
|
});
|
|
});
|
|
|
|
describe('cross-section parity (guards the Critical: Entry-Format fields must also appear in the archive append template)', () => {
|
|
test('every Entry-Format KB field appears in the archive_session append template', () => {
|
|
// DEFECT.GENERATIVE-FIX guard: the <knowledge_base_protocol> Entry Format
|
|
// and the archive_session append template are two parallel surfaces that
|
|
// must agree. A field declared in one but not the other is dead data.
|
|
const content = fs.readFileSync(AGENT, 'utf8');
|
|
const requiredFields = ['Date', 'Error patterns', 'Root cause', 'Fix', 'Files changed', 'Why not caught', 'Recurrence guard'];
|
|
for (const field of requiredFields) {
|
|
const matches = content.match(new RegExp(`\\*\\*${field}`, 'gi'));
|
|
assert.ok(
|
|
matches && matches.length >= 2,
|
|
`KB field "${field}" must appear in BOTH the Entry Format and the archive append template (found ${matches ? matches.length : 0} occurrence(s)) — parallel-surface drift`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('Phase 0 consumes the prevention fields (why_not_caught + recurrence_guard) when present', () => {
|
|
const content = fs.readFileSync(AGENT, 'utf8');
|
|
// Anchor on the specific Phase 0 heading in investigation_loop (the
|
|
// string "Phase 0" also appears in knowledge_base_protocol prose, which
|
|
// would match the wrong region).
|
|
const phase0Block = content.match(/\*\*Phase 0: Check knowledge base\*\*[\s\S]{0,1500}/);
|
|
assert.ok(phase0Block, 'a Phase 0: Check knowledge base block must exist in investigation_loop');
|
|
assert.ok(/why.?not.?caught/i.test(phase0Block[0]) && /recurrence.?guard/i.test(phase0Block[0]),
|
|
'the Phase 0 Evidence line must include why_not_caught + recurrence_guard (consume when present, absent on old entries)');
|
|
});
|
|
});
|
|
});
|