Files
msd-core/tests/ci-test-scope.test.cjs
Tom Boucher cdd78bd2aa fix(#670): self-healing recovery for installer-migration checksum drift (#675)
Editing the body of an already-released installer migration drifts its computed
checksum (it hashes plan.toString()). The integrity guard then hard-aborted
every prior install on upgrade with "applied migration checksum changed" — a
100% reproducible blocker (v1.3.0, all platforms).

Already-applied migrations are filtered out of `pending` and never re-run, so
a drifted checksum is functionally inert. ADR-0008 anticipates checksum-mismatch
state as something the install-state layer must handle gracefully (plan -> apply
-> recover/report), not abort on.

This supersedes the published-checksum allowlist merged in #674 (per-release
maintenance debt — every historical checksum hand-pinned, still throws for any
unregistered value) with a general, self-healing recovery:

- Replace the throwing guard with non-fatal `collectAppliedChecksumDrift`,
  surfaced on `plan.checksumDrift`.
- Reconcile drifted stored checksums durably on the next state write
  (`reconcileDriftedChecksums`), idempotently (no perpetual writes).
- Relocate the "shipped migration bodies are immutable" rule to a CI baseline
  test that locks every shipped migration's checksum and fails on body drift —
  where #615 should have been caught, instead of blocking users.

Removes #674's legacyChecksums field, per-migration checksum pins,
published-checksums.json fixture, and compat test.

Fixes #670

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 13:44:05 -04:00

152 lines
7.1 KiB
JavaScript

