Make install + surface read the registry's derived profileMembership/ capabilityClusters so a capability's tier drives what installs + surfaces. resolveProfile (when given the registry) unions capability skills for the profiles its tier implies before the requires: closure; resolveSurface merges capabilityClusters into the cluster map. bin/install.js, /gsd:surface, and the capability-state resolver all thread the registry. Shipped as a proven no-op: the UI capability is reconciled to tier:full (its skills were full-only in the hand-authored profiles), so it contributes only to the full profile (already the '*' sentinel) and core/standard are unchanged. Equivalence tests prove resolveProfile/resolveSurface/listSurface/staging/ capability-state are identical with vs without the registry; the core-alias staging path is verified equivalent (empty manifest → raw PROFILES.core). Closes #949 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
787 lines
31 KiB
JavaScript
787 lines
31 KiB
JavaScript
'use strict';
|
|
/**
|
|
* capability-consumption.test.cjs — ADR-857 phase 4c
|
|
*
|
|
* Tests that resolveProfile and resolveSurface correctly CONSUME the capability
|
|
* registry (capabilityClusters + profileMembership) in an additive, no-op-when-
|
|
* current manner.
|
|
*
|
|
* Test categories:
|
|
* 1. RECONCILIATION — registry profileMembership.ui is {tier:'full',profiles:['full']}
|
|
* 2. EQUIVALENCE — registry present with REAL registry = same result as absent
|
|
* 3. FUNCTIONAL — synthetic registry adds capability skills at correct tiers
|
|
* 4. SURFACE EQUIVALENCE — resolveSurface with vs without real registry
|
|
* 5. SURFACE FUNCTIONAL — synthetic registry cluster merge + disable-ability
|
|
* 6. LIVE-PATH EQUIVALENCE — resolveSurface with disabledClusters/adds/removes, real registry
|
|
* 7. listSurface EQUIVALENCE — listSurface with vs without real registry
|
|
* 8. FIX-3 CORE INSTALL — synthetic tier:core capability skills in stageSkillsForProfile
|
|
* 9. FIX-4 DIVERGENCE GUARD — colliding capId + CLUSTERS name with diff value → hand-authored wins
|
|
* 10. FIX-5 MALFORMED-ARRAY — non-array capabilityClusters entry is skipped without throw
|
|
*/
|
|
|
|
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 { resolveProfile, loadSkillsManifest, stageSkillsForProfile } = require('../gsd-core/bin/lib/install-profiles.cjs');
|
|
const { resolveSurface, writeSurface, listSurface } = require('../gsd-core/bin/lib/surface.cjs');
|
|
const realRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
|
const { cleanup } = require('./helpers.cjs');
|
|
|
|
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
|
|
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function tmpDir() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cap-cons-'));
|
|
}
|
|
|
|
/** Compare two Sets for equality (membership only). */
|
|
function setsEqual(a, b) {
|
|
if (!(a instanceof Set) || !(b instanceof Set)) return false;
|
|
if (a.size !== b.size) return false;
|
|
for (const x of a) if (!b.has(x)) return false;
|
|
return true;
|
|
}
|
|
|
|
function setDiff(a, b) {
|
|
const extra = [...a].filter(x => !b.has(x));
|
|
const missing = [...b].filter(x => !a.has(x));
|
|
return { extra, missing };
|
|
}
|
|
|
|
// ─── 1. RECONCILIATION ──────────────────────────────────────────────────────
|
|
|
|
describe('registry reconciliation: UI tier=full', () => {
|
|
test('profileMembership.ui.tier is "full"', () => {
|
|
assert.strictEqual(
|
|
realRegistry.profileMembership.ui.tier,
|
|
'full',
|
|
'After tier:full reconciliation, profileMembership.ui.tier must be "full"'
|
|
);
|
|
});
|
|
|
|
test('profileMembership.ui.profiles contains only ["full"]', () => {
|
|
assert.deepStrictEqual(
|
|
realRegistry.profileMembership.ui.profiles,
|
|
['full'],
|
|
'tier:full capability should only appear in the full profile'
|
|
);
|
|
});
|
|
|
|
test('capabilityClusters.ui is ["ui-phase","ui-review"]', () => {
|
|
assert.deepStrictEqual(
|
|
realRegistry.capabilityClusters.ui,
|
|
['ui-phase', 'ui-review'],
|
|
'capabilityClusters.ui should list both UI skills'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── 2. EQUIVALENCE: resolveProfile ─────────────────────────────────────────
|
|
//
|
|
// For every named profile, resolveProfile with and without the real registry
|
|
// MUST produce identical skill and agent sets.
|
|
//
|
|
// The real registry's UI capability is tier:full → only included in 'full' profile.
|
|
// 'full' early-returns '*' (sentinel path) regardless of registry, so the real
|
|
// registry adds NOTHING for any current profile. This is the no-op proof.
|
|
|
|
describe('resolveProfile equivalence: real registry is a no-op', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
|
|
for (const profile of ['core', 'standard', 'full']) {
|
|
test(`profile="${profile}" — same skills with vs without real registry`, () => {
|
|
const without = resolveProfile({ modes: [profile], manifest });
|
|
const withReg = resolveProfile({ modes: [profile], manifest, registry: realRegistry });
|
|
|
|
if (without.skills === '*') {
|
|
assert.strictEqual(withReg.skills, '*',
|
|
`${profile}: sentinel path must be preserved with registry`);
|
|
} else {
|
|
assert.ok(
|
|
setsEqual(without.skills, withReg.skills),
|
|
`${profile}: skills differ. Extra: ${[...setDiff(withReg.skills, without.skills).extra]}. Missing: ${[...setDiff(withReg.skills, without.skills).missing]}`
|
|
);
|
|
assert.ok(
|
|
setsEqual(without.agents, withReg.agents),
|
|
`${profile}: agents differ`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
test('profile="core,standard" composed — no-op with real registry', () => {
|
|
const without = resolveProfile({ modes: ['core', 'standard'], manifest });
|
|
const withReg = resolveProfile({ modes: ['core', 'standard'], manifest, registry: realRegistry });
|
|
assert.ok(
|
|
setsEqual(without.skills, withReg.skills),
|
|
'composed profile: skills must be identical with and without real registry'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── 3. FUNCTIONAL: synthetic registry ──────────────────────────────────────
|
|
|
|
describe('resolveProfile functional: synthetic registry adds capability skills', () => {
|
|
// Minimal manifest: foo-skill exists (capability-owned), help exists (profile base)
|
|
const manifest = new Map([
|
|
['help', []],
|
|
['foo-skill', []],
|
|
['bar-skill', ['foo-skill']], // bar depends on foo (transitive test)
|
|
['_calls_agents_help', []],
|
|
['_calls_agents_foo-skill', []],
|
|
['_calls_agents_bar-skill', []],
|
|
]);
|
|
// Synthetic registry: 'foo' capability is in core/standard/full
|
|
const syntheticRegistry = {
|
|
capabilityClusters: { foo: ['foo-skill'] },
|
|
profileMembership: { foo: { tier: 'core', profiles: ['core', 'standard', 'full'] } },
|
|
};
|
|
// Override profiles so 'core' does NOT include foo-skill in its base list
|
|
const profilesOverride = {
|
|
core: ['help'],
|
|
standard: ['help'],
|
|
full: '*',
|
|
};
|
|
|
|
test('core profile WITHOUT registry: foo-skill absent', () => {
|
|
const result = resolveProfile({ modes: ['core'], manifest, _profilesOverride: profilesOverride });
|
|
assert.ok(result.skills instanceof Set, 'skills must be a Set');
|
|
assert.ok(!result.skills.has('foo-skill'), 'foo-skill should NOT be in core without registry');
|
|
});
|
|
|
|
test('core profile WITH synthetic registry: foo-skill is included', () => {
|
|
const result = resolveProfile({
|
|
modes: ['core'],
|
|
manifest,
|
|
_profilesOverride: profilesOverride,
|
|
registry: syntheticRegistry,
|
|
});
|
|
assert.ok(result.skills instanceof Set, 'skills must be a Set');
|
|
assert.ok(result.skills.has('foo-skill'),
|
|
'foo-skill should be in core when capability registry maps it to core');
|
|
assert.ok(result.skills.has('help'),
|
|
'core base skill must still be present');
|
|
});
|
|
|
|
test('standard profile WITH synthetic registry: foo-skill is included', () => {
|
|
const result = resolveProfile({
|
|
modes: ['standard'],
|
|
manifest,
|
|
_profilesOverride: profilesOverride,
|
|
registry: syntheticRegistry,
|
|
});
|
|
assert.ok(result.skills.has('foo-skill'),
|
|
'foo-skill should be in standard when capability registry maps it to standard');
|
|
});
|
|
|
|
test('transitive closure: capability skill that requires another skill pulls it in', () => {
|
|
const transitiveRegistry = {
|
|
capabilityClusters: { bar: ['bar-skill'] },
|
|
profileMembership: { bar: { tier: 'core', profiles: ['core', 'standard', 'full'] } },
|
|
};
|
|
const result = resolveProfile({
|
|
modes: ['core'],
|
|
manifest,
|
|
_profilesOverride: profilesOverride,
|
|
registry: transitiveRegistry,
|
|
});
|
|
assert.ok(result.skills.has('bar-skill'),
|
|
'bar-skill from capability cluster should be included');
|
|
assert.ok(result.skills.has('foo-skill'),
|
|
'foo-skill should be pulled in transitively (bar-skill requires foo-skill)');
|
|
});
|
|
|
|
test('full profile with synthetic registry: returns sentinel (not skill set)', () => {
|
|
// full always returns '*' — synthetic registry must not break this
|
|
const result = resolveProfile({
|
|
modes: ['full'],
|
|
manifest,
|
|
_profilesOverride: profilesOverride,
|
|
registry: syntheticRegistry,
|
|
});
|
|
assert.strictEqual(result.skills, '*',
|
|
'full profile must always return "*" sentinel regardless of registry');
|
|
});
|
|
|
|
test('capability whose profiles do NOT include mode: skill not added', () => {
|
|
const fullOnlyRegistry = {
|
|
capabilityClusters: { foo: ['foo-skill'] },
|
|
profileMembership: { foo: { tier: 'full', profiles: ['full'] } },
|
|
};
|
|
const result = resolveProfile({
|
|
modes: ['core'],
|
|
manifest,
|
|
_profilesOverride: profilesOverride,
|
|
registry: fullOnlyRegistry,
|
|
});
|
|
assert.ok(!result.skills.has('foo-skill'),
|
|
'foo-skill must NOT appear in core when capability only maps to full');
|
|
});
|
|
|
|
test('malformed registry (missing capabilityClusters) is tolerated — no throw', () => {
|
|
const badRegistry = { profileMembership: { foo: { tier: 'core', profiles: ['core'] } } };
|
|
assert.doesNotThrow(() => {
|
|
resolveProfile({ modes: ['core'], manifest, _profilesOverride: profilesOverride, registry: badRegistry });
|
|
});
|
|
});
|
|
|
|
test('malformed registry (non-array skills) is tolerated — no throw', () => {
|
|
const badRegistry = {
|
|
capabilityClusters: { foo: 'not-an-array' },
|
|
profileMembership: { foo: { tier: 'core', profiles: ['core'] } },
|
|
};
|
|
assert.doesNotThrow(() => {
|
|
resolveProfile({ modes: ['core'], manifest, _profilesOverride: profilesOverride, registry: badRegistry });
|
|
});
|
|
});
|
|
|
|
test('prototype pollution guard: __proto__ key in capabilityClusters is skipped', () => {
|
|
const pollutionRegistry = {
|
|
capabilityClusters: { __proto__: ['foo-skill'], foo: ['foo-skill'] },
|
|
profileMembership: { __proto__: { tier: 'core', profiles: ['core'] }, foo: { tier: 'core', profiles: ['core'] } },
|
|
};
|
|
// Should not throw and should not corrupt Object.prototype
|
|
assert.doesNotThrow(() => {
|
|
resolveProfile({
|
|
modes: ['core'],
|
|
manifest,
|
|
_profilesOverride: profilesOverride,
|
|
registry: pollutionRegistry,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── 4. SURFACE EQUIVALENCE ─────────────────────────────────────────────────
|
|
|
|
describe('resolveSurface equivalence: real registry is a no-op', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
|
|
function makeSurfaceDir(profile, disabledClusters) {
|
|
const dir = tmpDir();
|
|
writeSurface(dir, {
|
|
baseProfile: profile,
|
|
disabledClusters: disabledClusters || [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
return dir;
|
|
}
|
|
|
|
for (const profile of ['core', 'standard', 'full']) {
|
|
test(`profile="${profile}" surface — same skills with vs without real registry`, (t) => {
|
|
const dir1 = makeSurfaceDir(profile, []);
|
|
const dir2 = makeSurfaceDir(profile, []);
|
|
t.after(() => { cleanup(dir1); cleanup(dir2); });
|
|
|
|
const without = resolveSurface(dir1, manifest);
|
|
const withReg = resolveSurface(dir2, manifest, undefined, realRegistry);
|
|
|
|
if (without.skills === '*' || withReg.skills === '*') {
|
|
// Both should be sets after resolveSurface materializes full
|
|
assert.ok(
|
|
setsEqual(without.skills, withReg.skills),
|
|
`${profile}: sentinel mismatch between with/without registry`
|
|
);
|
|
} else {
|
|
assert.ok(
|
|
setsEqual(without.skills, withReg.skills),
|
|
`${profile}: surface skills differ. Extra: ${[...setDiff(withReg.skills, without.skills).extra]}. Missing: ${[...setDiff(withReg.skills, without.skills).missing]}`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── 5. SURFACE FUNCTIONAL ──────────────────────────────────────────────────
|
|
|
|
describe('resolveSurface functional: synthetic registry cluster merge', () => {
|
|
const manifest = new Map([
|
|
['help', []],
|
|
['foo-skill', []],
|
|
['_calls_agents_help', []],
|
|
['_calls_agents_foo-skill', []],
|
|
]);
|
|
|
|
// resolveSurface calls resolveProfile internally without _profilesOverride, so we test
|
|
// the cluster merge path using disabledClusters + a full base profile (which materializes
|
|
// all manifest skills, so foo-skill is present before any cluster disable).
|
|
|
|
|
|
test('synthetic capability cluster is disable-able via disabledClusters', (t) => {
|
|
// Write a surface state that disables the synthetic capability cluster by its capId key.
|
|
const dir = tmpDir();
|
|
t.after(() => cleanup(dir));
|
|
|
|
// Use 'full' base profile — resolveSurface materializes all manifest skills.
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: ['foo'], // disable the synthetic capability cluster
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
const syntheticRegistry = {
|
|
capabilityClusters: { foo: ['foo-skill'] },
|
|
profileMembership: { foo: { tier: 'full', profiles: ['full'] } },
|
|
};
|
|
|
|
const without = resolveSurface(dir, manifest);
|
|
const withReg = resolveSurface(dir, manifest, undefined, syntheticRegistry);
|
|
|
|
// Without registry: 'foo' is not a known cluster key, so disabledClusters=['foo']
|
|
// removes nothing → foo-skill stays enabled.
|
|
assert.ok(without.skills.has('foo-skill'),
|
|
'without registry, foo-skill should remain (foo cluster unknown)');
|
|
|
|
// With registry: 'foo' cluster is known, disabledClusters=['foo'] removes foo-skill.
|
|
assert.ok(!withReg.skills.has('foo-skill'),
|
|
'with registry, foo-skill should be disabled when its capability cluster is in disabledClusters');
|
|
|
|
// Non-capability skills must not be affected.
|
|
assert.ok(withReg.skills.has('help'),
|
|
'help should remain enabled (not in foo capability cluster)');
|
|
});
|
|
|
|
test('resolveSurface with absent registry still returns correct result', (t) => {
|
|
const dir = tmpDir();
|
|
t.after(() => cleanup(dir));
|
|
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
assert.doesNotThrow(() => {
|
|
resolveSurface(dir, manifest);
|
|
});
|
|
});
|
|
|
|
test('resolveSurface with malformed registry.capabilityClusters is tolerated', (t) => {
|
|
const dir = tmpDir();
|
|
t.after(() => cleanup(dir));
|
|
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
const badRegistry = { capabilityClusters: null, profileMembership: {} };
|
|
assert.doesNotThrow(() => {
|
|
resolveSurface(dir, manifest, undefined, badRegistry);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── 6. LIVE-PATH EQUIVALENCE ───────────────────────────────────────────────
|
|
//
|
|
// FIX 6: prove no-op across the LIVE paths (disabledClusters, adds, removes,
|
|
// non-empty base profile) with the real registry vs without.
|
|
|
|
describe('resolveSurface live-path equivalence: real registry is a no-op', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
|
|
test('disabledClusters:["ui"] surface — same with vs without real registry', (t) => {
|
|
// 'ui' exists in CLUSTERS (the hand-authored map) so the registry's entry
|
|
// must be equal (4a gate) — disable outcome identical with or without registry.
|
|
const dir1 = tmpDir();
|
|
const dir2 = tmpDir();
|
|
t.after(() => { cleanup(dir1); cleanup(dir2); });
|
|
|
|
for (const dir of [dir1, dir2]) {
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: ['ui'],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
}
|
|
|
|
const without = resolveSurface(dir1, manifest);
|
|
const withReg = resolveSurface(dir2, manifest, undefined, realRegistry);
|
|
|
|
assert.ok(
|
|
setsEqual(without.skills, withReg.skills),
|
|
`disabledClusters:["ui"] — skills differ. Extra: ${[...setDiff(withReg.skills, without.skills).extra]}. Missing: ${[...setDiff(withReg.skills, without.skills).missing]}`
|
|
);
|
|
});
|
|
|
|
test('explicit adds — same with vs without real registry', (t) => {
|
|
const dir1 = tmpDir();
|
|
const dir2 = tmpDir();
|
|
t.after(() => { cleanup(dir1); cleanup(dir2); });
|
|
|
|
for (const dir of [dir1, dir2]) {
|
|
writeSurface(dir, {
|
|
baseProfile: 'core',
|
|
disabledClusters: [],
|
|
explicitAdds: ['review'],
|
|
explicitRemoves: [],
|
|
});
|
|
}
|
|
|
|
const without = resolveSurface(dir1, manifest);
|
|
const withReg = resolveSurface(dir2, manifest, undefined, realRegistry);
|
|
|
|
assert.ok(
|
|
setsEqual(without.skills, withReg.skills),
|
|
`explicit adds — skills differ. Extra: ${[...setDiff(withReg.skills, without.skills).extra]}. Missing: ${[...setDiff(withReg.skills, without.skills).missing]}`
|
|
);
|
|
});
|
|
|
|
test('explicit removes — same with vs without real registry', (t) => {
|
|
const dir1 = tmpDir();
|
|
const dir2 = tmpDir();
|
|
t.after(() => { cleanup(dir1); cleanup(dir2); });
|
|
|
|
for (const dir of [dir1, dir2]) {
|
|
writeSurface(dir, {
|
|
baseProfile: 'standard',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: ['review'],
|
|
});
|
|
}
|
|
|
|
const without = resolveSurface(dir1, manifest);
|
|
const withReg = resolveSurface(dir2, manifest, undefined, realRegistry);
|
|
|
|
assert.ok(
|
|
setsEqual(without.skills, withReg.skills),
|
|
`explicit removes — skills differ. Extra: ${[...setDiff(withReg.skills, without.skills).extra]}. Missing: ${[...setDiff(withReg.skills, without.skills).missing]}`
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── 7. listSurface EQUIVALENCE ─────────────────────────────────────────────
|
|
//
|
|
// FIX 6: prove listSurface is a no-op with real registry.
|
|
|
|
describe('listSurface equivalence: real registry is a no-op', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
|
|
for (const profile of ['core', 'standard', 'full']) {
|
|
test(`listSurface profile="${profile}" — enabled/disabled/tokenCost identical with vs without real registry`, (t) => {
|
|
const dir1 = tmpDir();
|
|
const dir2 = tmpDir();
|
|
t.after(() => { cleanup(dir1); cleanup(dir2); });
|
|
|
|
for (const dir of [dir1, dir2]) {
|
|
writeSurface(dir, {
|
|
baseProfile: profile,
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
}
|
|
|
|
const without = listSurface(dir1, manifest);
|
|
const withReg = listSurface(dir2, manifest, undefined, realRegistry);
|
|
|
|
assert.deepStrictEqual(without.enabled, withReg.enabled,
|
|
`${profile}: enabled list differs`);
|
|
assert.deepStrictEqual(without.disabled, withReg.disabled,
|
|
`${profile}: disabled list differs`);
|
|
assert.strictEqual(without.tokenCost, withReg.tokenCost,
|
|
`${profile}: tokenCost differs`);
|
|
});
|
|
}
|
|
|
|
test('listSurface with disabledClusters:["utility"] — identical with vs without real registry', (t) => {
|
|
const dir1 = tmpDir();
|
|
const dir2 = tmpDir();
|
|
t.after(() => { cleanup(dir1); cleanup(dir2); });
|
|
|
|
for (const dir of [dir1, dir2]) {
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: ['utility'],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
}
|
|
|
|
const without = listSurface(dir1, manifest);
|
|
const withReg = listSurface(dir2, manifest, undefined, realRegistry);
|
|
|
|
assert.deepStrictEqual(without.enabled, withReg.enabled,
|
|
'disabled utility cluster: enabled list differs');
|
|
assert.deepStrictEqual(without.disabled, withReg.disabled,
|
|
'disabled utility cluster: disabled list differs');
|
|
});
|
|
});
|
|
|
|
// ─── 8. FIX-3 CORE INSTALL ─────────────────────────────────────────────────
|
|
//
|
|
// FIX 6: stageSkillsForProfile with a registry-aware _resolvedProfile for the
|
|
// core profile includes synthetic tier:core capability skills.
|
|
// Today (real registry, tier:full): stageSkillsForProfile(core) == PROFILES.core.
|
|
// With a synthetic tier:core registry: capability skill is present in staged set.
|
|
|
|
describe('FIX-3 core install: stageSkillsForProfile honors tier:core capabilities', () => {
|
|
test('real registry: core-profile staged set equals PROFILES.core (no-op today)', (_t) => {
|
|
const coreManifest = new Map(); // empty — core has no transitive deps
|
|
const { PROFILES } = require('../gsd-core/bin/lib/install-profiles.cjs');
|
|
const expectedCore = new Set(PROFILES.core);
|
|
|
|
const resolvedWithReg = resolveProfile({
|
|
modes: ['core'],
|
|
manifest: coreManifest,
|
|
registry: realRegistry,
|
|
});
|
|
assert.ok(resolvedWithReg.skills instanceof Set, 'core profile must return a Set (not sentinel)');
|
|
assert.ok(
|
|
setsEqual(resolvedWithReg.skills, expectedCore),
|
|
`core profile with real registry differs from PROFILES.core. Extra: ${[...setDiff(resolvedWithReg.skills, expectedCore).extra]}. Missing: ${[...setDiff(resolvedWithReg.skills, expectedCore).missing]}`
|
|
);
|
|
});
|
|
|
|
test('synthetic tier:core registry: capability skill appears in stageSkillsForProfile output', (t) => {
|
|
// We cannot add a real skill file, but we can verify the resolved profile
|
|
// contains the synthetic skill from the registry — the staged set is profile-driven.
|
|
// The synthetic skill 'foo-skill' is not in commands/gsd, but resolveProfile WOULD
|
|
// include it. We verify the profile-level inclusion here; stageSkillsForProfile
|
|
// would then copy it if a file existed.
|
|
const syntheticManifest = new Map([
|
|
['help', []],
|
|
['foo-skill', []],
|
|
['_calls_agents_help', []],
|
|
['_calls_agents_foo-skill', []],
|
|
]);
|
|
const profilesOverride = { core: ['help'], standard: ['help'], full: '*' };
|
|
const syntheticRegistry = {
|
|
capabilityClusters: { foo: ['foo-skill'] },
|
|
profileMembership: { foo: { tier: 'core', profiles: ['core', 'standard', 'full'] } },
|
|
};
|
|
|
|
const resolved = resolveProfile({
|
|
modes: ['core'],
|
|
manifest: syntheticManifest,
|
|
_profilesOverride: profilesOverride,
|
|
registry: syntheticRegistry,
|
|
});
|
|
assert.ok(resolved.skills instanceof Set, 'should return Set');
|
|
assert.ok(resolved.skills.has('foo-skill'),
|
|
'tier:core capability skill must appear in core install resolved profile (FIX-3)');
|
|
|
|
// Verify stageSkillsForProfile on a temp dir with a synthetic foo-skill.md
|
|
const synSrcDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-fix3-src-'));
|
|
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-fix3-stage-'));
|
|
t.after(() => { cleanup(synSrcDir); cleanup(stageDir); });
|
|
|
|
fs.writeFileSync(path.join(synSrcDir, 'help.md'), '---\ndescription: help\n---\n', 'utf8');
|
|
fs.writeFileSync(path.join(synSrcDir, 'foo-skill.md'), '---\ndescription: foo\n---\n', 'utf8');
|
|
|
|
// stageSkillsForProfile returns a temp dir with files matching resolved.skills
|
|
const staged = stageSkillsForProfile(synSrcDir, resolved);
|
|
t.after(() => cleanup(staged));
|
|
|
|
const stagedFiles = fs.readdirSync(staged);
|
|
assert.ok(stagedFiles.includes('foo-skill.md'),
|
|
'stageSkillsForProfile must include foo-skill.md for tier:core capability (FIX-3)');
|
|
assert.ok(stagedFiles.includes('help.md'),
|
|
'stageSkillsForProfile must include help.md (base skill)');
|
|
});
|
|
|
|
test('FIX-3 documented behavior: minimal==core; tier:core capability IS in minimal', () => {
|
|
// MINIMAL_SKILL_ALLOWLIST === [...PROFILES.core] — explicitly verified.
|
|
// MINIMAL_ALLOWLIST_SET (internal) is new Set(MINIMAL_SKILL_ALLOWLIST).
|
|
// Any tier:core capability DOES belong in minimal/core install.
|
|
// This test documents and asserts that equivalence.
|
|
const { MINIMAL_SKILL_ALLOWLIST, PROFILES } = require('../gsd-core/bin/lib/install-profiles.cjs');
|
|
const minimalSet = new Set(MINIMAL_SKILL_ALLOWLIST);
|
|
const profilesCore = new Set(PROFILES.core);
|
|
assert.ok(
|
|
setsEqual(minimalSet, profilesCore),
|
|
'MINIMAL_SKILL_ALLOWLIST must equal PROFILES.core — minimal IS the core profile'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── 9. FIX-4 DIVERGENCE GUARD ─────────────────────────────────────────────
|
|
//
|
|
// FIX 6: when a capabilityClusters entry collides with a CLUSTERS key but has
|
|
// DIFFERENT values, the hand-authored CLUSTERS value must win (no silent override).
|
|
|
|
describe('FIX-4 divergence guard: colliding capId uses hand-authored CLUSTERS value', () => {
|
|
// Construct a scenario where a capId matches a real CLUSTERS key but the
|
|
// registry reports a different (shorter) skill list. The disable behavior
|
|
// must use the hand-authored (longer/correct) list.
|
|
test('colliding capId with different value: hand-authored wins for disabledClusters', (t) => {
|
|
const dir = tmpDir();
|
|
t.after(() => cleanup(dir));
|
|
|
|
// Use 'utility' as the colliding cluster — it exists in the real CLUSTERS map.
|
|
// The hand-authored utility cluster has several members.
|
|
// The divergent registry would report a shorter list (fewer skills).
|
|
const { CLUSTERS } = require('../gsd-core/bin/lib/clusters.cjs');
|
|
const realUtilitySkills = CLUSTERS.utility; // e.g. ['health', 'stats', ...]
|
|
assert.ok(Array.isArray(realUtilitySkills) && realUtilitySkills.length >= 1,
|
|
'test pre-condition: utility cluster must have at least one skill');
|
|
|
|
// Create a divergent registry that reports a single-skill utility cluster
|
|
// (different from the real multi-skill one).
|
|
const divergentRegistry = {
|
|
capabilityClusters: { utility: [realUtilitySkills[0]] }, // only first skill
|
|
profileMembership: { utility: { tier: 'full', profiles: ['full'] } },
|
|
};
|
|
|
|
// Build a minimal manifest covering the real utility skills
|
|
const manifestEntries = [['help', []]];
|
|
const agentEntries = [['_calls_agents_help', []]];
|
|
for (const s of realUtilitySkills) {
|
|
manifestEntries.push([s, []]);
|
|
agentEntries.push([`_calls_agents_${s}`, []]);
|
|
}
|
|
const manifest = new Map([...manifestEntries, ...agentEntries]);
|
|
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: ['utility'],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
// Without registry: uses hand-authored CLUSTERS.utility (all skills disabled)
|
|
const without = resolveSurface(dir, manifest);
|
|
// With divergent registry: hand-authored wins, so result is still all disabled
|
|
const withDiv = resolveSurface(dir, manifest, undefined, divergentRegistry);
|
|
|
|
// All hand-authored utility skills must be disabled in BOTH cases.
|
|
for (const s of realUtilitySkills) {
|
|
assert.ok(!without.skills.has(s),
|
|
`without registry: ${s} must be disabled (utility cluster disabled)`);
|
|
assert.ok(!withDiv.skills.has(s),
|
|
`with divergent registry: ${s} must be disabled (hand-authored wins over divergent registry)`);
|
|
}
|
|
|
|
// The results must be identical — divergent registry must not change outcomes.
|
|
assert.ok(
|
|
setsEqual(without.skills, withDiv.skills),
|
|
`divergent registry changed outcome vs hand-authored CLUSTERS — skills differ. Extra: ${[...setDiff(withDiv.skills, without.skills).extra]}. Missing: ${[...setDiff(withDiv.skills, without.skills).missing]}`
|
|
);
|
|
});
|
|
|
|
test('non-colliding capId: new capability cluster is merged normally', (t) => {
|
|
const dir = tmpDir();
|
|
t.after(() => cleanup(dir));
|
|
|
|
const manifest = new Map([
|
|
['help', []],
|
|
['novel-skill', []],
|
|
['_calls_agents_help', []],
|
|
['_calls_agents_novel-skill', []],
|
|
]);
|
|
|
|
// 'novel-cap' does NOT exist in CLUSTERS → no collision → merged normally
|
|
const nonCollidingRegistry = {
|
|
capabilityClusters: { 'novel-cap': ['novel-skill'] },
|
|
profileMembership: { 'novel-cap': { tier: 'full', profiles: ['full'] } },
|
|
};
|
|
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: ['novel-cap'],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
const withReg = resolveSurface(dir, manifest, undefined, nonCollidingRegistry);
|
|
|
|
// novel-skill must be disabled (novel-cap cluster is disabled and registry provided it)
|
|
assert.ok(!withReg.skills.has('novel-skill'),
|
|
'novel-skill must be disabled via non-colliding capability cluster');
|
|
assert.ok(withReg.skills.has('help'),
|
|
'help must remain enabled');
|
|
});
|
|
});
|
|
|
|
// ─── 10. FIX-5 MALFORMED-ARRAY GUARD ───────────────────────────────────────
|
|
//
|
|
// FIX 6: non-array capabilityClusters entries must be silently skipped,
|
|
// never causing a throw from clustersToSkills or resolveSurface.
|
|
|
|
describe('FIX-5 malformed-array guard: non-array cluster values are skipped', () => {
|
|
const manifest = new Map([
|
|
['help', []],
|
|
['foo-skill', []],
|
|
['_calls_agents_help', []],
|
|
['_calls_agents_foo-skill', []],
|
|
]);
|
|
|
|
test('non-array capabilityClusters value (string): no throw, foo-skill not disabled', (t) => {
|
|
const dir = tmpDir();
|
|
t.after(() => cleanup(dir));
|
|
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: ['foo'],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
const badRegistry = {
|
|
capabilityClusters: { foo: 'not-an-array' },
|
|
profileMembership: { foo: { tier: 'full', profiles: ['full'] } },
|
|
};
|
|
|
|
let result;
|
|
assert.doesNotThrow(() => {
|
|
result = resolveSurface(dir, manifest, undefined, badRegistry);
|
|
}, 'non-array cluster value must not throw');
|
|
|
|
// 'foo' cluster had a non-array value → skipped → disabledClusters=['foo']
|
|
// removes nothing → foo-skill stays enabled.
|
|
assert.ok(result.skills.has('foo-skill'),
|
|
'foo-skill must remain enabled when cluster value is non-array (FIX-5)');
|
|
});
|
|
|
|
test('non-array capabilityClusters value (object): no throw', (t) => {
|
|
const dir = tmpDir();
|
|
t.after(() => cleanup(dir));
|
|
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
const badRegistry = {
|
|
capabilityClusters: { foo: { not: 'an-array' } },
|
|
profileMembership: { foo: { tier: 'full', profiles: ['full'] } },
|
|
};
|
|
|
|
assert.doesNotThrow(() => {
|
|
resolveSurface(dir, manifest, undefined, badRegistry);
|
|
}, 'object-valued cluster must not throw (FIX-5)');
|
|
});
|
|
|
|
test('null capabilityClusters value: no throw', (t) => {
|
|
const dir = tmpDir();
|
|
t.after(() => cleanup(dir));
|
|
|
|
writeSurface(dir, {
|
|
baseProfile: 'full',
|
|
disabledClusters: [],
|
|
explicitAdds: [],
|
|
explicitRemoves: [],
|
|
});
|
|
|
|
const badRegistry = {
|
|
capabilityClusters: { foo: null },
|
|
profileMembership: { foo: { tier: 'full', profiles: ['full'] } },
|
|
};
|
|
|
|
assert.doesNotThrow(() => {
|
|
resolveSurface(dir, manifest, undefined, badRegistry);
|
|
}, 'null cluster value must not throw (FIX-5)');
|
|
});
|
|
});
|