Files
msd-core/tests/debugger-bug-taxonomy.test.cjs
Tom Boucher 6baa2a8182 feat(#1961): add bug-taxonomy classification + strategy routing to gsd-debugger (#2407)
* test(#1961): add failing-first bug-taxonomy routing contract tests

Epic #1957 Phase 2B. Source-text-is-the-product contract tests (3 taxonomy
classes, explicit class->technique routing table, Bohrbug->repro+SBFL+bisect,
Heisenbug->record-replay/stability+SKIP-SBFL, Concurrency->atomicity/order/
deadlock checklist, bug_class in DEBUG Current Focus, supersede-not-append)
plus a routing-table specification object pinning the documented decisions
(SBFL forbidden on Heisenbug is the load-bearing 1B/2B seam).

Failing-first: reference, Phase 1.75, and routing-table reframe do not yet exist.

* feat(#1961): add bug-taxonomy classification + strategy routing to gsd-debugger

Epic #1957 Phase 2B (reliability-critical). Adds Phase 1.75: classify the
failure as Bohrbug / Heisenbug-Mandelbug / Concurrency, then route the
investigation technique via an explicit class->technique table (Kernighan: no
opaque heuristic). Bohrbug -> reproduction + SBFL (Phase 1.25) + git bisect;
Heisenbug/Mandelbug -> record-replay (rr) + stability-stress + statistical
sampling, with SBFL explicitly SKIPPED (a flaky spectrum poisons the Ochiai
ranking — the load-bearing 1B/2B seam); Concurrency -> the
atomicity/order/deadlock checklist first.

Reframes (supersedes, not appends — Zawinski) the flat 'Technique Selection by
situation' table into a class-routed table; the 11 techniques remain as routed
targets. bug_class recorded in Current Focus (DEBUG template); common-bug-
patterns catalog cross-referenced to the taxonomy.

Full rules extracted to gsd-core/references/debugger-bug-taxonomy.md. INVENTORY
+ manifest + agent-size baseline + install-parity goldens + AGENTS.md updated.

* fix(#1961): address orthogonal review (phase-name drift, General lane, revoke framing, row-scoped tests, bounding)

- HIGH: reference said 'Phase 1B' (epic shorthand); corrected to the deployed
  'Phase 1.25' (matches the agent + SBFL reference).
- HIGH: 6 of 11 techniques (Rubber duck, Delta, Working backwards,
  Differential, Comment-out, Follow-the-indirection) were orphaned by the
  situation-table reframe. Added a 'General (any class, situation-cued)'
  lane to BOTH the reference routing table and the agent's Technique
  Selection table that re-homes them — supersede-not-append now holds.
- MEDIUM: the SBFL-skip is structurally retroactive (Phase 1.25 runs before
  Phase 1.75 classification), so reframed the table column from 'Do NOT use'
  to 'Revoke if already run' + an explicit 'retroactive revocation, not
  proactive skip' note stating the ordering honestly.
- MEDIUM: contract tests are now row-scoped (parse the table by class, assert
  per-row) instead of presence-only; added a guard that the previously-
  orphaned techniques now have a General-lane route.
- LOW: pinned the canonical bug_class value form (lowercase-kebab:
  bohrbug|heisenbug-mandelbug|concurrency; prose may use title-case).
- NIT: added a 'Bound the Heisenbug-chase runs' note (rr/stability/sampling
  timeouts) per the unbounded-subprocess gauntlet.

* chore(#1961): backfill changeset pr number (PR #2407)
2026-07-18 14:42:29 -04:00

179 lines
8.2 KiB
JavaScript

// allow-test-rule: source-text-is-the-product (see #1961)
// Agent .md + reference .md + template .md files — their text IS what the
// runtime loads. Testing text content tests the deployed bug-taxonomy routing
// contract. The pure-JS routing-table specification below pins the key
// routing decisions (Bohrbug->SBFL; Heisenbug->!SBFL; Concurrency->checklist).
// Per CONTRIBUTING.md exception matrix. Covers epic #1957 Phase 2B (#1961).
'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/gsd-debugger.md');
const REFERENCE = path.join(ROOT, 'gsd-core/references/debugger-bug-taxonomy.md');
const DEBUG_TEMPLATE = path.join(ROOT, 'gsd-core/templates/DEBUG.md');
// The documented routing table, as a specification. The Tier-1 source-text
// contract tests below carry the real enforcement that the reference's table
// matches this; this object pins the key routing decisions a reviewer cares
// about (the SBFL-on-flaky-skip is the load-bearing one — it's the 1B/2B seam).
const ROUTING = {
bohrbug: {
recommended: ['deterministic reproduction', 'SBFL', 'git bisect', 'binary search'],
forbidden: [],
},
heisenbug: {
recommended: ['record-replay', 'stability-stress', 'statistical sampling'],
forbidden: ['SBFL'], // a flaky spectrum poisons failed(s) — SBFL must be skipped
},
concurrency: {
recommended: ['atomicity', 'order', 'deadlock'],
forbidden: [],
},
};
describe('bug-taxonomy classification + strategy routing (#1961, epic #1957 Phase 2B)', () => {
describe('reference extract exists and is wired in', () => {
test('gsd-core/references/debugger-bug-taxonomy.md exists', () => {
assert.ok(fs.existsSync(REFERENCE), 'debugger-bug-taxonomy.md reference must exist');
});
test('gsd-debugger.md @-includes the bug-taxonomy reference', () => {
const content = fs.readFileSync(AGENT, 'utf8');
assert.ok(
content.includes('@~/.claude/gsd-core/references/debugger-bug-taxonomy.md'),
'gsd-debugger.md must @-include the bug-taxonomy reference (from Phase 1.75 or the technique-selection table)'
);
});
});
describe('the taxonomy is documented (criterion: every session records a bug_class)', () => {
test('reference defines the three classes', () => {
const content = fs.readFileSync(REFERENCE, 'utf8');
assert.ok(/bohrbug/i.test(content), 'class: Bohrbug');
assert.ok(/heisenbug|mandelbug/i.test(content), 'class: Heisenbug/Mandelbug');
assert.ok(/concurrency/i.test(content), 'class: Concurrency');
});
test('DEBUG.md template Current Focus records bug_class', () => {
const content = fs.readFileSync(DEBUG_TEMPLATE, 'utf8');
assert.ok(/bug_class/.test(content), 'DEBUG.md Current Focus must carry a bug_class field');
});
});
describe('routing is an explicit, inspectable table (criterion 4)', () => {
// Row-scoped: parse the markdown table and assert per-row, so a substring
// appearing in the wrong cell (e.g. SBFL only in the Heisenbug revoke
// column) cannot satisfy the Bohrbug route assertion.
function tableRows() {
const content = fs.readFileSync(REFERENCE, 'utf8');
const rows = [];
for (const line of content.split(/\r?\n/)) {
if (/^\|/.test(line) && !/^\|[\s|-]+\|?$/.test(line) && (line.match(/\|/g) || []).length >= 3) {
rows.push(line.toLowerCase());
}
}
return rows;
}
function rowFor(cls) {
return tableRows().find((r) => r.includes(cls.toLowerCase())) || null;
}
test('there is a markdown table with class + route columns', () => {
const content = fs.readFileSync(REFERENCE, 'utf8');
assert.ok(/rout/i.test(content), 'the word route/routing must appear');
assert.ok(/\|.*\|.*\|/m.test(content), 'there must be a markdown table (inspectable)');
assert.ok(tableRows().length >= 3, 'routing table must have at least 3 class rows');
});
test('a Bohrbug row routes to reproduction + SBFL + bisection (criterion 2, row-scoped)', () => {
const row = rowFor('bohrbug');
assert.ok(row, 'there must be a Bohrbug row in the routing table');
assert.ok(/reproduc/.test(row), 'Bohrbug row must include reproduction');
assert.ok(/sbfl/.test(row), 'Bohrbug row must include SBFL (Phase 1.25, not the epic shorthand Phase 1B)');
assert.ok(/bisect/.test(row), 'Bohrbug row must include git bisect');
});
test('a Heisenbug row routes to record-replay/stability and revokes SBFL (row-scoped, load-bearing 1B/2B seam)', () => {
const row = rowFor('heisenbug');
assert.ok(row, 'there must be a Heisenbug/Mandelbug row in the routing table');
assert.ok(/record-replay|\brr\b|stability/.test(row),
'Heisenbug row must include record-replay (rr) and/or stability-stress');
assert.ok(/sbfl/.test(row), 'Heisenbug row must reference SBFL (in the revoke column)');
assert.ok(/revoke|revocat/.test(row),
'Heisenbug row must state SBFL is REVOKED if already run (Phase 1.25 precedes classification)');
});
test('a Concurrency row surfaces the atomicity/order/deadlock checklist (criterion 3, row-scoped)', () => {
const row = rowFor('concurrency');
assert.ok(row, 'there must be a Concurrency row in the routing table');
assert.ok(/atomicity/.test(row), 'Concurrency row must include atomicity');
assert.ok(/\border\b/.test(row), 'Concurrency row must include order');
assert.ok(/deadlock/.test(row), 'Concurrency row must include deadlock');
});
});
describe('supersede, not append — no technique is orphaned (criterion 4)', () => {
test('the previously-situational techniques now have a General-lane route', () => {
// These 6 were orphaned when the flat situation table was reframed; the
// General lane re-homes them. Guards against the "supersede" claim
// silently dropping them.
const ref = fs.readFileSync(REFERENCE, 'utf8').toLowerCase();
const previouslyOrphaned = [
'rubber duck', 'delta debugging', 'working backwards',
'differential', 'comment out everything', 'follow the indirection',
];
for (const t of previouslyOrphaned) {
assert.ok(
ref.includes(t),
`technique "${t}" must appear in the bug-taxonomy reference (General lane) — was orphaned by the reframe`
);
}
});
test('the reference states the flat menu is superseded by class-routed selection', () => {
const content = fs.readFileSync(REFERENCE, 'utf8');
assert.ok(/replace|supersede|routed? selection|not.*append/i.test(content),
'must state the flat menu is replaced/superseded by class-routed selection (not merely appended)');
});
});
describe('routing-table specification (pins the documented decisions)', () => {
// Honest framing: this object is a SPEC of the reference's routing table,
// not production code. The Tier-1 source-text tests above enforce that the
// reference's table actually matches these decisions.
test('SBFL is recommended for Bohrbug', () => {
assert.ok(ROUTING.bohrbug.recommended.includes('SBFL'));
assert.ok(!ROUTING.bohrbug.forbidden.includes('SBFL'));
});
test('SBFL is FORBIDDEN for Heisenbug (flaky spectrum — the load-bearing 1B/2B constraint)', () => {
assert.ok(ROUTING.heisenbug.forbidden.includes('SBFL'),
'SBFL must not be used on a Heisenbug spectrum (poisons failed(s))');
assert.ok(!ROUTING.heisenbug.recommended.includes('SBFL'));
});
test('Concurrency route includes all three of atomicity/order/deadlock', () => {
for (const c of ['atomicity', 'order', 'deadlock']) {
assert.ok(ROUTING.concurrency.recommended.includes(c),
`concurrency route must include ${c}`);
}
});
test('no class both recommends AND forbids the same technique (internal consistency)', () => {
for (const cls of Object.keys(ROUTING)) {
for (const tech of ROUTING[cls].recommended) {
assert.ok(!ROUTING[cls].forbidden.includes(tech),
`${cls} both recommends and forbids ${tech} (contradiction)`);
}
}
});
});
});