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:
@@ -18,9 +18,9 @@ const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const lifecycle = require('../gsd-core/bin/lib/capability-lifecycle.cjs');
|
||||
const ledgerMod = require('../gsd-core/bin/lib/capability-ledger.cjs');
|
||||
const { escapeRegex } = require('../gsd-core/bin/lib/pattern.cjs');
|
||||
const lifecycle = require('../msd-core/bin/lib/capability-lifecycle.cjs');
|
||||
const ledgerMod = require('../msd-core/bin/lib/capability-ledger.cjs');
|
||||
const { escapeRegex } = require('../msd-core/bin/lib/pattern.cjs');
|
||||
const { CAP_MARKER } = lifecycle;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -56,7 +56,7 @@ function declarativeCap(id, version = '1.0.0') {
|
||||
description: 'test capability',
|
||||
tier: 'standard',
|
||||
requires: [],
|
||||
engines: { gsd: '>=1.0.0' },
|
||||
engines: { msd: '>=1.0.0' },
|
||||
runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills: [],
|
||||
agents: [],
|
||||
@@ -88,7 +88,7 @@ function materialize(dir, rel) {
|
||||
function fakeResolve(manifest, { integrity = null, throwErr = null } = {}) {
|
||||
return async (spec, opts) => {
|
||||
if (throwErr) throw new Error(throwErr);
|
||||
const root = path.join(opts.gsdHome, '.gsd', 'capabilities', '.staging');
|
||||
const root = path.join(opts.msdHome, '.msd', 'capabilities', '.staging');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const dir = path.join(root, `${manifest.id}-${++stageCounter}`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
@@ -108,7 +108,7 @@ function readSettings(dir) {
|
||||
}
|
||||
function capManifestVersion(dir, id) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path.join(dir, '.gsd', 'capabilities', id, 'capability.json'), 'utf8')).version;
|
||||
return JSON.parse(fs.readFileSync(path.join(dir, '.msd', 'capabilities', id, 'capability.json'), 'utf8')).version;
|
||||
} catch { return null; }
|
||||
}
|
||||
/**
|
||||
@@ -117,7 +117,7 @@ function capManifestVersion(dir, id) {
|
||||
* (so an ancestor symlink cannot escape). Mirrors confinedBundleScript in the source.
|
||||
*/
|
||||
function expectedBundleCommand(dir, id, script) {
|
||||
const capDir = path.join(dir, '.gsd', 'capabilities', id);
|
||||
const capDir = path.join(dir, '.msd', 'capabilities', id);
|
||||
const target = path.resolve(capDir, script);
|
||||
let parent = path.dirname(target);
|
||||
try { parent = fs.realpathSync(parent); } catch { /* lexical fallback below */ }
|
||||
@@ -137,7 +137,7 @@ test('install: declarative capability installs without consent and records the l
|
||||
const entry = readLedgerEntry(dir, 'decl');
|
||||
assert.ok(entry, 'ledger entry recorded');
|
||||
assert.strictEqual(entry.version, '1.0.0');
|
||||
assert.ok(fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'decl', 'capability.json')));
|
||||
assert.ok(fs.existsSync(path.join(dir, '.msd', 'capabilities', 'decl', 'capability.json')));
|
||||
});
|
||||
|
||||
test('install: executable capability without consent aborts and writes NOTHING', async () => {
|
||||
@@ -149,7 +149,7 @@ test('install: executable capability without consent aborts and writes NOTHING',
|
||||
assert.strictEqual(res.status, 'aborted');
|
||||
assert.strictEqual(res.requiresConsent, true);
|
||||
assert.strictEqual(readLedgerEntry(dir, 'exec'), null, 'no ledger entry');
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'exec')), 'no install dir');
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.msd', 'capabilities', 'exec')), 'no install dir');
|
||||
assert.strictEqual(readSettings(dir), null, 'no settings.json written');
|
||||
});
|
||||
|
||||
@@ -183,17 +183,17 @@ test('install: a disallowed source is blocked BEFORE the resolver runs', async (
|
||||
test('install: a reserved-namespace capability is blocked', async () => {
|
||||
const dir = runtime();
|
||||
const res = await lifecycle.installCapability('./x', {
|
||||
runtimeDir: dir, hostVersion: '1.6.0', _resolve: fakeResolve(declarativeCap('gsd-evil')),
|
||||
runtimeDir: dir, hostVersion: '1.6.0', _resolve: fakeResolve(declarativeCap('msd-evil')),
|
||||
});
|
||||
assert.strictEqual(res.status, 'blocked');
|
||||
assert.ok(res.blockReasons.some((r) => /reserved namespace/.test(r)));
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'gsd-evil')));
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.msd', 'capabilities', 'msd-evil')));
|
||||
});
|
||||
|
||||
test('install: engines mismatch is blocked at install WITH a compatVersions downgrade hint', async () => {
|
||||
const dir = runtime();
|
||||
const cap = declarativeCap('eng', '3.0.0');
|
||||
cap.engines = { gsd: '>=2.0.0' };
|
||||
cap.engines = { msd: '>=2.0.0' };
|
||||
cap.compatVersions = { '1.4.0': '>=1.5.0 <2.0.0' };
|
||||
const res = await lifecycle.installCapability('./eng', {
|
||||
runtimeDir: dir, hostVersion: '1.6.0', _resolve: fakeResolve(cap),
|
||||
@@ -208,13 +208,13 @@ test('integration: engines-incompatible local cap is blocked by the lifecycle, n
|
||||
const src = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-src-eng-'));
|
||||
cleanups.push(src);
|
||||
const cap = declarativeCap('engreal');
|
||||
cap.engines = { gsd: '>=99.0.0' };
|
||||
cap.engines = { msd: '>=99.0.0' };
|
||||
fs.writeFileSync(path.join(src, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
const res = await lifecycle.installCapability(src, { runtimeDir: dir, hostVersion: '1.6.0' });
|
||||
assert.strictEqual(res.status, 'blocked', JSON.stringify(res));
|
||||
assert.ok(res.blockReasons.some((r) => /engines\.gsd/.test(r)));
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'engreal')), 'nothing installed');
|
||||
const staging = path.join(dir, '.gsd', 'capabilities', '.staging');
|
||||
assert.ok(res.blockReasons.some((r) => /engines\.msd/.test(r)));
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.msd', 'capabilities', 'engreal')), 'nothing installed');
|
||||
const staging = path.join(dir, '.msd', 'capabilities', '.staging');
|
||||
assert.deepStrictEqual(fs.existsSync(staging) ? fs.readdirSync(staging) : [], [], 'staging cleaned');
|
||||
});
|
||||
|
||||
@@ -232,13 +232,13 @@ test('install: re-installing over an existing capability (via install, not upgra
|
||||
assert.strictEqual(capManifestVersion(dir, 'e'), '2.0.0');
|
||||
assert.strictEqual(readLedgerEntry(dir, 'e').version, '2.0.0');
|
||||
assert.ok(!readLedgerEntry(dir, 'e')._pending, 'intent cleared on commit');
|
||||
assert.deepStrictEqual(fs.readdirSync(path.join(dir, '.gsd', 'capabilities')).filter((n) => n.includes('.upgrading-')), [], 'no backup left');
|
||||
assert.deepStrictEqual(fs.readdirSync(path.join(dir, '.msd', 'capabilities')).filter((n) => n.includes('.upgrading-')), [], 'no backup left');
|
||||
assert.strictEqual(readSettings(dir).hooks.PostToolUse.length, 1, 'exactly one stamped hook');
|
||||
});
|
||||
|
||||
test('install: a deadman-stale lock is stolen so a crashed prior holder does not block forever', async () => {
|
||||
const dir = runtime();
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
// A legacy no-pid body: liveness cannot be verified, so (finding 1) it is reclaimable ONLY by the
|
||||
// HARD deadman timeout — backdate it past LOCK_DEADMAN_MS (10 min) to simulate a crashed holder the
|
||||
@@ -289,7 +289,7 @@ test('upgrade: same executable set upgrades without re-consent; bundle + ledger
|
||||
assert.strictEqual(res.toVersion, '2.0.0');
|
||||
assert.strictEqual(capManifestVersion(dir, 'e'), '2.0.0');
|
||||
assert.strictEqual(readLedgerEntry(dir, 'e').version, '2.0.0');
|
||||
const leftovers = fs.readdirSync(path.join(dir, '.gsd', 'capabilities')).filter((n) => n.includes('.upgrading-'));
|
||||
const leftovers = fs.readdirSync(path.join(dir, '.msd', 'capabilities')).filter((n) => n.includes('.upgrading-'));
|
||||
assert.deepStrictEqual(leftovers, [], 'no backup dir left behind');
|
||||
// Exactly one stamped hook remains (old stripped, new applied).
|
||||
assert.strictEqual(readSettings(dir).hooks.PostToolUse.length, 1);
|
||||
@@ -403,7 +403,7 @@ test('#1460 (R): a normal script under a normal prefix emits the quoted absolute
|
||||
test('#1460 (R): idempotent strip-then-reapply yields the identical quoted command', () => {
|
||||
const dir = runtime();
|
||||
const capId = 'e';
|
||||
const capDirPath = path.join(dir, '.gsd', 'capabilities', capId);
|
||||
const capDirPath = path.join(dir, '.msd', 'capabilities', capId);
|
||||
fs.mkdirSync(path.join(capDirPath, 'hooks'), { recursive: true });
|
||||
fs.writeFileSync(path.join(capDirPath, 'hooks', 'run.js'), '// x', 'utf8');
|
||||
const manifest = execCap(capId, '1.0.0', { script: 'hooks/run.js' });
|
||||
@@ -428,7 +428,7 @@ test('#1460 (R): idempotent strip-then-reapply yields the identical quoted comma
|
||||
test('#1634: a declared matcher is preserved on the emitted shared-config hook entry', () => {
|
||||
const dir = runtime();
|
||||
const capId = 'toolkit';
|
||||
const capDirPath = path.join(dir, '.gsd', 'capabilities', capId);
|
||||
const capDirPath = path.join(dir, '.msd', 'capabilities', capId);
|
||||
fs.mkdirSync(path.join(capDirPath, 'hooks'), { recursive: true });
|
||||
fs.writeFileSync(path.join(capDirPath, 'hooks', 'genfile-guard.cjs'), '// guard\n', 'utf8');
|
||||
const manifest = declarativeCap(capId, '1.0.0');
|
||||
@@ -445,7 +445,7 @@ test('#1634: a declared matcher is preserved on the emitted shared-config hook e
|
||||
test('#1634: an absent matcher is omitted (match-all) so shipped capabilities stay unchanged', () => {
|
||||
const dir = runtime();
|
||||
const capId = 'plain';
|
||||
const capDirPath = path.join(dir, '.gsd', 'capabilities', capId);
|
||||
const capDirPath = path.join(dir, '.msd', 'capabilities', capId);
|
||||
fs.mkdirSync(path.join(capDirPath, 'hooks'), { recursive: true });
|
||||
fs.writeFileSync(path.join(capDirPath, 'hooks', 'run.js'), '// x\n', 'utf8');
|
||||
const manifest = declarativeCap(capId, '1.0.0');
|
||||
@@ -460,7 +460,7 @@ test('#1634: an absent matcher is omitted (match-all) so shipped capabilities st
|
||||
test('#1634: a .cjs hook command is node-prefixed so it runs without the executable bit', () => {
|
||||
const dir = runtime();
|
||||
const capId = 'toolkit';
|
||||
const capDirPath = path.join(dir, '.gsd', 'capabilities', capId);
|
||||
const capDirPath = path.join(dir, '.msd', 'capabilities', capId);
|
||||
fs.mkdirSync(path.join(capDirPath, 'hooks'), { recursive: true });
|
||||
// Stage at 0644 (no +x) — a git/tarball source that lost the executable bit.
|
||||
fs.writeFileSync(path.join(capDirPath, 'hooks', 'genfile-guard.cjs'), '// guard\n', { encoding: 'utf8', mode: 0o644 });
|
||||
@@ -484,7 +484,7 @@ test('#1634: a .cjs hook command is node-prefixed so it runs without the executa
|
||||
test('#1634: matcher + node-prefix together; marker strip round-trip unaffected', () => {
|
||||
const dir = runtime();
|
||||
const capId = 'toolkit';
|
||||
const capDirPath = path.join(dir, '.gsd', 'capabilities', capId);
|
||||
const capDirPath = path.join(dir, '.msd', 'capabilities', capId);
|
||||
fs.mkdirSync(path.join(capDirPath, 'hooks'), { recursive: true });
|
||||
fs.writeFileSync(path.join(capDirPath, 'hooks', 'genfile-guard.cjs'), '// guard\n', 'utf8');
|
||||
const manifest = declarativeCap(capId, '1.0.0');
|
||||
@@ -504,7 +504,7 @@ test('#1634: matcher + node-prefix together; marker strip round-trip unaffected'
|
||||
|
||||
test('#1460 (R): confinedBundleScript returns null for an unsafe-char script (defense-in-depth)', () => {
|
||||
const dir = runtime();
|
||||
const capDirPath = path.join(dir, '.gsd', 'capabilities', 'e');
|
||||
const capDirPath = path.join(dir, '.msd', 'capabilities', 'e');
|
||||
fs.mkdirSync(capDirPath, { recursive: true });
|
||||
// Even when the file literally exists on disk inside the bundle, an unsafe-char script
|
||||
// name must be refused — so applyCapabilitySharedEdits skips it even if validation were
|
||||
@@ -522,7 +522,7 @@ test('#1460 (R): confinedBundleScript returns null for an unsafe-char script (de
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function seedCapDir(dir, name, manifest) {
|
||||
const d = path.join(dir, '.gsd', 'capabilities', name);
|
||||
const d = path.join(dir, '.msd', 'capabilities', name);
|
||||
fs.mkdirSync(d, { recursive: true });
|
||||
fs.writeFileSync(path.join(d, 'capability.json'), JSON.stringify(manifest), 'utf8');
|
||||
return d;
|
||||
@@ -530,7 +530,7 @@ function seedCapDir(dir, name, manifest) {
|
||||
// Record a ledger entry carrying an in-flight INTENT (the commit signal).
|
||||
function recordPending(dir, id, version, { kind = 'upgrade', backupName, sharedFiles = [], sharedEdits = [] }) {
|
||||
ledgerMod.recordInstall(dir, {
|
||||
id, version, source: 's', integrity: '', files: ['.gsd/capabilities/' + id], sharedEdits,
|
||||
id, version, source: 's', integrity: '', files: ['.msd/capabilities/' + id], sharedEdits,
|
||||
_pending: { kind, backupName, sharedFiles },
|
||||
});
|
||||
}
|
||||
@@ -543,7 +543,7 @@ test('reconcile: crash before new swapped in (final missing, backup present) rol
|
||||
assert.ok(report.rolledBack.includes('c'));
|
||||
assert.strictEqual(capManifestVersion(dir, 'c'), '1.0.0');
|
||||
assert.ok(!readLedgerEntry(dir, 'c')._pending, 'intent cleared');
|
||||
assert.deepStrictEqual(fs.readdirSync(path.join(dir, '.gsd', 'capabilities')).filter((n) => n.includes('.upgrading-')), []);
|
||||
assert.deepStrictEqual(fs.readdirSync(path.join(dir, '.msd', 'capabilities')).filter((n) => n.includes('.upgrading-')), []);
|
||||
});
|
||||
|
||||
test('reconcile: crash after swap before commit (intent present) rolls back to old', () => {
|
||||
@@ -564,7 +564,7 @@ test('reconcile H3: SAME-version malicious bundle with intent present is rolled
|
||||
recordPending(dir, 'c', '1.0.0', { backupName: 'c.upgrading-111-222' });
|
||||
const report = lifecycle.reconcileCapabilities({ runtimeDir: dir });
|
||||
assert.ok(report.rolledBack.includes('c'), 'must roll back despite equal version strings');
|
||||
const live = JSON.parse(fs.readFileSync(path.join(dir, '.gsd', 'capabilities', 'c', 'capability.json'), 'utf8'));
|
||||
const live = JSON.parse(fs.readFileSync(path.join(dir, '.msd', 'capabilities', 'c', 'capability.json'), 'utf8'));
|
||||
assert.strictEqual(live._evil, false, 'the genuine old bundle is restored, not the uncommitted one');
|
||||
});
|
||||
|
||||
@@ -593,16 +593,16 @@ test('reconcile: committed leftover backup (no intent) rolls forward, dropping t
|
||||
seedCapDir(dir, 'c', declarativeCap('c', '2.0.0')); // new, committed, live
|
||||
seedCapDir(dir, 'c.upgrading-111-222', declarativeCap('c', '1.0.0')); // stale backup
|
||||
// Committed: ledger entry has NO _pendingUpgrade.
|
||||
ledgerMod.recordInstall(dir, { id: 'c', version: '2.0.0', source: 's', integrity: '', files: ['.gsd/capabilities/c'], sharedEdits: [] });
|
||||
ledgerMod.recordInstall(dir, { id: 'c', version: '2.0.0', source: 's', integrity: '', files: ['.msd/capabilities/c'], sharedEdits: [] });
|
||||
const report = lifecycle.reconcileCapabilities({ runtimeDir: dir });
|
||||
assert.ok(report.rolledForward.includes('c'));
|
||||
assert.strictEqual(capManifestVersion(dir, 'c'), '2.0.0', 'new kept');
|
||||
assert.deepStrictEqual(fs.readdirSync(path.join(dir, '.gsd', 'capabilities')).filter((n) => n.includes('.upgrading-')), []);
|
||||
assert.deepStrictEqual(fs.readdirSync(path.join(dir, '.msd', 'capabilities')).filter((n) => n.includes('.upgrading-')), []);
|
||||
});
|
||||
|
||||
test('reconcile: an AGED staging orphan is swept, a FRESH one (possible in-flight resolve) is spared', () => {
|
||||
const dir = runtime();
|
||||
const stagingRoot = path.join(dir, '.gsd', 'capabilities', '.staging');
|
||||
const stagingRoot = path.join(dir, '.msd', 'capabilities', '.staging');
|
||||
const aged = path.join(stagingRoot, 'aged-1');
|
||||
const fresh = path.join(stagingRoot, 'fresh-2');
|
||||
fs.mkdirSync(aged, { recursive: true });
|
||||
@@ -630,7 +630,7 @@ test('reconcile H1: a crashed FRESH install (intent kind=install) is rolled back
|
||||
const report = lifecycle.reconcileCapabilities({ runtimeDir: dir });
|
||||
assert.ok(report.rolledBack.includes('f'));
|
||||
assert.strictEqual(readLedgerEntry(dir, 'f'), null, 'half-installed ledger entry removed');
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'f')), 'half-installed dir removed');
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.msd', 'capabilities', 'f')), 'half-installed dir removed');
|
||||
assert.ok(!readSettings(dir).hooks, 'stranded shared edit stripped');
|
||||
});
|
||||
|
||||
@@ -650,7 +650,7 @@ test('reconcile M5: a tampered ledger key (non-kebab id) is skipped, never used
|
||||
entries: {
|
||||
'../../precious': {
|
||||
id: '../../precious', version: '1.0.0', source: 's', integrity: '',
|
||||
files: ['.gsd/capabilities/x'], sharedEdits: [],
|
||||
files: ['.msd/capabilities/x'], sharedEdits: [],
|
||||
_pending: { kind: 'install', backupName: null, sharedFiles: [] },
|
||||
},
|
||||
},
|
||||
@@ -684,9 +684,9 @@ test('reconcile H2: reinstall rollback restores OLD ledger metadata, not the new
|
||||
|
||||
test('remove: a held lock makes remove report "in progress" (does not mutate)', () => {
|
||||
const dir = runtime();
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
ledgerMod.recordInstall(dir, { id: 'q', version: '1.0.0', source: 's', integrity: '', files: ['.gsd/capabilities/q'], sharedEdits: [] });
|
||||
ledgerMod.recordInstall(dir, { id: 'q', version: '1.0.0', source: 's', integrity: '', files: ['.msd/capabilities/q'], sharedEdits: [] });
|
||||
fs.writeFileSync(lockPath, '99999', 'utf8');
|
||||
try {
|
||||
const res = lifecycle.removeCapability('q', { runtimeDir: dir });
|
||||
@@ -700,7 +700,7 @@ test('remove: a held lock makes remove report "in progress" (does not mutate)',
|
||||
test('reconcile: a held lock makes reconcile defer (no-op) and a mutation report "in progress"', async () => {
|
||||
const dir = runtime();
|
||||
// Manually hold the lock (fresh mtime => not stale).
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
fs.writeFileSync(lockPath, '99999', 'utf8');
|
||||
try {
|
||||
@@ -734,7 +734,7 @@ test('remove: deletes recorded files, strips marked shared edits, drops the ledg
|
||||
const res = lifecycle.removeCapability('e', { runtimeDir: dir });
|
||||
assert.strictEqual(res.status, 'removed');
|
||||
assert.strictEqual(readLedgerEntry(dir, 'e'), null);
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'e')));
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.msd', 'capabilities', 'e')));
|
||||
const settings = readSettings(dir);
|
||||
assert.ok(!settings.hooks, 'empty hooks object pruned');
|
||||
assert.ok(!settings.mcpServers, 'empty mcpServers object pruned');
|
||||
@@ -771,7 +771,7 @@ test('remove: tolerates a user having already deleted the shared file and the in
|
||||
_resolve: fakeResolve(execCap('e', '1.0.0')),
|
||||
});
|
||||
cleanup(path.join(dir, 'settings.json'));
|
||||
cleanup(path.join(dir, '.gsd', 'capabilities', 'e'));
|
||||
cleanup(path.join(dir, '.msd', 'capabilities', 'e'));
|
||||
const res = lifecycle.removeCapability('e', { runtimeDir: dir });
|
||||
assert.strictEqual(res.status, 'removed', 'idempotent despite missing artifacts');
|
||||
assert.strictEqual(readLedgerEntry(dir, 'e'), null);
|
||||
@@ -784,11 +784,11 @@ test('remove: a tampered ledger files[] routed through a symlink cannot delete o
|
||||
const victim = path.join(outside, 'victim.txt');
|
||||
fs.writeFileSync(victim, 'precious', 'utf8');
|
||||
// A symlink inside runtimeDir pointing OUT, plus a tampered ledger routing files[] through it.
|
||||
fs.mkdirSync(path.join(dir, '.gsd'), { recursive: true });
|
||||
fs.symlinkSync(outside, path.join(dir, '.gsd', 'link'), 'dir');
|
||||
fs.mkdirSync(path.join(dir, '.msd'), { recursive: true });
|
||||
fs.symlinkSync(outside, path.join(dir, '.msd', 'link'), 'dir');
|
||||
ledgerMod.recordInstall(dir, {
|
||||
id: 'evil', version: '1.0.0', source: 's', integrity: '',
|
||||
files: ['.gsd/link/victim.txt'], sharedEdits: [],
|
||||
files: ['.msd/link/victim.txt'], sharedEdits: [],
|
||||
});
|
||||
lifecycle.removeCapability('evil', { runtimeDir: dir });
|
||||
assert.ok(fs.existsSync(victim), 'a file OUTSIDE runtimeDir (reached via symlink) must NOT be deleted');
|
||||
@@ -800,7 +800,7 @@ test('remove: CAPABILITY_DATA is preserved by default and deleted only on remove
|
||||
runtimeDir: dir, hostVersion: '1.6.0', consentGranted: true,
|
||||
_resolve: fakeResolve(declarativeCap('e')),
|
||||
});
|
||||
const dataDir = path.join(dir, '.gsd', 'capability-data', 'e');
|
||||
const dataDir = path.join(dir, '.msd', 'capability-data', 'e');
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dataDir, 'state.json'), '{}', 'utf8');
|
||||
|
||||
@@ -888,17 +888,17 @@ test('remove: a corrupt-present ledger fails closed with status=blocked — must
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Finding 2 (HIGH): remove must run a READ-ONLY corruption preflight BEFORE acquireLock.
|
||||
// On a corrupt ledger it must NOT create .gsd/capabilities and must NOT create a .lock.
|
||||
// On a corrupt ledger it must NOT create .msd/capabilities and must NOT create a .lock.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('finding-2: removeCapability on a corrupt ledger does NOT acquire a lock or create .gsd/capabilities (preflight precedes acquireLock)', () => {
|
||||
test('finding-2: removeCapability on a corrupt ledger does NOT acquire a lock or create .msd/capabilities (preflight precedes acquireLock)', () => {
|
||||
const dir = runtime();
|
||||
const ledgerPath = path.join(dir, ledgerMod.LEDGER_FILE_NAME);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(ledgerPath, '{ broken json ---');
|
||||
|
||||
const capsRoot = path.join(dir, '.gsd', 'capabilities');
|
||||
assert.ok(!fs.existsSync(capsRoot), 'precondition: .gsd/capabilities must not exist yet');
|
||||
const capsRoot = path.join(dir, '.msd', 'capabilities');
|
||||
assert.ok(!fs.existsSync(capsRoot), 'precondition: .msd/capabilities must not exist yet');
|
||||
|
||||
const result = lifecycle.removeCapability('some-cap', { runtimeDir: dir });
|
||||
|
||||
@@ -909,7 +909,7 @@ test('finding-2: removeCapability on a corrupt ledger does NOT acquire a lock or
|
||||
|
||||
// UNCONDITIONAL: the read-only preflight failed BEFORE acquireLock, so no lock dir/file exists.
|
||||
assert.ok(!fs.existsSync(capsRoot),
|
||||
'.gsd/capabilities must NOT be created by acquireLock when the corruption preflight blocks first');
|
||||
'.msd/capabilities must NOT be created by acquireLock when the corruption preflight blocks first');
|
||||
assert.ok(!fs.existsSync(path.join(capsRoot, '.lock')),
|
||||
'no .lock file may be created when the corruption preflight blocks before acquireLock');
|
||||
});
|
||||
@@ -1029,7 +1029,7 @@ test('#1460 CONF-1: a relative hook script is emitted as the absolute path insid
|
||||
const dir = runtime();
|
||||
const capId = 'conf1';
|
||||
// Make the install dir real so realpath confinement resolves a concrete chain.
|
||||
const capDir = path.join(dir, '.gsd', 'capabilities', capId);
|
||||
const capDir = path.join(dir, '.msd', 'capabilities', capId);
|
||||
fs.mkdirSync(path.join(capDir, 'subdir'), { recursive: true });
|
||||
fs.writeFileSync(path.join(capDir, 'subdir', 'run.js'), '// hook', 'utf8');
|
||||
|
||||
@@ -1053,7 +1053,7 @@ test('#1460 CONF-1: a relative hook script is emitted as the absolute path insid
|
||||
test('#1460 CONF-1: a script resolving OUTSIDE capDir via a symlinked subdir is NOT written (skipped)', (t) => {
|
||||
const dir = runtime();
|
||||
const capId = 'conf1-escape';
|
||||
const capDir = path.join(dir, '.gsd', 'capabilities', capId);
|
||||
const capDir = path.join(dir, '.msd', 'capabilities', capId);
|
||||
fs.mkdirSync(capDir, { recursive: true });
|
||||
|
||||
// Plant a victim file outside the bundle and a symlinked subdir inside the bundle
|
||||
@@ -1139,16 +1139,16 @@ test('reconcileCapabilities: a corrupt-present ledger surfaces a warning in repo
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Finding 2 (HIGH): reconcile must run a READ-ONLY corruption preflight BEFORE acquireLock.
|
||||
// On a corrupt ledger it must warn WITHOUT creating .gsd/capabilities or a .lock.
|
||||
// On a corrupt ledger it must warn WITHOUT creating .msd/capabilities or a .lock.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('finding-2: reconcileCapabilities on a corrupt ledger does NOT acquire a lock or create .gsd/capabilities (preflight precedes acquireLock)', () => {
|
||||
test('finding-2: reconcileCapabilities on a corrupt ledger does NOT acquire a lock or create .msd/capabilities (preflight precedes acquireLock)', () => {
|
||||
const dir = runtime();
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, ledgerMod.LEDGER_FILE_NAME), '{ broken json ---');
|
||||
|
||||
const capsRoot = path.join(dir, '.gsd', 'capabilities');
|
||||
assert.ok(!fs.existsSync(capsRoot), 'precondition: .gsd/capabilities must not exist yet');
|
||||
const capsRoot = path.join(dir, '.msd', 'capabilities');
|
||||
assert.ok(!fs.existsSync(capsRoot), 'precondition: .msd/capabilities must not exist yet');
|
||||
|
||||
const report = lifecycle.reconcileCapabilities({ runtimeDir: dir });
|
||||
|
||||
@@ -1157,7 +1157,7 @@ test('finding-2: reconcileCapabilities on a corrupt ledger does NOT acquire a lo
|
||||
|
||||
// UNCONDITIONAL: the read-only preflight warned BEFORE acquireLock, so no lock dir/file exists.
|
||||
assert.ok(!fs.existsSync(capsRoot),
|
||||
'.gsd/capabilities must NOT be created by acquireLock when the corruption preflight warns first');
|
||||
'.msd/capabilities must NOT be created by acquireLock when the corruption preflight warns first');
|
||||
assert.ok(!fs.existsSync(path.join(capsRoot, '.lock')),
|
||||
'no .lock file may be created when the corruption preflight warns before acquireLock');
|
||||
});
|
||||
@@ -1297,7 +1297,7 @@ test('integration: install a real, valid, declarative local capability through t
|
||||
const res = await lifecycle.installCapability(src, { runtimeDir: dir, hostVersion: '1.6.0' });
|
||||
assert.strictEqual(res.status, 'installed', JSON.stringify(res));
|
||||
assert.strictEqual(res.id, 'realcap');
|
||||
assert.ok(fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'realcap', 'capability.json')));
|
||||
assert.ok(fs.existsSync(path.join(dir, '.msd', 'capabilities', 'realcap', 'capability.json')));
|
||||
assert.ok(readLedgerEntry(dir, 'realcap'), 'ledger entry recorded via real path');
|
||||
});
|
||||
|
||||
@@ -1357,8 +1357,8 @@ test('remove: finding-3 — mid-remove corruption blocks coherently: capability
|
||||
const renameMock = mock.method(fs, 'renameSync', function (src, dst) {
|
||||
renameCount++;
|
||||
// The first rename may be for staging during install setup; skip it.
|
||||
// The ledger commit rename will be for a .tmp.<pid>-<nonce> → .gsd-capabilities.json path.
|
||||
if (typeof dst === 'string' && dst.includes('.gsd-capabilities.json') && renameCount >= 1) {
|
||||
// The ledger commit rename will be for a .tmp.<pid>-<nonce> → .msd-capabilities.json path.
|
||||
if (typeof dst === 'string' && dst.includes('.msd-capabilities.json') && renameCount >= 1) {
|
||||
const err = new Error('EXDEV: cross-device link not permitted');
|
||||
err.code = 'EXDEV';
|
||||
throw err;
|
||||
@@ -1377,11 +1377,11 @@ test('remove: finding-3 — mid-remove corruption blocks coherently: capability
|
||||
result.blockReasons && result.blockReasons.length > 0,
|
||||
'must include blockReasons explaining the failure',
|
||||
);
|
||||
// The error message must NOT reference a non-existent CLI command ('gsd capability reconcile').
|
||||
// The error message must NOT reference a non-existent CLI command ('msd capability reconcile').
|
||||
const reason = result.blockReasons[0] || '';
|
||||
assert.ok(
|
||||
!reason.includes('gsd capability reconcile'),
|
||||
`blockReasons must not reference non-existent CLI subcommand 'gsd capability reconcile'; got: "${reason}"`,
|
||||
!reason.includes('msd capability reconcile'),
|
||||
`blockReasons must not reference non-existent CLI subcommand 'msd capability reconcile'; got: "${reason}"`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1394,20 +1394,20 @@ test('root-fix-2: install on a corrupt-present ledger blocks BEFORE resolving so
|
||||
const dir = runtime();
|
||||
|
||||
// Write a corrupt ledger before attempting install.
|
||||
const ledgerPath = path.join(dir, '.gsd-capabilities.json');
|
||||
const ledgerPath = path.join(dir, '.msd-capabilities.json');
|
||||
fs.writeFileSync(ledgerPath, '{ broken json ---', 'utf8');
|
||||
|
||||
let resolveCalled = false;
|
||||
const trackingResolve = async (spec, opts) => {
|
||||
resolveCalled = true;
|
||||
// Materialize a staging dir so a regression (resolve before strict read) is observable.
|
||||
const root = path.join(opts.gsdHome, '.gsd', 'capabilities', '.staging');
|
||||
const root = path.join(opts.msdHome, '.msd', 'capabilities', '.staging');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const staged = path.join(root, 'preflight-test');
|
||||
fs.mkdirSync(staged, { recursive: true });
|
||||
fs.writeFileSync(path.join(staged, 'capability.json'), JSON.stringify(
|
||||
{ id: 'pf', role: 'feature', version: '1.0.0', title: 'pf', description: 'x',
|
||||
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: [] }
|
||||
), 'utf8');
|
||||
@@ -1434,7 +1434,7 @@ test('root-fix-2: install on a corrupt-present ledger blocks BEFORE resolving so
|
||||
'resolver must NOT be called when the ledger is corrupt (strict read precedes _resolve)');
|
||||
|
||||
// And because resolve never ran, NO staging dir was created.
|
||||
const stagingRoot = path.join(dir, '.gsd', 'capabilities', '.staging');
|
||||
const stagingRoot = path.join(dir, '.msd', 'capabilities', '.staging');
|
||||
assert.ok(
|
||||
!fs.existsSync(stagingRoot),
|
||||
'no .staging dir may be created when the ledger is corrupt (preflight precedes staging)',
|
||||
@@ -1451,19 +1451,19 @@ test('root-fix-2: upgrade on a corrupt-present ledger blocks BEFORE resolving so
|
||||
});
|
||||
|
||||
// Then corrupt the ledger.
|
||||
const ledgerPath = path.join(dir, '.gsd-capabilities.json');
|
||||
const ledgerPath = path.join(dir, '.msd-capabilities.json');
|
||||
fs.writeFileSync(ledgerPath, '{ broken json ---', 'utf8');
|
||||
|
||||
let resolveCalled = false;
|
||||
const trackingResolve = async (spec, opts) => {
|
||||
resolveCalled = true;
|
||||
const root = path.join(opts.gsdHome, '.gsd', 'capabilities', '.staging');
|
||||
const root = path.join(opts.msdHome, '.msd', 'capabilities', '.staging');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const staged = path.join(root, 'preflight-upgrade-test');
|
||||
fs.mkdirSync(staged, { recursive: true });
|
||||
fs.writeFileSync(path.join(staged, 'capability.json'), JSON.stringify(
|
||||
{ id: 'u2', role: 'feature', version: '2.0.0', title: 'u2', description: 'x',
|
||||
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: [] }
|
||||
), 'utf8');
|
||||
@@ -1472,7 +1472,7 @@ test('root-fix-2: upgrade on a corrupt-present ledger blocks BEFORE resolving so
|
||||
|
||||
// Snapshot the .staging dir's prior contents (the successful install above may have left none,
|
||||
// but be precise): the corrupt-upgrade attempt must add NOTHING.
|
||||
const stagingRoot = path.join(dir, '.gsd', 'capabilities', '.staging');
|
||||
const stagingRoot = path.join(dir, '.msd', 'capabilities', '.staging');
|
||||
const before = fs.existsSync(stagingRoot) ? fs.readdirSync(stagingRoot).sort() : [];
|
||||
|
||||
const result = await lifecycle.upgradeCapability('./u2-v2', {
|
||||
@@ -1502,13 +1502,13 @@ test('root-fix-2: corrupt-ledger EXECUTABLE install WITHOUT --yes blocks on CORR
|
||||
const dir = runtime();
|
||||
|
||||
// Write a corrupt ledger before attempting install.
|
||||
const ledgerPath = path.join(dir, '.gsd-capabilities.json');
|
||||
const ledgerPath = path.join(dir, '.msd-capabilities.json');
|
||||
fs.writeFileSync(ledgerPath, '{ broken json ---', 'utf8');
|
||||
|
||||
let resolveCalled = false;
|
||||
const trackingResolve = async (spec, opts) => {
|
||||
resolveCalled = true;
|
||||
const root = path.join(opts.gsdHome, '.gsd', 'capabilities', '.staging');
|
||||
const root = path.join(opts.msdHome, '.msd', 'capabilities', '.staging');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const staged = path.join(root, 'exec-corrupt-test');
|
||||
fs.mkdirSync(staged, { recursive: true });
|
||||
@@ -1549,13 +1549,13 @@ test('root-fix-3: installCapability rejects unsafe id (constructor) before stagi
|
||||
const result = await lifecycle.installCapability('./evil', {
|
||||
runtimeDir: dir, hostVersion: '1.6.0',
|
||||
_resolve: async (spec, opts) => {
|
||||
const root = path.join(opts.gsdHome, '.gsd', 'capabilities', '.staging');
|
||||
const root = path.join(opts.msdHome, '.msd', 'capabilities', '.staging');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const staged = path.join(root, 'unsafe-id-test');
|
||||
fs.mkdirSync(staged, { recursive: true });
|
||||
fs.writeFileSync(path.join(staged, 'capability.json'), JSON.stringify(
|
||||
{ id: 'constructor', role: 'feature', version: '1.0.0', title: 'evil',
|
||||
description: 'evil', tier: 'standard', requires: [], engines: { gsd: '>=1.0.0' },
|
||||
description: 'evil', tier: 'standard', requires: [], engines: { msd: '>=1.0.0' },
|
||||
runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [] }
|
||||
), 'utf8');
|
||||
@@ -1566,10 +1566,10 @@ test('root-fix-3: installCapability rejects unsafe id (constructor) before stagi
|
||||
assert.strictEqual(result.status, 'blocked',
|
||||
`installCapability must block a capability with id='constructor'; got: ${result.status}`);
|
||||
assert.ok(result.blockReasons && result.blockReasons.length > 0, 'must have blockReasons');
|
||||
// Must NOT have installed a bundle at .gsd/capabilities/constructor
|
||||
// Must NOT have installed a bundle at .msd/capabilities/constructor
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'constructor')),
|
||||
'no .gsd/capabilities/constructor bundle must be promoted',
|
||||
!fs.existsSync(path.join(dir, '.msd', 'capabilities', 'constructor')),
|
||||
'no .msd/capabilities/constructor bundle must be promoted',
|
||||
);
|
||||
// Must NOT have a ledger entry for 'constructor'
|
||||
const l = ledgerMod.readLedger(dir);
|
||||
@@ -1584,13 +1584,13 @@ test('root-fix-3: installCapability rejects __proto__ and prototype ids', async
|
||||
const res = await lifecycle.installCapability('./evil', {
|
||||
runtimeDir: dir, hostVersion: '1.6.0',
|
||||
_resolve: async (spec, opts) => {
|
||||
const root = path.join(opts.gsdHome, '.gsd', 'capabilities', '.staging');
|
||||
const root = path.join(opts.msdHome, '.msd', 'capabilities', '.staging');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const staged = path.join(root, `unsafe-${unsafeId}`);
|
||||
fs.mkdirSync(staged, { recursive: true });
|
||||
fs.writeFileSync(path.join(staged, 'capability.json'), JSON.stringify(
|
||||
{ id: unsafeId, role: 'feature', version: '1.0.0', title: 'evil',
|
||||
description: 'evil', tier: 'standard', requires: [], engines: { gsd: '>=1.0.0' },
|
||||
description: 'evil', tier: 'standard', requires: [], engines: { msd: '>=1.0.0' },
|
||||
runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [] }
|
||||
), 'utf8');
|
||||
@@ -1615,13 +1615,13 @@ test('root-fix-3: upgradeCapability rejects unsafe id (constructor) before stagi
|
||||
const result = await lifecycle.upgradeCapability('./evil-upgrade', {
|
||||
runtimeDir: dir, hostVersion: '1.6.0',
|
||||
_resolve: async (spec, opts) => {
|
||||
const root = path.join(opts.gsdHome, '.gsd', 'capabilities', '.staging');
|
||||
const root = path.join(opts.msdHome, '.msd', 'capabilities', '.staging');
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const staged = path.join(root, 'unsafe-upgrade-test');
|
||||
fs.mkdirSync(staged, { recursive: true });
|
||||
fs.writeFileSync(path.join(staged, 'capability.json'), JSON.stringify(
|
||||
{ id: 'constructor', role: 'feature', version: '2.0.0', title: 'evil',
|
||||
description: 'evil', tier: 'standard', requires: [], engines: { gsd: '>=1.0.0' },
|
||||
description: 'evil', tier: 'standard', requires: [], engines: { msd: '>=1.0.0' },
|
||||
runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [] }
|
||||
), 'utf8');
|
||||
@@ -1631,16 +1631,16 @@ test('root-fix-3: upgradeCapability rejects unsafe id (constructor) before stagi
|
||||
|
||||
assert.strictEqual(result.status, 'blocked',
|
||||
`upgradeCapability must block unsafe id='constructor'; got: ${result.status}`);
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'constructor')),
|
||||
'no .gsd/capabilities/constructor bundle must be promoted on upgrade');
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.msd', 'capabilities', 'constructor')),
|
||||
'no .msd/capabilities/constructor bundle must be promoted on upgrade');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FIX 5: removeCapability failure guidance must NOT reference 'gsd capability reconcile'
|
||||
// FIX 5: removeCapability failure guidance must NOT reference 'msd capability reconcile'
|
||||
// (a non-existent CLI subcommand).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('fix-5: removeCapability commit-fail guidance does not reference nonexistent "gsd capability reconcile" subcommand', async (t) => {
|
||||
test('fix-5: removeCapability commit-fail guidance does not reference nonexistent "msd capability reconcile" subcommand', async (t) => {
|
||||
const dir = runtime();
|
||||
|
||||
// Install a capability.
|
||||
@@ -1653,7 +1653,7 @@ test('fix-5: removeCapability commit-fail guidance does not reference nonexisten
|
||||
const { mock } = require('node:test');
|
||||
const realRename = fs.renameSync.bind(fs);
|
||||
const renameMock = mock.method(fs, 'renameSync', function (src, dst) {
|
||||
if (typeof dst === 'string' && dst.includes('.gsd-capabilities.json')) {
|
||||
if (typeof dst === 'string' && dst.includes('.msd-capabilities.json')) {
|
||||
const err = new Error('EXDEV: cross-device link not permitted');
|
||||
err.code = 'EXDEV';
|
||||
throw err;
|
||||
@@ -1667,12 +1667,12 @@ test('fix-5: removeCapability commit-fail guidance does not reference nonexisten
|
||||
|
||||
const reason = (result.blockReasons || []).join(' ');
|
||||
assert.ok(
|
||||
!reason.includes('gsd capability reconcile'),
|
||||
`Failure guidance must not reference non-existent "gsd capability reconcile"; got: "${reason}"`,
|
||||
!reason.includes('msd capability reconcile'),
|
||||
`Failure guidance must not reference non-existent "msd capability reconcile"; got: "${reason}"`,
|
||||
);
|
||||
// Must still mention a useful recovery action (inspect/restore the ledger file).
|
||||
assert.ok(
|
||||
reason.includes('ledger') || reason.includes('.gsd-capabilities.json') || reason.includes('remove'),
|
||||
reason.includes('ledger') || reason.includes('.msd-capabilities.json') || reason.includes('remove'),
|
||||
`Failure guidance must mention the ledger or re-run remove; got: "${reason}"`,
|
||||
);
|
||||
});
|
||||
@@ -1709,7 +1709,7 @@ function lockBody({ pid = process.pid, host = os.hostname(), ts = Date.now(), st
|
||||
}
|
||||
|
||||
function writeLock(dir, body, ageMs) {
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
// Finding 1: age now binds to the BODY's own `ts` for a JSON body (not the file mtime). So a lock
|
||||
// that is `ageMs` old must carry a `ts` that is `ageMs` in the past — backdate BOTH the body ts (for
|
||||
@@ -1957,7 +1957,7 @@ test('finding-1: a JSON lock with a RECENT body `ts` but an artificially-OLD mti
|
||||
const dir = runtime();
|
||||
// pid "dead" so a mtime-derived STALE age would steal it; only ts-bound freshness can protect it.
|
||||
withLockProbes(t, { alive: false, startTime: 'START-A' });
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
// Body `ts` is NOW (fresh replacement); file mtime is backdated 2 minutes (would look stale).
|
||||
const freshBody = JSON.stringify({ token: `${process.pid}-${Date.now()}-1`, pid: process.pid, hostname: os.hostname(), startTime: 'START-A', ts: Date.now() });
|
||||
@@ -1992,7 +1992,7 @@ test('finding-1: an inode change between the steal-decision and the rename cause
|
||||
const dir = runtime();
|
||||
const { mock } = require('node:test');
|
||||
withLockProbes(t, { alive: false, startTime: 'START-A' }); // dead pid → otherwise steal-eligible
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
// Stale legacy body (no ts → mtime age), backdated past the deadman so the steal branch is reached.
|
||||
const body = 'legacy-token-no-pid';
|
||||
@@ -2044,7 +2044,7 @@ test('finding-1: a body `ts` change between the steal-decision and the rename ca
|
||||
const dir = runtime();
|
||||
const { mock } = require('node:test');
|
||||
withLockProbes(t, { alive: false, startTime: 'START-A' }); // dead pid → steal-eligible if stale
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
// DECISION body: OLD ts (stale → steal-eligible with a dead pid). RECHECK body: fresh ts (B's swap).
|
||||
const oldTs = Date.now() - 2 * 60 * 1000;
|
||||
@@ -2122,7 +2122,7 @@ test('finding-3: a body `ts` going NULL between the steal-decision and the renam
|
||||
const dir = runtime();
|
||||
const { mock } = require('node:test');
|
||||
withLockProbes(t, { alive: false, startTime: 'START-A' }); // dead pid → steal-eligible if stale
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
// DECISION body: a JSON body with a non-null OLD ts (stale → steal-eligible with a dead pid).
|
||||
// RECHECK body: a no-`ts` legacy/garbage body on the SAME inode (B replaced the body content).
|
||||
@@ -2199,7 +2199,7 @@ test('finding-3: a body `ts` going NULL between the steal-decision and the renam
|
||||
test('finding-2: a FIFO lock body does NOT hang acquireLock — treated as unparseable (deadman policy)', (t) => {
|
||||
const dir = runtime();
|
||||
withLockProbes(t, { alive: true, startTime: 'START-A' });
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
if (!tryMkfifoLife(lockPath)) { t.skip('mkfifo unavailable on this platform'); return; }
|
||||
const old = new Date(Date.now() - 2 * 60 * 1000); // stale, under the deadman
|
||||
@@ -2227,7 +2227,7 @@ test('finding-2: a FIFO lock body does NOT hang acquireLock — treated as unpar
|
||||
test('finding-3/2: a body-write (writeFileSync) failure after the exclusive create leaves NO orphan .lock behind', (t) => {
|
||||
const dir = runtime();
|
||||
const { mock } = require('node:test');
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
|
||||
// Let the exclusive create succeed (real openSync), then force the body write to fail. The body MUST
|
||||
@@ -2259,7 +2259,7 @@ test('finding-3/2: a body-write (writeFileSync) failure after the exclusive crea
|
||||
test('finding-3: a closeSync failure after the exclusive create+write leaves NO orphan .lock behind', (t) => {
|
||||
const dir = runtime();
|
||||
const { mock } = require('node:test');
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
|
||||
// Let the exclusive create AND the body write succeed; force ONLY the .lock fd's closeSync to fail.
|
||||
@@ -2303,7 +2303,7 @@ test('finding-1: a lock whose JSON body `ts` is far in the FUTURE is still recla
|
||||
// Different host + past the deadman by MTIME so the deadman branch reclaims it once the future ts is
|
||||
// distrusted. (A future ts under the trusting code would compute a negative age → never stale.)
|
||||
withLockProbes(t, { alive: true, startTime: 'START-A' });
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
// Body ts is 1 hour in the FUTURE; file mtime is backdated 11 min (past the deadman).
|
||||
const futureBody = JSON.stringify({
|
||||
@@ -2340,7 +2340,7 @@ test('finding-1: a lock whose JSON body `ts` is far in the FUTURE is still recla
|
||||
// token re-check (body token != handle.token) is what prevents the delete.
|
||||
test('finding-4: releaseLock does NOT delete a SUCCESSOR lock (different token) at the same path', () => {
|
||||
const dir = runtime();
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
|
||||
// The original holder acquires (captures its token + dev/ino in the handle).
|
||||
const handle = lifecycle.acquireLock(dir);
|
||||
@@ -2369,7 +2369,7 @@ test('finding-4: releaseLock does NOT delete a SUCCESSOR lock (different token)
|
||||
// over-strict (the dev/ino captured at acquire matches the unchanged on-disk lock → rmSync runs).
|
||||
test('finding-4: releaseLock STILL deletes our own unchanged lock (inode guard is not over-strict)', () => {
|
||||
const dir = runtime();
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
const handle = lifecycle.acquireLock(dir);
|
||||
assert.ok(handle, 'must acquire');
|
||||
assert.ok(fs.existsSync(lockPath), 'lock present before release');
|
||||
@@ -2389,7 +2389,7 @@ test('finding-4: releaseLock STILL deletes our own unchanged lock (inode guard i
|
||||
test('CONC-2: acquireLock returns null on contention exhaustion WITHOUT a stack overflow (bounded loop)', (t) => {
|
||||
const dir = runtime();
|
||||
const { mock } = require('node:test');
|
||||
const lockPath = path.join(dir, '.gsd', 'capabilities', '.lock');
|
||||
const lockPath = path.join(dir, '.msd', 'capabilities', '.lock');
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
|
||||
// TV-06/07/11/17: a FAITHFUL pathological-contention sim. Write a REAL, stale, DEAD-pid JSON lock body
|
||||
@@ -2573,7 +2573,7 @@ test('DUR-3: promoteStagingToFinal fsyncs the parent directory after BOTH rename
|
||||
_resolve: fakeResolve(declarativeCap('e', '1.0.0')),
|
||||
});
|
||||
|
||||
const capsRoot = path.join(dir, '.gsd', 'capabilities');
|
||||
const capsRoot = path.join(dir, '.msd', 'capabilities');
|
||||
const { mock } = require('node:test');
|
||||
const realOpen = fs.openSync.bind(fs);
|
||||
const realFsync = fs.fsyncSync.bind(fs);
|
||||
@@ -2646,7 +2646,7 @@ function withLifecycleDirFsyncError(t, capsRoot, errno) {
|
||||
// this "must be blocked" assertion fails.
|
||||
test('finding-4: a NON-tolerated dir-fsync errno (EIO) during install promotion surfaces as blocked (durability not silently claimed)', async (t) => {
|
||||
const dir = runtime();
|
||||
const capsRoot = path.join(dir, '.gsd', 'capabilities');
|
||||
const capsRoot = path.join(dir, '.msd', 'capabilities');
|
||||
fs.mkdirSync(capsRoot, { recursive: true });
|
||||
const { closed } = withLifecycleDirFsyncError(t, capsRoot, 'EIO');
|
||||
|
||||
@@ -2665,7 +2665,7 @@ test('finding-4: a NON-tolerated dir-fsync errno (EIO) during install promotion
|
||||
// would block the install, so this 'installed' assertion fails.
|
||||
test('finding-4: a TOLERATED dir-fsync errno (EISDIR) during install promotion is ignored (install succeeds)', async (t) => {
|
||||
const dir = runtime();
|
||||
const capsRoot = path.join(dir, '.gsd', 'capabilities');
|
||||
const capsRoot = path.join(dir, '.msd', 'capabilities');
|
||||
fs.mkdirSync(capsRoot, { recursive: true });
|
||||
const { closed } = withLifecycleDirFsyncError(t, capsRoot, 'EISDIR');
|
||||
|
||||
@@ -2702,14 +2702,14 @@ test('DUR-6: reconcile upgrade-rollback renames backup→final atomically (no rm
|
||||
// order is restored, finalDir is rmSync'd first; the new order must rename the backup over
|
||||
// finalDir directly (renameSync replaces atomically), so no rmSync of finalDir occurs.
|
||||
const { mock } = require('node:test');
|
||||
const finalDir = path.join(dir, '.gsd', 'capabilities', 'c');
|
||||
const finalDir = path.join(dir, '.msd', 'capabilities', 'c');
|
||||
const realRm = fs.rmSync.bind(fs);
|
||||
const realRename = fs.renameSync.bind(fs); // capture BEFORE mocking
|
||||
let finalDirRmBeforeRename = false;
|
||||
let backupRenamedToFinal = false;
|
||||
const renameMock = mock.method(fs, 'renameSync', function (src, dst, ...rest) {
|
||||
if (typeof src === 'string' && typeof dst === 'string'
|
||||
&& path.resolve(src) === path.resolve(path.join(dir, '.gsd', 'capabilities', backupName))
|
||||
&& path.resolve(src) === path.resolve(path.join(dir, '.msd', 'capabilities', backupName))
|
||||
&& path.resolve(dst) === path.resolve(finalDir)) {
|
||||
backupRenamedToFinal = true;
|
||||
}
|
||||
@@ -2804,7 +2804,7 @@ test('W-6: a throwing per-entry mutation does not abort reconcile (warns and con
|
||||
|
||||
const { mock } = require('node:test');
|
||||
const realExists = fs.existsSync.bind(fs);
|
||||
const badBackupPath = path.join(dir, '.gsd', 'capabilities', badBackup);
|
||||
const badBackupPath = path.join(dir, '.msd', 'capabilities', badBackup);
|
||||
// existsSync(backupDir) is a direct per-entry call in reconcile's step-1 loop (outside the inner
|
||||
// restore try/catch) — making it throw for bad-cap exercises the W-6 per-entry catch.
|
||||
const existsMock = mock.method(fs, 'existsSync', function (p) {
|
||||
@@ -2833,13 +2833,13 @@ test('W-6: a throwing per-entry mutation does not abort reconcile (warns and con
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// W-3 / DUR-5 (LOW): reconcile step-2 must sweep stale `.gsd-capabilities.json.tmp.*`
|
||||
// W-3 / DUR-5 (LOW): reconcile step-2 must sweep stale `.msd-capabilities.json.tmp.*`
|
||||
// orphan temp files (older than a threshold) from the runtime dir.
|
||||
// Revert-fails: remove the stale-temp sweep → the old orphan temp file remains
|
||||
// after reconcile, so the "orphan removed" assertion fails.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('W-3/DUR-5: reconcile sweeps a stale .gsd-capabilities.json.tmp.* orphan from the runtime dir', () => {
|
||||
test('W-3/DUR-5: reconcile sweeps a stale .msd-capabilities.json.tmp.* orphan from the runtime dir', () => {
|
||||
const dir = runtime();
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
// A committed, valid ledger so reconcile proceeds past the corruption preflight.
|
||||
@@ -2863,8 +2863,8 @@ test('W-3/DUR-5: reconcile sweeps a stale .gsd-capabilities.json.tmp.* orphan fr
|
||||
// Consent store binding on project install / upgrade / remove (#1459)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const consentMod = require('../gsd-core/bin/lib/capability-consent.cjs');
|
||||
const trustMod = require('../gsd-core/bin/lib/capability-trust.cjs');
|
||||
const consentMod = require('../msd-core/bin/lib/capability-consent.cjs');
|
||||
const trustMod = require('../msd-core/bin/lib/capability-trust.cjs');
|
||||
|
||||
/** A consent home OUTSIDE the project tree (user-owned). */
|
||||
function consentHome() {
|
||||
@@ -2875,11 +2875,11 @@ function consentHome() {
|
||||
|
||||
/**
|
||||
* #1459 CB-1/CB-2: the SECURITY binding `hasProjectConsent` checks is the RECOMPUTED full-bundle
|
||||
* content hash over the INSTALLED capDir (`<runtimeDir>/.gsd/capabilities/<id>`) — exactly what the
|
||||
* content hash over the INSTALLED capDir (`<runtimeDir>/.msd/capabilities/<id>`) — exactly what the
|
||||
* loader recomputes at load. Tests assert consent presence by recomputing the same hash here.
|
||||
*/
|
||||
function installedCapDir(runtimeDir, id) {
|
||||
return path.join(runtimeDir, '.gsd', 'capabilities', id);
|
||||
return path.join(runtimeDir, '.msd', 'capabilities', id);
|
||||
}
|
||||
function installedContentHash(runtimeDir, id) {
|
||||
return consentMod.bundleContentHash(installedCapDir(runtimeDir, id));
|
||||
@@ -2900,7 +2900,7 @@ test('install (project scope, consented): writes a consent record under the cons
|
||||
// manifest while consent still matched; the contentHash binds the whole bundle.
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({
|
||||
gsdHome: home, projectRoot: dir, id: 'proj-decl',
|
||||
msdHome: home, projectRoot: dir, id: 'proj-decl',
|
||||
contentHash: installedContentHash(dir, 'proj-decl'),
|
||||
}),
|
||||
true,
|
||||
@@ -2908,7 +2908,7 @@ test('install (project scope, consented): writes a consent record under the cons
|
||||
);
|
||||
// It is under the consent HOME, not under the project runtimeDir.
|
||||
assert.ok(fs.existsSync(consentMod.consentStorePath(home)), 'store under consent home');
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.gsd', 'consent.json')), 'NOT written inside the project');
|
||||
assert.ok(!fs.existsSync(path.join(dir, '.msd', 'consent.json')), 'NOT written inside the project');
|
||||
});
|
||||
|
||||
test('install (project scope, executable + consented): consent record matches the executable disclosure signature', async () => {
|
||||
@@ -2926,7 +2926,7 @@ test('install (project scope, executable + consented): consent record matches th
|
||||
// deactivates (loader test). The stored disclosureSignature (incl. env) remains for the UX layer.
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({
|
||||
gsdHome: home, projectRoot: dir, id: 'proj-exec',
|
||||
msdHome: home, projectRoot: dir, id: 'proj-exec',
|
||||
contentHash: installedContentHash(dir, 'proj-exec'),
|
||||
}),
|
||||
true,
|
||||
@@ -2961,14 +2961,14 @@ test('remove (project scope): revokes the consent record', async () => {
|
||||
});
|
||||
const rmHash = installedContentHash(dir, 'proj-rm');
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: dir, id: 'proj-rm', contentHash: rmHash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: dir, id: 'proj-rm', contentHash: rmHash }),
|
||||
true,
|
||||
'consent present after install',
|
||||
);
|
||||
const rm = lifecycle.removeCapability('proj-rm', { runtimeDir: dir, scope: 'project', consentStoreDir: home });
|
||||
assert.strictEqual(rm.status, 'removed');
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: dir, id: 'proj-rm', contentHash: rmHash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: dir, id: 'proj-rm', contentHash: rmHash }),
|
||||
false,
|
||||
'remove fully revokes the consent record',
|
||||
);
|
||||
@@ -2978,7 +2978,7 @@ test('remove (project scope): revokes the consent record', async () => {
|
||||
// (round-3). removeCapability must NOT silently swallow that throw and still report a clean
|
||||
// 'removed' — that leaves a STALE consent record a byte-identical re-drop + forged ledger could
|
||||
// reactivate against. The revoke failure must be SURFACED (a stderr warning naming the record AND
|
||||
// a flag in the returned result) so the user knows to clear it (`gsd capability trust revoke`).
|
||||
// a flag in the returned result) so the user knows to clear it (`msd capability trust revoke`).
|
||||
function plantFreshConsentLockLife(home) {
|
||||
const lockPath = consentMod.consentLockPath(home);
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
@@ -3004,7 +3004,7 @@ test('remove (project scope): a revoke-on-lock-failure is SURFACED, not swallowe
|
||||
});
|
||||
const rmHash = installedContentHash(dir, 'proj-rm-lk');
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: dir, id: 'proj-rm-lk', contentHash: rmHash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: dir, id: 'proj-rm-lk', contentHash: rmHash }),
|
||||
true, 'consent present after install',
|
||||
);
|
||||
// Hold the consent-store lock so the revoke inside remove cannot acquire it → revokeProjectConsent throws.
|
||||
@@ -3024,7 +3024,7 @@ test('remove (project scope): a revoke-on-lock-failure is SURFACED, not swallowe
|
||||
);
|
||||
// The consent record is STALE (could not be revoked) — confirming the failure was real, not a no-op.
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: dir, id: 'proj-rm-lk', contentHash: rmHash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: dir, id: 'proj-rm-lk', contentHash: rmHash }),
|
||||
true, 'the consent record is left STALE (revoke was blocked by the held lock) — the user must clear it',
|
||||
);
|
||||
});
|
||||
@@ -3052,7 +3052,7 @@ test('upgrade (project scope, consented): re-records the consent for the new ver
|
||||
const v2Hash = installedContentHash(dir, 'proj-up');
|
||||
assert.notStrictEqual(v1Hash, v2Hash, 'precondition: the v1 and v2 bundles hash differently');
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: dir, id: 'proj-up', contentHash: v2Hash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: dir, id: 'proj-up', contentHash: v2Hash }),
|
||||
true,
|
||||
'consent re-recorded against the upgraded bundle content hash',
|
||||
);
|
||||
@@ -3060,7 +3060,7 @@ test('upgrade (project scope, consented): re-records the consent for the new ver
|
||||
// OLD version may linger. revert-fails: if the upgrade ADDED a new record instead of overwriting (or
|
||||
// left the v1 binding around), the store would carry 2 records and/or the OLD hash would still match.
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: dir, id: 'proj-up', contentHash: v1Hash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: dir, id: 'proj-up', contentHash: v1Hash }),
|
||||
false,
|
||||
'the OLD-version content hash no longer matches (no stale consent record)',
|
||||
);
|
||||
@@ -3073,7 +3073,7 @@ test('upgrade (project scope, consented): re-records the consent for the new ver
|
||||
// looks up via consentProjectRoot = realpath(findProjectRoot(cwd))) finds it from any descendant.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { loadRegistry } = require('../gsd-core/bin/lib/capability-loader.cjs');
|
||||
const { loadRegistry } = require('../msd-core/bin/lib/capability-loader.cjs');
|
||||
|
||||
test('D (IC-01/CB-4): install --scope project from a SUBDIR → cap is ACTIVE (record key matches loader lookup)', async () => {
|
||||
// revert-fails: if the RECORD site bound consent to realpath(subdir) and the loader looked it up at
|
||||
@@ -3097,7 +3097,7 @@ test('D (IC-01/CB-4): install --scope project from a SUBDIR → cap is ACTIVE (r
|
||||
assert.strictEqual(res.status, 'installed');
|
||||
// Load the registry FROM the nested subdir, pointing the consent home at the same store. The loader
|
||||
// resolves the project root (findProjectRoot finds the .planning/ marker) and finds the record.
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: subdir, hostVersion: '1.6.0' });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: subdir, hostVersion: '1.6.0' });
|
||||
assert.ok(reg.capabilities && reg.capabilities['subdir-cap'], 'cap ACTIVE when loaded from a subdir (no install-then-inactive)');
|
||||
assert.strictEqual(reg.bySkill['subdir-skill'], 'subdir-cap', 'skill surface present from the subdir');
|
||||
});
|
||||
@@ -3126,35 +3126,35 @@ test('IC-03: reconcile rollback of a deleted project bundle revokes consent →
|
||||
assert.strictEqual(installed.status, 'installed');
|
||||
const consentedHash = installedContentHash(dir, 'redrop-cap');
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: dir, id: 'redrop-cap', contentHash: consentedHash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: dir, id: 'redrop-cap', contentHash: consentedHash }),
|
||||
true, 'consent present after install',
|
||||
);
|
||||
|
||||
// 2. Simulate a crashed/interrupted state: mark the entry as an in-flight (uncommitted) install and
|
||||
// delete its on-disk bundle dir. reconcile's install-rollback path then drops the entry.
|
||||
recordPending(dir, 'redrop-cap', '1.0.0', { kind: 'install', backupName: null, sharedFiles: [] });
|
||||
cleanup(path.join(dir, '.gsd', 'capabilities', 'redrop-cap')); // delete the on-disk bundle dir (helpers.cleanup: Windows-EBUSY retry budget)
|
||||
cleanup(path.join(dir, '.msd', 'capabilities', 'redrop-cap')); // delete the on-disk bundle dir (helpers.cleanup: Windows-EBUSY retry budget)
|
||||
|
||||
// 3. Reconcile WITH the consent context — the rollback must revoke the stale consent.
|
||||
const report = lifecycle.reconcileCapabilities({ runtimeDir: dir, scope: 'project', consentStoreDir: home });
|
||||
assert.ok(report.rolledBack.includes('redrop-cap'), 'the deleted-bundle entry is rolled back');
|
||||
assert.strictEqual(
|
||||
consentMod.hasProjectConsent({ gsdHome: home, projectRoot: dir, id: 'redrop-cap', contentHash: consentedHash }),
|
||||
consentMod.hasProjectConsent({ msdHome: home, projectRoot: dir, id: 'redrop-cap', contentHash: consentedHash }),
|
||||
false, 'reconcile rollback revoked the now-stale consent record',
|
||||
);
|
||||
|
||||
// 4. Re-drop the BYTE-IDENTICAL bundle + a committed (forged) project ledger — no new consent.
|
||||
const reDir = path.join(dir, '.gsd', 'capabilities', 'redrop-cap');
|
||||
const reDir = path.join(dir, '.msd', 'capabilities', 'redrop-cap');
|
||||
fs.mkdirSync(reDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(reDir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
assert.strictEqual(consentMod.bundleContentHash(reDir), consentedHash, 'precondition: the re-drop is byte-identical (same hash)');
|
||||
fs.writeFileSync(path.join(dir, '.gsd-capabilities.json'), JSON.stringify({
|
||||
fs.writeFileSync(path.join(dir, '.msd-capabilities.json'), JSON.stringify({
|
||||
version: '1', updatedAt: '2026-01-01T00:00:00Z',
|
||||
entries: { 'redrop-cap': { id: 'redrop-cap', version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } },
|
||||
}), 'utf8');
|
||||
|
||||
// 5. The loader must NOT re-activate the identical re-drop — consent was revoked.
|
||||
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: dir, hostVersion: '1.6.0' });
|
||||
const reg = loadRegistry({ includeInstalled: true, msdHome: home, cwd: dir, hostVersion: '1.6.0' });
|
||||
assert.ok(reg.capabilities['redrop-cap'] === undefined, 'an identical re-drop stays INACTIVE after the rollback revoked consent');
|
||||
});
|
||||
|
||||
@@ -3216,7 +3216,7 @@ test('IC-05/WIN-2: a consent-store write failure leaves the install status:insta
|
||||
}
|
||||
void realRecord;
|
||||
assert.strictEqual(res.status, 'installed', 'a consent-store IO error must NOT fail an otherwise-successful install');
|
||||
assert.ok(fs.existsSync(path.join(dir, '.gsd', 'capabilities', 'rofs-cap', 'capability.json')), 'the bundle is committed on disk');
|
||||
assert.ok(fs.existsSync(path.join(dir, '.msd', 'capabilities', 'rofs-cap', 'capability.json')), 'the bundle is committed on disk');
|
||||
assert.match(buf, /capability consent:/i, 'a consent diagnostic was written to stderr');
|
||||
assert.match(buf, /could not write the consent record/i, 'the warning explains the write failure');
|
||||
assert.match(buf, new RegExp(escapeRegex(home)), 'the warning names the consent store path');
|
||||
@@ -3230,7 +3230,7 @@ test('IC-05/WIN-2: a consent-store write failure leaves the install status:insta
|
||||
function plantEntry(dir, id, version, source) {
|
||||
ledgerMod.recordInstall(dir, {
|
||||
id, version, source, integrity: '',
|
||||
files: [`.gsd/capabilities/${id}`], sharedEdits: [],
|
||||
files: [`.msd/capabilities/${id}`], sharedEdits: [],
|
||||
});
|
||||
}
|
||||
/** A git ls-remote --tags line for a tag. */
|
||||
@@ -3298,7 +3298,7 @@ test('#1463 outdated: npm newer → outdated; npm peek error → unknown, other
|
||||
test('#1463 outdated: tarball → manual; registry → unknown', () => {
|
||||
const dir = runtime();
|
||||
plantEntry(dir, 'tarcap', '1.0.0', 'https://host/path/cap-1.0.0.tgz');
|
||||
plantEntry(dir, 'regcap', '1.0.0', 'my-cap@gsd-registry');
|
||||
plantEntry(dir, 'regcap', '1.0.0', 'my-cap@msd-registry');
|
||||
const recs = lifecycle.outdatedCapabilities({ runtimeDir: dir });
|
||||
const byId = Object.fromEntries(recs.map((r) => [r.id, r]));
|
||||
assert.strictEqual(byId.tarcap.status, 'manual');
|
||||
|
||||
Reference in New Issue
Block a user