'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('child_process');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const SCRIPT = path.join(ROOT, 'scripts', 'ci-test-scope.cjs');
function scopeFor(files) {
const r = spawnSync(process.execPath, [SCRIPT, '--files', files.join(' ')], {
cwd: ROOT,
encoding: 'utf8',
});
assert.strictEqual(r.status, 0, `stderr: ${r.stderr}\nstdout: ${r.stdout}`);
return JSON.parse(r.stdout);
}
describe('ci-test-scope.cjs', () => {
test('docs-only changes mark code_changed and select docs-parity (new correct contract)', () => {
const result = scopeFor(['docs/usage.md']);
assert.strictEqual(result.code_changed, true);
assert.strictEqual(result.full_matrix, false);
assert.ok(
result.targeted_tests.some(t => t.includes('docs-parity-live-registry')),
`expected docs-parity-live-registry in targeted_tests, got: ${JSON.stringify(result.targeted_tests)}`,
);
});
test('workflow changes request full matrix and workflow contract tests', () => {
const result = scopeFor(['.github/workflows/test.yml']);
assert.strictEqual(result.code_changed, true);
assert.strictEqual(result.full_matrix, true);
assert.ok(result.targeted_tests.includes('tests/workflow-shell-pinning.test.cjs'));
assert.ok(result.targeted_tests.includes('tests/release-tarball-smoke-workflow.test.cjs'));
assert.ok(result.windows_tests.includes('tests/workflow-shell-pinning.test.cjs'));
});
test('command changes request command tests without full parity matrix', () => {
const result = scopeFor(['commands/gsd/plan-phase.md']);
assert.strictEqual(result.code_changed, true);
assert.strictEqual(result.full_matrix, false);
assert.ok(result.targeted_tests.includes('tests/command-contract.test.cjs'));
assert.ok(result.targeted_tests.includes('tests/commands.test.cjs'));
});
test('changed test files are selected directly', () => {
const result = scopeFor(['tests/run-tests-harness.test.cjs']);
assert.strictEqual(result.code_changed, true);
assert.ok(result.targeted_tests.includes('tests/run-tests-harness.test.cjs'));
});
test('installer-sensitive changes request full matrix and install tests', () => {
const result = scopeFor(['bin/gsd']);
assert.strictEqual(result.code_changed, true);
assert.strictEqual(result.full_matrix, true);
assert.ok(result.targeted_tests.includes('tests/install.test.cjs'));
assert.ok(result.targeted_tests.includes('tests/release-tarball-smoke.install.test.cjs'));
});
test('missing required CLI values fail with usage', () => {
const r = spawnSync(process.execPath, [SCRIPT, '--files'], {
cwd: ROOT,
encoding: 'utf8',
});
assert.notStrictEqual(r.status, 0);
// allow-test-rule: CLI usage failure text is user-facing contract for this parser guard.
assert.match(r.stderr, /--files requires a value/);
// allow-test-rule: CLI usage banner presence is a user-facing contract.
assert.match(r.stderr, /Usage:/);
});
// bug-408: unconditional DEFAULT_SMOKE_TESTS injection removed; unit fallback added
test('bug-408: code change with matched rules produces exactly the rule-selected tests (no smoke list appended)', () => {
// commands/ matches the "command definitions" rule only — no smoke list should be added
const result = scopeFor(['commands/gsd/plan-phase.md']);
assert.strictEqual(result.code_changed, true);
const expectedTests = [
'tests/command-contract.test.cjs',
'tests/command-routing-hub.test.cjs',
'tests/commands.test.cjs',
'tests/phase-command-router.test.cjs',
'tests/roadmap-command-router.test.cjs',
];
// Every expected test must be present
for (const t of expectedTests) {
assert.ok(result.targeted_tests.includes(t), `expected ${t} in targeted_tests`);
}
// No DEFAULT_SMOKE_TESTS files should be injected beyond what the rule selects.
// The former smoke list contained package-manifest.test.cjs and core.test.cjs —
// neither is in the "command definitions" rule, so they must not appear.
assert.ok(!result.targeted_tests.includes('tests/core.test.cjs'),
'tests/core.test.cjs must NOT be unconditionally injected for command changes');
assert.ok(!result.targeted_tests.includes('tests/package-manifest.test.cjs'),
'tests/package-manifest.test.cjs must NOT be unconditionally injected for command changes');
});
test('bug-408: code change with no rule match falls back to unit suite token', () => {
// A plain source file that matches no RULES entry but is under gsd-core/ (code path)
const result = scopeFor(['gsd-core/src/some-util.js']);
assert.strictEqual(result.code_changed, true);
// allow-test-rule: the unit-fallback contract is the exact subject of bug #408.
assert.deepStrictEqual(result.targeted_tests, ['unit'],
'targeted_tests must be [\'unit\'] when code changed but no rule matched');
});
});
describe('ci-test-scope superset invariant (#494)', () => {
// Facet A: any tests/** change → full_matrix === true
test('A1: a specific changed test file forces full_matrix', () => {
const result = scopeFor(['tests/bug-1974-context-exhaustion-record.test.cjs']);
assert.strictEqual(result.full_matrix, true,
`expected full_matrix=true for tests/** change, got: ${JSON.stringify(result)}`);
});
test('A2: any tests/** path forces full_matrix', () => {
const result = scopeFor(['tests/some-new.test.cjs']);
assert.strictEqual(result.full_matrix, true,
`expected full_matrix=true for tests/** change, got: ${JSON.stringify(result)}`);
});
// Facet B: docs/**, commands/**, agents/** → code_changed AND docs-parity selected
test('B1: docs/adr change marks code_changed and selects docs-parity-live-registry', () => {
const result = scopeFor(['docs/adr/22-plan-drift-guard.md']);
assert.strictEqual(result.code_changed, true,
`expected code_changed=true for docs/** change, got: ${JSON.stringify(result)}`);
assert.ok(
result.targeted_tests.some(t => t.includes('docs-parity-live-registry')),
`expected docs-parity-live-registry in targeted_tests, got: ${JSON.stringify(result.targeted_tests)}`,
);
});
test('B2: docs locale dir change marks code_changed and selects docs-parity-live-registry', () => {
const result = scopeFor(['docs/ja-JP/USAGE.md']);
assert.strictEqual(result.code_changed, true,
`expected code_changed=true for docs/ja-JP/** change, got: ${JSON.stringify(result)}`);
assert.ok(
result.targeted_tests.some(t => t.includes('docs-parity-live-registry')),
`expected docs-parity-live-registry in targeted_tests, got: ${JSON.stringify(result.targeted_tests)}`,
);
});
test('B3: commands/** change selects docs-parity-live-registry', () => {
const result = scopeFor(['commands/gsd/plan-phase.md']);
assert.ok(
result.targeted_tests.some(t => t.includes('docs-parity-live-registry')),
`expected docs-parity-live-registry in targeted_tests for commands/** change, got: ${JSON.stringify(result.targeted_tests)}`,
);
});
});