'use strict'; /** * capability-loader.test.cjs — ADR-1244 D2 runtime registry overlay. * * Behavioral tests for loadRegistry({ includeInstalled }): first-party ∪ * validated overlay composition, first-party-wins collisions, reserved * namespace, engines.gsd load-time re-gate (skip-with-warning), gate-kind * fail-closed tracking, and parity with the canonical builder. */ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { cleanup } = require('./helpers.cjs'); const { loadRegistry } = require('../gsd-core/bin/lib/capability-loader.cjs'); const baseRegistry = require('../gsd-core/bin/lib/capability-registry.cjs'); const { buildRegistry } = require('../scripts/gen-capability-registry.cjs'); const HOST = '1.6.0'; function featureCap(id, extra) { return { id, role: 'feature', version: '1.0.0', title: id, description: 'overlay cap', tier: 'standard', requires: [], engines: { gsd: '>=1.0.0' }, runtimeCompat: { supported: ['*'], unsupported: [] }, skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [], ...extra, }; } // Build a temp GSD home containing .gsd/capabilities//capability.json for each cap. function makeOverlayHome(caps) { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-overlay-')); for (const cap of caps) { const dir = path.join(home, '.gsd', 'capabilities', cap.id); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8'); } return home; } // Always pass cwd === home so the project-root probe cannot wander into the // real repo; root-dedup makes the project scope a no-op there. function load(home, opts) { return loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST, ...opts }); } describe('loadRegistry — base behavior', () => { test('without includeInstalled returns the frozen registry (identity-stable)', () => { assert.strictEqual(loadRegistry(), baseRegistry); assert.strictEqual(loadRegistry({ includeInstalled: false }), baseRegistry); }); test('includeInstalled with no overlay directory returns the frozen registry unchanged', () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-empty-')); try { assert.strictEqual(load(home), baseRegistry); } finally { cleanup(home); } }); }); describe('loadRegistry — accepting valid overlays', () => { test('a valid overlay capability appears in every derived view (toggable + federated)', (t) => { const home = makeOverlayHome([ featureCap('deploy-gate', { skills: ['deploy-review'], agents: ['gsd-deploy-checker'], config: { 'workflow.deploy_gate': { type: 'boolean', default: true, description: 'Enable the deploy gate.' } }, steps: [{ point: 'execute:wave:post', ref: { skill: 'deploy-review' }, produces: ['DEPLOY.md'], consumes: [], when: 'workflow.deploy_gate', onError: 'skip' }], }), ]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(reg.capabilities['deploy-gate'], 'overlay in capabilities'); assert.equal(reg.bySkill['deploy-review'], 'deploy-gate', 'overlay skill indexed (surface)'); assert.equal(reg.byAgent['gsd-deploy-checker'], 'deploy-gate', 'overlay agent indexed'); assert.ok(reg.configSchema['workflow.deploy_gate'], 'overlay config federated'); assert.equal(reg.configKeys['workflow.deploy_gate'], 'deploy-gate', 'overlay config key owned'); assert.ok(reg.capabilityClusters['deploy-gate'], 'overlay in capabilityClusters (surface toggle)'); assert.ok(reg.profileMembership['deploy-gate'], 'overlay in profileMembership (surface toggle)'); const wavePost = reg.byLoopPoint['execute:wave:post']; assert.ok(wavePost && Array.isArray(wavePost.steps) && wavePost.steps.some((h) => h.capId === 'deploy-gate'), 'overlay step wired into the loop'); // First-party is preserved. assert.equal(reg.capabilities['ui'].title, 'UI design contracts'); assert.equal(reg._overlay.warnings.length, 0, 'no warnings when all overlays accepted'); assert.deepEqual(reg._overlay.incompatibleGateCapIds, []); }); test('composed registry equals buildRegistry over the same merged cap-map (no drift / no dropped caps)', (t) => { const overlay = featureCap('extra-cap', { skills: ['extra-skill'] }); const home = makeOverlayHome([overlay]); t.after(() => cleanup(home)); const reg = load(home); const mergedMap = new Map(Object.entries(baseRegistry.capabilities)); mergedMap.set('extra-cap', overlay); const expected = buildRegistry(mergedMap); assert.deepEqual(Object.keys(reg.capabilities).sort(), Object.keys(expected.capabilities).sort()); assert.deepEqual(reg.bySkill, expected.bySkill); assert.deepEqual(Object.keys(reg.configSchema).sort(), Object.keys(expected.configSchema).sort()); assert.deepEqual(reg.capabilityClusters['extra-cap'], expected.capabilityClusters['extra-cap']); }); }); describe('loadRegistry — first-party always wins', () => { test('overlay whose id collides with a first-party id is rejected; first-party preserved', (t) => { const home = makeOverlayHome([featureCap('ui', { skills: ['hijacked'] })]); t.after(() => cleanup(home)); const reg = load(home); assert.equal(reg.capabilities['ui'].title, 'UI design contracts', 'first-party ui untouched'); assert.ok(reg._overlay.warnings.some((w) => w.id === 'ui' && /collide/i.test(w.reason))); }); test('overlay claiming a first-party skill stem is rejected', (t) => { const home = makeOverlayHome([featureCap('skill-thief', { skills: ['ui-phase'] })]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(!reg.capabilities['skill-thief']); assert.ok(reg._overlay.warnings.some((w) => w.id === 'skill-thief' && /skill/i.test(w.reason))); }); test('reserved id prefix (gsd-/gsd-core-/anthropic-) is rejected', (t) => { const home = makeOverlayHome([ featureCap('gsd-impostor'), featureCap('anthropic-impostor'), ]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(!reg.capabilities['gsd-impostor']); assert.ok(!reg.capabilities['anthropic-impostor']); assert.equal(reg._overlay.warnings.filter((w) => /reserved/i.test(w.reason)).length, 2); }); }); describe('loadRegistry — load-time re-gate (engines.gsd) + fail-closed gates', () => { test('incompatible engines.gsd is skipped with a warning', (t) => { const home = makeOverlayHome([featureCap('future-cap', { engines: { gsd: '>=99.0.0' } })]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(!reg.capabilities['future-cap']); assert.ok(reg._overlay.warnings.some((w) => w.id === 'future-cap' && /incompatible/i.test(w.reason))); assert.deepEqual(reg._overlay.incompatibleGateCapIds, [], 'no gate declared → not a fail-closed blocker'); }); test('a skipped capability that DECLARES a gate is recorded for fail-closed handling', (t) => { const home = makeOverlayHome([ featureCap('incompat-gate', { engines: { gsd: '>=99.0.0' }, gates: [{ point: 'execute:wave:post', check: { query: 'x.deploy' }, blocking: true, onError: 'halt' }], }), ]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(!reg.capabilities['incompat-gate'], 'incompatible cap not loaded'); assert.ok(reg._overlay.incompatibleGateCapIds.includes('incompat-gate'), 'gate-kind tracked as fail-closed'); assert.ok( reg._overlay.blockedGates.some((g) => g.point === 'execute:wave:post' && g.capId === 'incompat-gate'), 'declared gate point recorded for per-point fail-closed injection', ); }); test('compatible engines.gsd is accepted', (t) => { const home = makeOverlayHome([featureCap('compat-cap', { engines: { gsd: '>=1.6.0 <3.0.0' } })]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(reg.capabilities['compat-cap']); }); }); describe('loadRegistry — malformed overlays are skipped, never crash', () => { test('manifest failing validation is skipped with a warning', (t) => { const home = makeOverlayHome([ // missing required version → validateCapability error (() => { const c = featureCap('no-version'); delete c.version; return c; })(), ]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(!reg.capabilities['no-version']); assert.ok(reg._overlay.warnings.some((w) => w.id === 'no-version' && /version/i.test(w.reason))); }); test('unreadable / invalid JSON is skipped with a warning (no throw)', (t) => { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-badjson-')); t.after(() => cleanup(home)); const dir = path.join(home, '.gsd', 'capabilities', 'broken'); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'capability.json'), '{ not valid json', 'utf8'); let reg; assert.doesNotThrow(() => { reg = load(home); }); assert.ok(!reg.capabilities['broken']); assert.ok(reg._overlay.warnings.some((w) => w.id === 'broken')); }); test('the loop never crashes — first-party registry remains fully intact alongside bad overlays', (t) => { const home = makeOverlayHome([ featureCap('gsd-reserved'), (() => { const c = featureCap('bad'); c.role = 'nonsense'; return c; })(), featureCap('good', { skills: ['good-only-skill'] }), ]); t.after(() => cleanup(home)); const reg = load(home); assert.equal(Object.keys(baseRegistry.capabilities).length + 1, Object.keys(reg.capabilities).length, 'exactly the one good overlay is added; first-party count preserved'); assert.ok(reg.capabilities['good']); }); }); describe('loadRegistry — full merged-set cross-capability validation', () => { test('overlay claiming a first-party command family is rejected (first-party wins)', (t) => { const firstPartyFamily = Object.keys(baseRegistry.commandFamilies || {})[0]; assert.ok(firstPartyFamily, 'precondition: first-party owns at least one command family'); const home = makeOverlayHome([ featureCap('cmd-thief', { commands: [{ family: firstPartyFamily, module: 'thief.cjs', router: 'route' }] }), ]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(!reg.capabilities['cmd-thief'], 'overlay hijacking a first-party command family is not loaded'); assert.ok(reg._overlay.warnings.some((w) => w.id === 'cmd-thief' && /command family/i.test(w.reason))); assert.ok(reg.commandFamilies[firstPartyFamily], 'first-party command family preserved'); }); test('overlay with an unsatisfiable consumes is rejected by cross-capability validation', (t) => { const home = makeOverlayHome([ featureCap('bad-consumes', { skills: ['bad-consumes-skill'], config: { 'workflow.bad_consumes': { type: 'boolean', default: true, description: 'x' } }, steps: [{ point: 'plan:pre', ref: { skill: 'bad-consumes-skill' }, produces: [], consumes: ['NONEXISTENT-ARTIFACT.md'], when: 'workflow.bad_consumes', onError: 'skip' }], }), ]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(!reg.capabilities['bad-consumes'], 'overlay failing consumes-satisfiability is not loaded'); assert.ok(reg._overlay.warnings.some((w) => w.id === 'bad-consumes' && /cross-capability/i.test(w.reason))); }); test('an invalid hook fragment path (escaping the capability dir) is rejected', (t) => { const home = makeOverlayHome([ featureCap('frag-escape', { contributions: [{ point: 'plan:pre', into: 'planner', fragment: { path: '../../../etc/passwd' }, when: 'workflow.frag', onError: 'skip' }], config: { 'workflow.frag': { type: 'boolean', default: true, description: 'x' } }, }), ]); t.after(() => cleanup(home)); const reg = load(home); assert.ok(!reg.capabilities['frag-escape'], 'overlay with an escaping fragment path is not loaded'); assert.ok(reg._overlay.warnings.some((w) => w.id === 'frag-escape' && /fragment/i.test(w.reason))); }); }); describe('loadRegistry — project-scoped overlay root', () => { test('reads an overlay from /.gsd/capabilities when cwd is inside a project', (t) => { const proj = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-proj-')); t.after(() => cleanup(proj)); fs.mkdirSync(path.join(proj, '.planning'), { recursive: true }); // project-root marker const dir = path.join(proj, '.gsd', 'capabilities', 'proj-cap'); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(featureCap('proj-cap', { skills: ['proj-skill'] })), 'utf8'); // Point the global home elsewhere (empty) so only the project scope contributes. const emptyHome = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-emptyhome-')); t.after(() => cleanup(emptyHome)); const reg = loadRegistry({ includeInstalled: true, gsdHome: emptyHome, cwd: proj, hostVersion: HOST }); assert.ok(reg.capabilities['proj-cap'], 'project-scoped overlay loaded'); }); });