* 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)
179 lines
8.2 KiB
JavaScript
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)`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|