Files
msd-core/tests/adr-22-plan-drift-guard.test.cjs
Tom Boucher 55eb1fd72e refactor(#1190): extract ADR-22 drift-guard decision logic into a testable seam (#1242)
* refactor(#1190): extract ADR-22 drift-guard decision logic into a testable seam

ADR-22's severity mapping, authority auto-upgrade, and rung>=3 hard-block lived only as prose in plan-review-convergence.md — untestable. Extracted into src/plan-drift-guard.cts (pure: AUTHORITY_RUNGS, getEffectiveAuthority, classifyDriftSeverity) + a gsd-tools drift-guard CLI seam (authority/severity), and rewired the workflow to call the seam deterministically instead of reasoning the decision in prose. 47 unit/e2e/structural tests cover the full severity table, the grep->intel auto-upgrade, and the rung>=3 HIGH hard-block.

Closes #1190

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1190): add changeset for ADR-22 drift-guard seam (#1242)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1190): register ADR-22 module in eslint-ignore + inventory manifest + docs-exempt changeset

Full-matrix CI surfaced new-module/command governance ripples beyond the lint-tests chain: (1) tsc-generated plan-drift-guard.cjs must be in the eslint ignore list (551-eslint-bin-lib-coverage); (2) docs/INVENTORY-MANIFEST.json must include the new module/command (regen via gen-inventory-manifest.cjs --write); (3) a type:Added changeset triggers docs-required — added a docs-exempt marker (internal seam, no user-facing surface).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:52:36 -04:00

329 lines
12 KiB
JavaScript

// allow-test-rule: source-text-is-the-product #1190
/**
* ADR-22 Drift-Guard Tests — issue #1190
*
* Covers:
* 1. Pure unit tests for `classifyDriftSeverity` (every ADR-22 table cell).
* 2. Pure unit tests for `getEffectiveAuthority` (auto-upgrade + pass-through).
* 3. e2e CLI tests via `gsd-tools drift-guard severity/authority`.
* 4. Structural test that plan-review-convergence.md invokes `gsd_run drift-guard`.
*/
'use strict';
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { cleanup, runGsdTools } = require('./helpers.cjs');
// ── Pure-module imports ──────────────────────────────────────────────────────
const {
AUTHORITY_RUNGS,
getEffectiveAuthority,
classifyDriftSeverity,
} = require('../gsd-core/bin/lib/plan-drift-guard.cjs');
// ── 1. AUTHORITY_RUNGS sanity ────────────────────────────────────────────────
describe('AUTHORITY_RUNGS', () => {
test('has all five adapters with correct rung order', () => {
assert.equal(AUTHORITY_RUNGS.grep, 0);
assert.equal(AUTHORITY_RUNGS.intel, 1);
assert.equal(AUTHORITY_RUNGS.treesitter, 2);
assert.equal(AUTHORITY_RUNGS.lsp, 3);
assert.equal(AUTHORITY_RUNGS.scip, 4);
});
test('is frozen (no mutation)', () => {
assert.ok(Object.isFrozen(AUTHORITY_RUNGS));
});
});
// ── 2. getEffectiveAuthority unit tests ──────────────────────────────────────
describe('getEffectiveAuthority', () => {
test('grep + intel enabled → intel', () => {
assert.equal(getEffectiveAuthority('grep', true), 'intel');
});
test('grep + intel disabled → grep', () => {
assert.equal(getEffectiveAuthority('grep', false), 'grep');
});
test('undefined + intel enabled → intel (grep is the default)', () => {
assert.equal(getEffectiveAuthority(undefined, true), 'intel');
});
test('null + intel disabled → grep', () => {
assert.equal(getEffectiveAuthority(null, false), 'grep');
});
test('empty string + intel enabled → intel', () => {
assert.equal(getEffectiveAuthority('', true), 'intel');
});
test('intel + intel enabled → intel (no double upgrade)', () => {
// intel is already intel; auto-upgrade rule only applies to grep
assert.equal(getEffectiveAuthority('intel', true), 'intel');
});
test('intel + intel disabled → intel (pass-through)', () => {
assert.equal(getEffectiveAuthority('intel', false), 'intel');
});
test('treesitter + intel enabled → treesitter (auto-upgrade only for grep)', () => {
assert.equal(getEffectiveAuthority('treesitter', true), 'treesitter');
});
test('lsp + intel enabled → lsp (auto-upgrade only for grep)', () => {
assert.equal(getEffectiveAuthority('lsp', true), 'lsp');
});
test('scip + intel disabled → scip', () => {
assert.equal(getEffectiveAuthority('scip', false), 'scip');
});
test('unknown authority → TypeError', () => {
assert.throws(
() => getEffectiveAuthority('grok', false),
(err) => err instanceof TypeError && /Unknown authority/i.test(err.message),
);
});
});
// ── 3. classifyDriftSeverity unit tests (every ADR-22 table cell) ──────────
describe('classifyDriftSeverity — VERIFIED', () => {
for (const authority of ['grep', 'intel', 'treesitter', 'lsp', 'scip']) {
test(`VERIFIED @ ${authority} → severity none, no hardBlock`, () => {
const result = classifyDriftSeverity({ status: 'VERIFIED', authority });
assert.equal(result.severity, 'none');
assert.equal(result.hardBlock, false);
});
}
});
describe('classifyDriftSeverity — MISSING', () => {
test('MISSING @ grep → needs-acknowledgement, no hardBlock', () => {
const result = classifyDriftSeverity({ status: 'MISSING', authority: 'grep' });
assert.equal(result.severity, 'needs-acknowledgement');
assert.equal(result.hardBlock, false);
});
test('MISSING @ intel → needs-acknowledgement, no hardBlock', () => {
const result = classifyDriftSeverity({ status: 'MISSING', authority: 'intel' });
assert.equal(result.severity, 'needs-acknowledgement');
assert.equal(result.hardBlock, false);
});
test('MISSING @ treesitter → needs-acknowledgement, no hardBlock', () => {
const result = classifyDriftSeverity({ status: 'MISSING', authority: 'treesitter' });
assert.equal(result.severity, 'needs-acknowledgement');
assert.equal(result.hardBlock, false);
});
test('MISSING @ lsp → HIGH, hardBlock TRUE', () => {
const result = classifyDriftSeverity({ status: 'MISSING', authority: 'lsp' });
assert.equal(result.severity, 'HIGH');
assert.equal(result.hardBlock, true);
});
test('MISSING @ scip → HIGH, hardBlock TRUE', () => {
const result = classifyDriftSeverity({ status: 'MISSING', authority: 'scip' });
assert.equal(result.severity, 'HIGH');
assert.equal(result.hardBlock, true);
});
});
describe('classifyDriftSeverity — AMBIGUOUS', () => {
for (const authority of ['grep', 'intel', 'treesitter', 'lsp', 'scip']) {
test(`AMBIGUOUS @ ${authority} → MEDIUM, no hardBlock`, () => {
const result = classifyDriftSeverity({ status: 'AMBIGUOUS', authority });
assert.equal(result.severity, 'MEDIUM');
assert.equal(result.hardBlock, false);
});
}
});
describe('classifyDriftSeverity — UNCHECKABLE', () => {
for (const authority of ['grep', 'intel', 'treesitter', 'lsp', 'scip']) {
test(`UNCHECKABLE @ ${authority} → INFO, no hardBlock`, () => {
const result = classifyDriftSeverity({ status: 'UNCHECKABLE', authority });
assert.equal(result.severity, 'INFO');
assert.equal(result.hardBlock, false);
});
}
});
describe('classifyDriftSeverity — validation', () => {
test('unknown status → TypeError', () => {
assert.throws(
() => classifyDriftSeverity({ status: 'WRONG', authority: 'grep' }),
(err) => err instanceof TypeError && /Unknown status/i.test(err.message),
);
});
test('unknown authority → TypeError', () => {
assert.throws(
() => classifyDriftSeverity({ status: 'MISSING', authority: 'magic' }),
(err) => err instanceof TypeError && /Unknown authority/i.test(err.message),
);
});
});
// ── 4. e2e CLI tests ─────────────────────────────────────────────────────────
describe('gsd-tools drift-guard — CLI e2e', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-drift-guard-'));
const planningDir = path.join(tmpDir, '.planning');
fs.mkdirSync(planningDir, { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
// Helper: write config.json into the fixture
function writeConfig(cfg) {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify(cfg),
);
}
test('severity --status MISSING --authority lsp → {severity:HIGH, hardBlock:true}', () => {
writeConfig({ plan_review: { source_grounding_authority: 'lsp' } });
const res = runGsdTools(
['drift-guard', 'severity', '--status', 'MISSING', '--authority', 'lsp', '--raw'],
tmpDir,
);
assert.ok(res.success, `Expected success, got: ${res.error}`);
const result = JSON.parse(res.output);
assert.equal(result.severity, 'HIGH');
assert.equal(result.hardBlock, true);
});
test('severity --status MISSING --authority grep → {severity:needs-acknowledgement, hardBlock:false}', () => {
writeConfig({});
const res = runGsdTools(
['drift-guard', 'severity', '--status', 'MISSING', '--authority', 'grep', '--raw'],
tmpDir,
);
assert.ok(res.success, `Expected success, got: ${res.error}`);
const result = JSON.parse(res.output);
assert.equal(result.severity, 'needs-acknowledgement');
assert.equal(result.hardBlock, false);
});
test('severity --status VERIFIED --authority scip → {severity:none, hardBlock:false}', () => {
writeConfig({});
const res = runGsdTools(
['drift-guard', 'severity', '--status', 'VERIFIED', '--authority', 'scip', '--raw'],
tmpDir,
);
assert.ok(res.success, `Expected success, got: ${res.error}`);
const result = JSON.parse(res.output);
assert.equal(result.severity, 'none');
assert.equal(result.hardBlock, false);
});
test('authority with source_grounding_authority=grep + intel.enabled=true → intel', () => {
writeConfig({
plan_review: { source_grounding_authority: 'grep' },
intel: { enabled: true },
});
const res = runGsdTools(
['drift-guard', 'authority', '--raw'],
tmpDir,
);
assert.ok(res.success, `Expected success, got: ${res.error}`);
assert.equal(res.output, 'intel');
});
test('authority with source_grounding_authority=lsp + intel.enabled=true → lsp (no upgrade)', () => {
writeConfig({
plan_review: { source_grounding_authority: 'lsp' },
intel: { enabled: true },
});
const res = runGsdTools(
['drift-guard', 'authority', '--raw'],
tmpDir,
);
assert.ok(res.success, `Expected success, got: ${res.error}`);
assert.equal(res.output, 'lsp');
});
test('authority with no config → grep (default)', () => {
writeConfig({});
const res = runGsdTools(
['drift-guard', 'authority', '--raw'],
tmpDir,
);
assert.ok(res.success, `Expected success, got: ${res.error}`);
assert.equal(res.output, 'grep');
});
test('severity without --status flag → exits non-zero', () => {
writeConfig({});
const res = runGsdTools(['drift-guard', 'severity', '--raw'], tmpDir);
assert.equal(res.success, false, 'Expected non-zero exit for missing --status');
assert.ok(res.exitCode !== 0, `exitCode should be non-zero, got ${res.exitCode}`);
});
test('unknown subcommand → exits non-zero', () => {
writeConfig({});
const res = runGsdTools(['drift-guard', 'badcmd', '--raw'], tmpDir);
assert.equal(res.success, false, 'Expected non-zero exit for unknown subcommand');
assert.ok(res.exitCode !== 0, `exitCode should be non-zero, got ${res.exitCode}`);
});
});
// ── 5. Structural test: plan-review-convergence.md invokes gsd_run drift-guard
describe('plan-review-convergence.md uses gsd_run drift-guard seam', () => {
const WORKFLOW_PATH = path.join(
__dirname, '..', 'gsd-core', 'workflows', 'plan-review-convergence.md',
);
test('workflow contains gsd_run drift-guard authority call', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(
content.includes('gsd_run drift-guard authority'),
'plan-review-convergence.md must contain: gsd_run drift-guard authority',
);
});
test('workflow drift-guard authority call includes --raw (prevents JSON-quoted capture)', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.match(
content,
/gsd_run drift-guard authority --raw/,
'plan-review-convergence.md authority capture must use --raw; without it the value is JSON-quoted ("intel") and --authority rejects it as unknown',
);
});
test('workflow contains gsd_run drift-guard severity call', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(
content.includes('gsd_run drift-guard severity'),
'plan-review-convergence.md must contain: gsd_run drift-guard severity',
);
});
test('workflow drift-guard severity call passes --authority flag', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.match(
content,
/gsd_run drift-guard severity[^\n]*--authority/,
'plan-review-convergence.md severity invocation must pass --authority so the resolved authority is forwarded to classifyDriftSeverity',
);
});
});