Phase 2 of golden-parity redesign (epic #2264). Adds an install file-set snapshot (golden-install-tree) and a ci-test-scope rule selecting golden-parity whenever any installed-source path changes, closing the silent-staleness hole behind the #2266 red. ADR-2264 amended (the copy/transform split premise was unsound). Closes #2267.
This commit is contained in:
@@ -145,6 +145,37 @@ const RULES = [
|
||||
'tests/golden-install-parity.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify golden install parity (drift guard)
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'shipped install content (golden-parity drift guard, #2267)',
|
||||
// Every source file the installer EMITS into a runtime layout is captured by
|
||||
// golden-install-parity + the install-tree snapshot. A source edit here that
|
||||
// changes emitted output MUST re-verify the fixtures — otherwise stale golden
|
||||
// fixtures merge silently (#2266: a hooks/gsd-statusline.js edit changed
|
||||
// installed output but no rule selected golden-parity, so stale fixtures
|
||||
// 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.
|
||||
// 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
|
||||
// package layout' rule (path.startsWith('gsd-core/bin/')) — so matching it
|
||||
// here would be pure noise; and
|
||||
// (2) enumerating only the installer-shipped content subtrees preserves the
|
||||
// bug-408 unit-fallback contract: a gsd-core/ path that is NOT shipped
|
||||
// verbatim (the bug-408 test uses gsd-core/src/some-util.js) must still
|
||||
// fall back to ['unit'] when no rule matches.
|
||||
// Listed: the four gsd-core content subtrees the installer ships verbatim
|
||||
// (contexts, references, templates, workflows) + bin/shared/*.json data files.
|
||||
// Verify against Object.keys(golden fixture) grouped by gsd-core/<subdir>.
|
||||
match: path =>
|
||||
['hooks/', 'commands/', 'agents/', 'skills/', 'gsd-core/workflows/', 'gsd-core/templates/', 'gsd-core/references/', 'gsd-core/contexts/', 'scripts/changeset/', 'scripts/lib/'].some(p => path.startsWith(p)) ||
|
||||
(path.startsWith('gsd-core/bin/shared/') && path.endsWith('.json')) ||
|
||||
['scripts/fix-slash-commands.cjs', 'scripts/gen-capability-registry.cjs', 'scripts/gen-loop-host-contract.cjs'].includes(path),
|
||||
tests: [
|
||||
'tests/golden-install-parity.test.cjs',
|
||||
'tests/golden-install-tree.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'hooks',
|
||||
match: path => path.startsWith('hooks/'),
|
||||
@@ -364,8 +395,15 @@ function classify(files) {
|
||||
for (const file of files) {
|
||||
// Determine if this file is product/pipeline code.
|
||||
// docs/ and root-level .md files are intentionally excluded.
|
||||
// 'skills/' is shipped agent-skill content installed into every runtime by
|
||||
// the installer (see the 'shipped install content' RULES entry below) — it
|
||||
// must be product code, or a skills/-only change silently gets
|
||||
// code_changed=false and skips the ENTIRE CI matrix, not merely golden-parity
|
||||
// (found while verifying the #2267 golden-parity rule against skills/**: the
|
||||
// rule fired in `reasons` but classify()'s codeChanged gate zeroed out every
|
||||
// targeted test because 'skills/' was absent from this list).
|
||||
if (
|
||||
['bin/', 'src/', 'gsd-core/', 'agents/', 'commands/', 'hooks/', 'tests/', 'scripts/', 'eslint-rules/'].some(p => file.startsWith(p)) ||
|
||||
['bin/', 'src/', 'gsd-core/', 'agents/', 'commands/', 'hooks/', 'skills/', 'tests/', 'scripts/', 'eslint-rules/'].some(p => file.startsWith(p)) ||
|
||||
file === 'package.json' || file === 'package-lock.json' ||
|
||||
(file.startsWith('tsconfig') && file.endsWith('.json')) ||
|
||||
file.startsWith('.github/rulesets/')
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
// buildParityManifest (and its exclusion constants) is the canonical single
|
||||
@@ -22,7 +23,7 @@ const ROOT = path.resolve(__dirname, '..');
|
||||
// generator no longer keeps its own inline copy, which had drifted from the
|
||||
// test harness's copy (missing the realpath/`<HOME>` normalization) and
|
||||
// mis-generated the claude-local fixture (#2100).
|
||||
const { runMinimalInstall, RUNTIME_META, buildParityManifest } = require(path.join(ROOT, 'tests', 'helpers', 'install-shared.cjs'));
|
||||
const { runMinimalInstall, RUNTIME_META, buildParityManifest, BUILD_SCRIPT } = require(path.join(ROOT, 'tests', 'helpers', 'install-shared.cjs'));
|
||||
const FIXTURE_DIR = path.join(ROOT, 'tests', 'fixtures', 'golden-install-parity');
|
||||
|
||||
function cleanup(root) {
|
||||
@@ -38,6 +39,12 @@ function cleanup(root) {
|
||||
const targets = process.argv.slice(2).length > 0 ? process.argv.slice(2) : Object.keys(RUNTIME_META);
|
||||
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
||||
|
||||
// Build hooks/dist before capturing so fixtures are complete even in a clean
|
||||
// checkout (hooks/dist is gitignored + built; DEFECT.HOOKS-DIST-SCOPED-CI). The
|
||||
// test harness's before() hook does the same; without it a fresh checkout omits
|
||||
// every hooks/* path and this generator silently writes short fixtures.
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { stdio: 'pipe' });
|
||||
|
||||
for (const runtime of targets) {
|
||||
if (!Object.prototype.hasOwnProperty.call(RUNTIME_META, runtime)) {
|
||||
process.stderr.write(`[gen] unknown runtime '${runtime}' (not in RUNTIME_META) — skipping\n`);
|
||||
|
||||
75
scripts/gen-install-tree-fixtures.cjs
Normal file
75
scripts/gen-install-tree-fixtures.cjs
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
/**
|
||||
* Standalone golden-fixture generator for tests/golden-install-tree (#2267 Phase 2).
|
||||
*
|
||||
* This is a BUILD-TIME generation script — NOT a test run. It imports the
|
||||
* canonical buildInstallTree builder from tests/helpers/install-shared.cjs,
|
||||
* which in turn reuses buildParityManifest's exact exclusion set (issue
|
||||
* #2266) so the file-set fixtures here and the golden-install-parity content
|
||||
* fixtures never diverge on which files they cover. The authoritative test
|
||||
* gate remains `gsd-test run`, never a local `node --test`.
|
||||
*
|
||||
* Usage: node scripts/gen-install-tree-fixtures.cjs [runtime ...]
|
||||
*/
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
// buildInstallTree (and the exclusion constants it reuses from
|
||||
// buildParityManifest) is the canonical single source of truth in
|
||||
// tests/helpers/install-shared.cjs (issue #2266/#2267) — this generator does
|
||||
// not keep its own inline copy of the walk/exclusion logic.
|
||||
const { runMinimalInstall, RUNTIME_META, buildInstallTree, BUILD_SCRIPT } = require(path.join(ROOT, 'tests', 'helpers', 'install-shared.cjs'));
|
||||
const FIXTURE_DIR = path.join(ROOT, 'tests', 'fixtures', 'install-tree');
|
||||
|
||||
function cleanup(root) {
|
||||
try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
|
||||
}
|
||||
|
||||
// Regenerate the fixture for every runtime in RUNTIME_META. Needed when a
|
||||
// SHARED gsd-core payload file (e.g. model-catalog.json, capability-registry)
|
||||
// changes content — its path appears in every runtime's manifest, so all
|
||||
// fixtures must be recaptured together. Usage:
|
||||
// node scripts/gen-install-tree-fixtures.cjs [runtime ...]
|
||||
// With no args, regenerates ALL runtimes. With args, only the named runtimes.
|
||||
const targets = process.argv.slice(2).length > 0 ? process.argv.slice(2) : Object.keys(RUNTIME_META);
|
||||
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
||||
|
||||
// Build hooks/dist before capturing so fixtures are complete even in a clean
|
||||
// checkout (hooks/dist is gitignored + built; DEFECT.HOOKS-DIST-SCOPED-CI). The
|
||||
// test harness's before() hook does the same; without it a fresh checkout omits
|
||||
// every hooks/* path and this generator silently writes short fixtures.
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { stdio: 'pipe' });
|
||||
|
||||
for (const runtime of targets) {
|
||||
if (!Object.prototype.hasOwnProperty.call(RUNTIME_META, runtime)) {
|
||||
process.stderr.write(`[gen] unknown runtime '${runtime}' (not in RUNTIME_META) — skipping\n`);
|
||||
continue;
|
||||
}
|
||||
const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' });
|
||||
let actual;
|
||||
try {
|
||||
actual = buildInstallTree(configDir, root);
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
const fixturePath = path.join(FIXTURE_DIR, `${runtime}.json`);
|
||||
fs.writeFileSync(fixturePath, JSON.stringify(actual, null, 2) + '\n', 'utf8');
|
||||
process.stdout.write(`[gen] ${runtime}: wrote ${actual.length} paths -> ${fixturePath}\n`);
|
||||
}
|
||||
|
||||
// Also regenerate the claude LOCAL legacy-layout fixture (claude-local.json).
|
||||
// This layout is distinct from the global install (commands/gsd-*.md +
|
||||
// agents/gsd-*.md) and has its own snapshot assertion in the test harness.
|
||||
const { configDir: localConfigDir, root: localRoot } = runMinimalInstall({ runtime: 'claude', scope: 'local' });
|
||||
let localActual;
|
||||
try {
|
||||
localActual = buildInstallTree(localConfigDir, localRoot);
|
||||
} finally {
|
||||
cleanup(localRoot);
|
||||
}
|
||||
const localFixturePath = path.join(FIXTURE_DIR, 'claude-local.json');
|
||||
fs.writeFileSync(localFixturePath, JSON.stringify(localActual, null, 2) + '\n', 'utf8');
|
||||
process.stdout.write(`[gen] claude-local: wrote ${localActual.length} paths -> ${localFixturePath}\n`);
|
||||
Reference in New Issue
Block a user