* fix(#2758): select the emitted differential wherever golden-parity runs Add tests/emitted-provenance.test.cjs and tests/emitted-attribution.test.cjs to every scripts/ci-test-scope.cjs rule that already selects tests/golden-install-parity.test.cjs, so the ADR-2719 dual-run differential travels with the golden on the targeted CI lane instead of being selected by no rule at all. Add an independent module-load totality guard (missingRuleTestFiles) that throws when any rule names a test file absent from disk -- the guard that would have caught the post-Phase-4-cutover hole. It immediately surfaced three pre-existing phantom entries left behind by consolidation epic #1969 (bug-3588/bug-10/bug-3683 filenames folded into other suites months ago but never removed from the rule table); fixed in the same change rather than deferred. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 * fix(#2758): trim unused exports and normalize the path require style Code-review (Standards axis) flagged two judgement-call smells: exporting classify/isInertCi with no caller (Speculative Generality), and requiring path with a node: prefix while the file's other core requires do not (inconsistent style within one file). Both addressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5kQs6ZufZDySC6zDJfYP6 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { existsSync, readdirSync, appendFileSync } = require('fs');
|
||||
|
||||
@@ -105,9 +106,8 @@ const RULES = [
|
||||
fullMatrix: true,
|
||||
tests: [
|
||||
'tests/check-env.test.cjs',
|
||||
'tests/npm-integrity-gate.test.cjs',
|
||||
'tests/npm-integrity-gate.test.cjs', // #2758: absorbs the former tests/bug-3588-npm-audit-clean.test.cjs (folded into it by consolidation epic #1969 B6 #1975; the stale filename here was a silent coverage hole this rule never actually re-selected)
|
||||
'tests/package-manifest.test.cjs',
|
||||
'tests/bug-3588-npm-audit-clean.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -116,9 +116,10 @@ const RULES = [
|
||||
// still trigger the migrated module's tests (otherwise CI silently skips them).
|
||||
match: path => path.startsWith('src/') || path === 'tsconfig.build.json',
|
||||
tests: [
|
||||
'tests/semver-compare.test.cjs',
|
||||
'tests/bug-10-semver-policy-consolidation.test.cjs',
|
||||
'tests/semver-compare.test.cjs', // #2758: absorbs the former tests/bug-10-semver-policy-consolidation.test.cjs (folded into it by consolidation epic #1969 B3 #1972; the stale filename here was a silent coverage hole this rule never actually re-selected)
|
||||
'tests/golden-install-parity.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify golden install parity (drift guard)
|
||||
'tests/emitted-provenance.test.cjs', // #2758: the differential travels with the golden — ADR-2719 dual-run
|
||||
'tests/emitted-attribution.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -143,6 +144,8 @@ const RULES = [
|
||||
// changed test file.
|
||||
'tests/runtime-artifact-layout.test.cjs',
|
||||
'tests/golden-install-parity.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify golden install parity (drift guard)
|
||||
'tests/emitted-provenance.test.cjs', // #2758: the differential travels with the golden — ADR-2719 dual-run
|
||||
'tests/emitted-attribution.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -155,6 +158,10 @@ const RULES = [
|
||||
// shipped to next undetected). Union semantics: this ADDS the parity guard on
|
||||
// top of each path's existing content-specific tests. Targeted lane only (the
|
||||
// golden test skips win32 by design), no fullMatrix.
|
||||
// #2758: the Phase 2/3 emitted differential (ADR-2719) travels alongside the
|
||||
// golden here too — a PR editing only shipped content is the archetypal
|
||||
// emitted ripple, and until this fix no rule selected the differential at
|
||||
// all. Both green, fixtures untouched, is the dual-run premise.
|
||||
// NOTE: intentionally NOT a blanket 'gsd-core/' prefix, for two reasons:
|
||||
// (1) gsd-core/bin/** is tsc-compiled runtime output — EXCLUDED_PREFIXES-
|
||||
// excluded from both manifests, and already covered by the 'installer and
|
||||
@@ -174,6 +181,8 @@ const RULES = [
|
||||
tests: [
|
||||
'tests/golden-install-parity.test.cjs',
|
||||
'tests/golden-install-tree.test.cjs',
|
||||
'tests/emitted-provenance.test.cjs',
|
||||
'tests/emitted-attribution.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -236,7 +245,13 @@ const RULES = [
|
||||
'tests/workflow-size-budget.test.cjs',
|
||||
'tests/workflow-guard-registration.test.cjs',
|
||||
'tests/commands.test.cjs',
|
||||
'tests/bug-3683-workflow-colon-namespace-leak.test.cjs',
|
||||
// #2758: was 'tests/bug-3683-workflow-colon-namespace-leak.test.cjs', deleted by
|
||||
// consolidation epic #1969 (B6 #1975) and folded into slash-command-namespace.test.cjs
|
||||
// ("folded:bug-3683-workflow-colon-namespace-leak" describe block). The stale filename
|
||||
// here was itself an instance of this issue's defect class — silently dropped by
|
||||
// existingTests() below, so gsd-core/workflows/ changes stopped re-running this
|
||||
// regression's coverage with nothing signaling it.
|
||||
'tests/slash-command-namespace.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -291,6 +306,41 @@ const RULES = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Every RULES[].tests entry (deduped, across every rule) that does NOT exist on
|
||||
* disk. #2758: a rule naming a test file that no longer exists is not merely
|
||||
* inert — existingTests() below silently drops it out of targeted_tests, with
|
||||
* nothing in the CI output signaling why. Phase 4 (#2724) deletes
|
||||
* tests/golden-install-parity.test.cjs; without this check, any rule still
|
||||
* naming it would stop selecting the guard entirely and CI would stay green
|
||||
* throughout. Pure and independent of which rule / which file: it catches ANY
|
||||
* phantom entry, not only the two names this issue is about.
|
||||
* Paths resolve relative to the repo root (this file's parent directory), not
|
||||
* the caller's cwd, so the check behaves identically whether invoked as the CLI
|
||||
* (`node scripts/ci-test-scope.cjs ...`, cwd == repo root by convention) or
|
||||
* required directly by a test.
|
||||
*/
|
||||
function missingRuleTestFiles(rules) {
|
||||
const referenced = new Set();
|
||||
for (const rule of rules) {
|
||||
for (const f of rule.tests) referenced.add(f);
|
||||
}
|
||||
return [...referenced].filter(f => !existsSync(path.join(__dirname, '..', f))).sort();
|
||||
}
|
||||
|
||||
// Fail loudly at module load, mirroring the PROTECTED_WORKFLOWS check above —
|
||||
// this fires on EVERY invocation of the CLI (including the real `changes` job
|
||||
// in .github/workflows/test.yml), not only when a test suite happens to run.
|
||||
{
|
||||
const missing = missingRuleTestFiles(RULES);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`ci-test-scope: RULES reference test file(s) that do not exist on disk ` +
|
||||
`(silent coverage hole — see #2758):\n ${missing.join('\n ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
'Usage:',
|
||||
@@ -523,4 +573,8 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
runMain(main);
|
||||
}
|
||||
|
||||
module.exports = { RULES, missingRuleTestFiles };
|
||||
|
||||
@@ -708,6 +708,115 @@ describe('shipped install content (golden-parity drift guard, #2267)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('differential attribution gates travel with golden-parity (#2758)', () => {
|
||||
// #2758: the golden-parity rules ran tests/golden-install-parity.test.cjs on a
|
||||
// shipped-content-only PR — the archetypal emitted ripple — but selected neither
|
||||
// tests/emitted-provenance.test.cjs nor tests/emitted-attribution.test.cjs. Fix:
|
||||
// every rule that selects the golden also selects both emitted gates, so the
|
||||
// differential travels with the golden everywhere the golden travels (the
|
||||
// ADR-2719 dual-run premise: both green, fixtures untouched, until Phase 4).
|
||||
const { RULES } = require('../scripts/ci-test-scope.cjs');
|
||||
const GOLDEN = 'tests/golden-install-parity.test.cjs';
|
||||
const GATES = ['tests/emitted-provenance.test.cjs', 'tests/emitted-attribution.test.cjs'];
|
||||
|
||||
test('every RULES entry selecting golden-install-parity also selects both emitted gates', () => {
|
||||
const goldenRules = RULES.filter(r => r.tests.includes(GOLDEN));
|
||||
// Guards the guard: if this count ever drops to 0, the assertion below is
|
||||
// vacuously true and would silently stop meaning anything.
|
||||
assert.ok(
|
||||
goldenRules.length >= 3,
|
||||
`expected at least 3 RULES entries selecting ${GOLDEN}, found ${goldenRules.length}: ` +
|
||||
`${goldenRules.map(r => r.name).join(', ')}`,
|
||||
);
|
||||
const offenders = goldenRules.filter(r => !GATES.every(g => r.tests.includes(g)));
|
||||
assert.deepStrictEqual(
|
||||
offenders.map(r => r.name),
|
||||
[],
|
||||
`rule(s) select ${GOLDEN} without both emitted gates: ${offenders.map(r => r.name).join(', ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('a pure shipped-content path selects both emitted gates alongside the golden', () => {
|
||||
// #2758 AC: "a pure shipped-content path (e.g. gsd-core/workflows/plan-phase.md)
|
||||
// selects the differential." The archetypal emitted ripple.
|
||||
const result = scopeFor(['gsd-core/workflows/plan-phase.md']);
|
||||
assert.strictEqual(result.code_changed, true);
|
||||
assert.ok(result.targeted_tests.includes(GOLDEN));
|
||||
for (const g of GATES) {
|
||||
assert.ok(
|
||||
result.targeted_tests.includes(g),
|
||||
`expected ${g} in targeted_tests for a shipped-content-only change, got: ${JSON.stringify(result.targeted_tests)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('a src/*.cts-only change selects both emitted gates (TS runtime sources rule)', () => {
|
||||
const result = scopeFor(['src/milestone.cts']);
|
||||
for (const g of GATES) {
|
||||
assert.ok(
|
||||
result.targeted_tests.includes(g),
|
||||
`expected ${g} in targeted_tests for src/ change, got: ${JSON.stringify(result.targeted_tests)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('bin/install.js selects both emitted gates (installer and package layout rule)', () => {
|
||||
const result = scopeFor(['bin/install.js']);
|
||||
for (const g of GATES) {
|
||||
assert.ok(
|
||||
result.targeted_tests.includes(g),
|
||||
`expected ${g} in targeted_tests for bin/install.js, got: ${JSON.stringify(result.targeted_tests)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('docs-only change still does NOT select the emitted gates (negative case)', () => {
|
||||
const result = scopeFor(['docs/usage.md']);
|
||||
for (const g of GATES) {
|
||||
assert.ok(!result.targeted_tests.includes(g), `docs-only must NOT select ${g}, got: ${JSON.stringify(result.targeted_tests)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('RULES totality guard: no rule names a test file absent from disk (#2758)', () => {
|
||||
// #2758: Phase 4 (#2724) deletes tests/golden-install-parity.test.cjs. Without an
|
||||
// independent guard, a rule still naming it would produce no signal at all —
|
||||
// existingTests() (scripts/ci-test-scope.cjs) silently filters missing files out
|
||||
// of targeted_tests, so the gate simply stops being selected while CI stays
|
||||
// green. This exists independently of the fix above: it catches ANY rule naming
|
||||
// ANY absent file, not only the two gate filenames this issue is about.
|
||||
const { RULES, missingRuleTestFiles } = require('../scripts/ci-test-scope.cjs');
|
||||
|
||||
test('no RULES entry today references a test file absent from disk', () => {
|
||||
assert.deepStrictEqual(
|
||||
missingRuleTestFiles(RULES), [],
|
||||
'RULES reference test file(s) that do not exist — see missingRuleTestFiles() in scripts/ci-test-scope.cjs',
|
||||
);
|
||||
});
|
||||
|
||||
test('the guard mechanism itself catches a phantom entry (hostile input)', () => {
|
||||
// Runs the REAL checker function used by the module-load assertion in
|
||||
// scripts/ci-test-scope.cjs — not a hand-copied reimplementation of it — against
|
||||
// a synthetic rule table, proving the mechanism would have caught exactly the
|
||||
// Phase-4 shape: a rule naming a file that no longer exists on disk.
|
||||
const phantomFile = 'tests/does-not-exist-2758.test.cjs';
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(ROOT, phantomFile)),
|
||||
'precondition: the phantom file must genuinely not exist for this test to discriminate',
|
||||
);
|
||||
const synthetic = [
|
||||
{ name: 'real', tests: ['tests/commands.test.cjs'] },
|
||||
{ name: 'phantom', tests: [phantomFile, 'tests/commands.test.cjs'] },
|
||||
];
|
||||
assert.deepStrictEqual(missingRuleTestFiles(synthetic), [phantomFile]);
|
||||
});
|
||||
|
||||
test('an all-real synthetic table reports nothing missing (negative case)', () => {
|
||||
const synthetic = [{ name: 'real', tests: ['tests/commands.test.cjs', 'tests/ci-test-scope.test.cjs'] }];
|
||||
assert.deepStrictEqual(missingRuleTestFiles(synthetic), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('code_changed=false implies clean output invariant', () => {
|
||||
// Fix 1: when code_changed is false, full_matrix, targeted_tests, windows_tests
|
||||
// must ALL be empty/false — even if a docs path coincidentally
|
||||
|
||||
Reference in New Issue
Block a user