fix(#3929): seed install-time capability validation with the merged registry (#4691)

* test(#3929): regression tests for singleton-map install validation

* fix(#3929): seed install-time cross-capability validation with the merged registry

* fix(#3929): seed install-time cross-capability validation with the merged registry

* fix(#3929): drop a seed overlay whose suite run throws, mirroring load

* test(#3929): match the issue repro tier so the live tier-monotone check passes it

* test(#3929): give the poisoned step its required onError field

* test(#3929): planted overlays must satisfy the full manifest contract

* chore(#3929): backfill changeset PR number (4691)

* fix(#3929): honor the generator override in central keys and skip reserved-id overlays in the seed

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-13 08:12:54 -04:00
committed by GitHub
parent 0763326ced
commit 4f487e4e75
4 changed files with 321 additions and 7 deletions

View File

@@ -60,7 +60,7 @@ function makeCwdWithStrict(strictValue) {
* (usable directly as an install <spec>). Declarative by default; pass `hooks`
* (with materialized scripts) to make it an executable surface requiring consent.
*/
function writeCapSource(id, { version = '1.0.0', hooks = [], engines, mcp } = {}) {
function writeCapSource(id, { version = '1.0.0', hooks = [], engines, mcp, requires, config, tier = 'standard' } = {}) {
const src = tmpDir(`cap-cli-src-${id}-`);
const cap = {
id,
@@ -68,13 +68,13 @@ function writeCapSource(id, { version = '1.0.0', hooks = [], engines, mcp } = {}
version,
title: id,
description: 'test capability',
tier: 'standard',
requires: [],
tier,
requires: requires ?? [],
runtimeCompat: { supported: ['*'], unsupported: [] },
skills: [],
agents: [],
hooks,
config: {},
config: config ?? {},
steps: [],
contributions: [],
gates: [],
@@ -1181,3 +1181,148 @@ describe('issue-2322: capability set --runtime materializes an installed third-p
assert.ok(staged.includes('workflows/example.md'), 'the rewrite must preserve the referenced path suffix, not corrupt the body');
});
});
// ─── #3929 — install-time cross-capability validation sees the merged registry ──
describe('#3929: install-time validation is seeded with first-party + installed overlays + candidate', () => {
test('requires on a first-party capability resolves at install time', () => {
const home = tmpDir('cap-cli-3929-fp-');
// tier: 'full' — the issue's own repro manifest; tdd is a full-tier
// capability and the (now-live) tier-monotone check forbids standard
// requiring full, so the candidate must be full to require it at all.
const src = writeCapSource('needs-fp', { requires: ['tdd'], tier: 'full' });
const r = runGsdTools(['capability', 'install', src, '--scope', 'global', '--raw'], makeCwd(), scopeEnv(home));
assert.equal(r.success, true, `install must succeed: tdd is first-party — got: ${r.error || r.output}`);
const o = parse(r.output);
assert.equal(o.status, 'installed');
assert.ok(readLedgerEntry(home, 'needs-fp'), 'ledger entry recorded');
});
test('requires on a genuinely missing capability still refuses', () => {
const home = tmpDir('cap-cli-3929-missing-');
const src = writeCapSource('needs-missing', { requires: ['no-such-capability-xyz'] });
const r = runGsdTools(['capability', 'install', src, '--scope', 'global'], makeCwd(), scopeEnv(home));
assert.equal(r.success, false, 'a genuinely missing requires id must still be refused');
assert.match(`${r.error}\n${r.output}`, /no-such-capability-xyz/);
assert.equal(readLedgerEntry(home, 'needs-missing'), null, 'no ledger entry');
});
test('requires on an installed overlay capability resolves at install time', () => {
const home = tmpDir('cap-cli-3929-overlay-');
const srcA = writeCapSource('overlay-a');
const first = runGsdTools(['capability', 'install', srcA, '--scope', 'global', '--raw'], makeCwd(), scopeEnv(home));
assert.equal(first.success, true, `seed install failed: ${first.error || first.output}`);
const srcB = writeCapSource('overlay-b', { requires: ['overlay-a'] });
const r = runGsdTools(['capability', 'install', srcB, '--scope', 'global', '--raw'], makeCwd(), scopeEnv(home));
assert.equal(r.success, true, `install must succeed: overlay-a is a committed installed overlay — got: ${r.error || r.output}`);
assert.ok(readLedgerEntry(home, 'overlay-b'), 'ledger entry recorded');
});
test('a pending (uncommitted) overlay does not satisfy requires', () => {
const home = tmpDir('cap-cli-3929-pending-');
const srcA = writeCapSource('pending-a');
const first = runGsdTools(['capability', 'install', srcA, '--scope', 'global', '--raw'], makeCwd(), scopeEnv(home));
assert.equal(first.success, true, `seed install failed: ${first.error || first.output}`);
// Flip cap-a's committed ledger entry to an in-flight _pending intent —
// exactly the state reconciliation defers on (the loader excludes it from
// the accepted map, so install-time validation must exclude it too).
// The shape must satisfy isValidLedgerEntry's _pending rules
// (kind 'install'|'upgrade', backupName string|null, string[] sharedFiles)
// or the shared ledger reader refuses the whole file as corrupt.
const lp = ledgerPath(home);
const ledger = JSON.parse(fs.readFileSync(lp, 'utf8'));
ledger.entries['pending-a']['_pending'] = { kind: 'upgrade', backupName: null, sharedFiles: [] };
fs.writeFileSync(lp, JSON.stringify(ledger, null, 2));
const srcB = writeCapSource('pending-b', { requires: ['pending-a'] });
const r = runGsdTools(['capability', 'install', srcB, '--scope', 'global'], makeCwd(), scopeEnv(home));
assert.equal(r.success, false, 'a pending overlay must not satisfy requires (committed-only rule)');
assert.match(`${r.error}\n${r.output}`, /pending-a/);
});
test('install-time config-key exclusivity against the central schema actually runs', () => {
const home = tmpDir('cap-cli-3929-central-');
// `mode` is a real central-schema validKey; declaring it federated must be
// refused at install once centralKeys is seeded (empty Set pre-fix).
const src = writeCapSource('claims-central', {
config: { mode: { type: 'string', default: 'standard', description: 'collides with the central schema' } },
});
const r = runGsdTools(['capability', 'install', src, '--scope', 'global'], makeCwd(), scopeEnv(home));
assert.equal(r.success, false, 'declaring a central config key must refuse at install');
assert.match(`${r.error}\n${r.output}`, /central config-schema/);
});
});
describe('#3929: a throwing (duplicate-producer) overlay is skipped, never attributed to the candidate', () => {
// Hand-plant a committed overlay (manifest + structurally-valid ledger
// entry) WITHOUT the CLI — needed because post-fix installs refuse a
// duplicate-producer candidate up front, so the poisoned pair can only
// exist from pre-fix history (the exact scenario the seed must survive).
function plantOverlay(home, id, cap) {
const dir = capDir(home, id);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap, null, 2));
const lp = ledgerPath(home);
let ledger;
try {
ledger = JSON.parse(fs.readFileSync(lp, 'utf8'));
} catch {
ledger = { schema_version: 1, updatedAt: new Date().toISOString(), entries: {} };
}
ledger.entries = ledger.entries || {};
ledger.entries[id] = {
id,
version: cap.version || '1.0.0',
source: 'test-plant',
integrity: '',
files: ['capability.json'],
sharedEdits: [],
};
fs.writeFileSync(lp, JSON.stringify(ledger, null, 2));
}
test('pre-existing duplicate-producer overlays do not fail a later install', () => {
const home = tmpDir('cap-cli-3929-poison-');
const cwd = makeCwd();
// poison-a must be seeded (and accepted) BEFORE poison-b trips the
// duplicate-producer throw during the seed's own suite run.
const srcA = writeCapSource('poison-a');
const first = runGsdTools(['capability', 'install', srcA, '--scope', 'global', '--raw'], cwd, scopeEnv(home));
assert.equal(first.success, true, `seed install failed: ${first.error || first.output}`);
const poisonedStep = {
point: 'plan:pre',
ref: { skill: 'poison-skill' },
produces: ['SHARED-ARTIFACT.md'],
consumes: [],
onError: 'skip',
};
plantOverlay(home, 'poison-b', {
id: 'poison-b', role: 'feature', version: '1.0.0', title: 'poison-b',
description: 'duplicate producer of SHARED-ARTIFACT.md', tier: 'standard',
requires: [], skills: ['poison-skill'], agents: [], config: {},
runtimeCompat: { supported: ['*'], unsupported: [] },
hooks: [], steps: [poisonedStep], contributions: [], gates: [],
});
// poison-a is re-planted with the SAME producer so the pair trips
// validateConsumesGlobal's duplicate-producer throw when both are seeded.
plantOverlay(home, 'poison-a', {
id: 'poison-a', role: 'feature', version: '1.0.0', title: 'poison-a',
description: 'duplicate producer of SHARED-ARTIFACT.md', tier: 'standard',
requires: [], skills: ['poison-skill'], agents: [], config: {},
runtimeCompat: { supported: ['*'], unsupported: [] },
hooks: [], steps: [poisonedStep], contributions: [], gates: [],
});
// The candidate only installs if the throwing overlay was DROPPED from
// the seed — if it were retained, the whole-map suite would throw during
// the candidate's own validation and this install would fail.
const srcC = writeCapSource('poison-c', { requires: ['poison-a'] });
const r = runGsdTools(['capability', 'install', srcC, '--scope', 'global', '--raw'], cwd, scopeEnv(home));
assert.equal(r.success, true, `install must succeed despite the poisoned overlay: ${r.error || r.output}`);
assert.ok(readLedgerEntry(home, 'poison-c'), 'candidate ledger entry recorded');
});
});