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:
@@ -2,7 +2,7 @@
|
||||
|
||||
/**
|
||||
* Tests for the user-owned capability CONSENT STORE — issue #1459 (capability trust model
|
||||
* bypassable). The consent store lives OUTSIDE any repo, at ${GSD_HOME||homedir()}/.gsd/consent.json,
|
||||
* bypassable). The consent store lives OUTSIDE any repo, at ${MSD_HOME||homedir()}/.msd/consent.json,
|
||||
* and binds each project-scope third-party capability activation to a user decision made on THIS
|
||||
* machine. A forged/cloned project ledger can no longer activate anything — activation requires a
|
||||
* matching consent record the user wrote here.
|
||||
@@ -12,7 +12,7 @@
|
||||
* hook script, or an empty-integrity local install all change the hash and fail to match. `integrity`
|
||||
* and `disclosureSignature` are kept on the record for the disclosure/re-consent UX, NOT the binding.
|
||||
*
|
||||
* Covers: path resolution (GSD_HOME honored, never under a repo), bundleContentHash (deterministic,
|
||||
* Covers: path resolution (MSD_HOME honored, never under a repo), bundleContentHash (deterministic,
|
||||
* tamper-sensitive, symlink/non-regular rejected, bounded), non-throwing bounded read, prototype-
|
||||
* pollution-safe keys, atomic round-trip, the contentHash match, revoke, concurrency (CONSENT-
|
||||
* CONCURRENCY-1), MAX_RECORDS at WRITE (CONSENT-MAXRECORDS-WRITE-1), and the WIN-3 space-boundary
|
||||
@@ -29,7 +29,7 @@ const crypto = require('node:crypto');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const consent = require('../gsd-core/bin/lib/capability-consent.cjs');
|
||||
const consent = require('../msd-core/bin/lib/capability-consent.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
@@ -66,34 +66,34 @@ function makeBundle(opts) {
|
||||
// consentStorePath
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('consentStorePath: honors an explicit gsdHome (store under <home>/.gsd/consent.json)', () => {
|
||||
test('consentStorePath: honors an explicit msdHome (store under <home>/.msd/consent.json)', () => {
|
||||
const home = tmpDir();
|
||||
try {
|
||||
assert.strictEqual(consent.consentStorePath(home), path.join(home, '.gsd', 'consent.json'));
|
||||
assert.strictEqual(consent.consentStorePath(home), path.join(home, '.msd', 'consent.json'));
|
||||
} finally {
|
||||
cleanup(home);
|
||||
}
|
||||
});
|
||||
|
||||
test('consentStorePath: honors GSD_HOME env when no arg is given', () => {
|
||||
test('consentStorePath: honors MSD_HOME env when no arg is given', () => {
|
||||
const home = tmpDir();
|
||||
const prev = process.env.GSD_HOME;
|
||||
const prev = process.env.MSD_HOME;
|
||||
try {
|
||||
process.env.GSD_HOME = home;
|
||||
assert.strictEqual(consent.consentStorePath(), path.join(home, '.gsd', 'consent.json'));
|
||||
process.env.MSD_HOME = home;
|
||||
assert.strictEqual(consent.consentStorePath(), path.join(home, '.msd', 'consent.json'));
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.GSD_HOME; else process.env.GSD_HOME = prev;
|
||||
if (prev === undefined) delete process.env.MSD_HOME; else process.env.MSD_HOME = prev;
|
||||
cleanup(home);
|
||||
}
|
||||
});
|
||||
|
||||
test('consentStorePath: falls back to homedir() when neither arg nor GSD_HOME is set', () => {
|
||||
const prev = process.env.GSD_HOME;
|
||||
test('consentStorePath: falls back to homedir() when neither arg nor MSD_HOME is set', () => {
|
||||
const prev = process.env.MSD_HOME;
|
||||
try {
|
||||
delete process.env.GSD_HOME;
|
||||
assert.strictEqual(consent.consentStorePath(), path.join(os.homedir(), '.gsd', 'consent.json'));
|
||||
delete process.env.MSD_HOME;
|
||||
assert.strictEqual(consent.consentStorePath(), path.join(os.homedir(), '.msd', 'consent.json'));
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.GSD_HOME; else process.env.GSD_HOME = prev;
|
||||
if (prev === undefined) delete process.env.MSD_HOME; else process.env.MSD_HOME = prev;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -375,7 +375,7 @@ test('bundleContentHash (finding 4): two files whose NAMES differ only in invali
|
||||
// This requires a filesystem that PERMITS arbitrary (invalid-UTF-8) filename bytes. Linux ext4/tmpfs
|
||||
// do; macOS APFS/HFS+ REJECT illegal byte sequences at create time (EILSEQ). When the fs refuses the
|
||||
// create, the vulnerable path is unreachable on this fs — skip rather than fail (the defect is fs-
|
||||
// observable only where such filenames can exist; gsd-test's Linux docker leg covers it).
|
||||
// observable only where such filenames can exist; msd-test's Linux docker leg covers it).
|
||||
const dirA = fs.realpathSync(tmpDir('cap-consent-pathA-'));
|
||||
const dirB = fs.realpathSync(tmpDir('cap-consent-pathB-'));
|
||||
try {
|
||||
@@ -426,7 +426,7 @@ test('readConsentStore: missing store returns an empty records map (non-throwing
|
||||
test('readConsentStore: corrupt JSON returns an empty records map (non-throwing)', () => {
|
||||
const home = tmpDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
fs.writeFileSync(consent.consentStorePath(home), '{ not valid json', 'utf8');
|
||||
assert.deepStrictEqual(consent.readConsentStore(home), { records: {} });
|
||||
} finally {
|
||||
@@ -437,7 +437,7 @@ test('readConsentStore: corrupt JSON returns an empty records map (non-throwing)
|
||||
test('readConsentStore: wrong-shape store (records not an object) returns an empty map', () => {
|
||||
const home = tmpDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
fs.writeFileSync(consent.consentStorePath(home), JSON.stringify({ version: '1', records: [1, 2, 3] }), 'utf8');
|
||||
assert.deepStrictEqual(consent.readConsentStore(home), { records: {} });
|
||||
} finally {
|
||||
@@ -448,7 +448,7 @@ test('readConsentStore: wrong-shape store (records not an object) returns an emp
|
||||
test('readConsentStore: a record missing contentHash is dropped (fail closed)', () => {
|
||||
const home = tmpDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
// A legacy/tampered record with no contentHash binding must be treated as invalid.
|
||||
const onDisk = { version: '1', records: { '{"r":"/p","i":"cap"}': { projectRoot: '/p', id: 'cap', scope: 'project', integrity: 'i', disclosureSignature: 's', consentedAt: '2026-01-01T00:00:00Z' } } };
|
||||
fs.writeFileSync(consent.consentStorePath(home), JSON.stringify(onDisk), 'utf8');
|
||||
@@ -461,7 +461,7 @@ test('readConsentStore: a record missing contentHash is dropped (fail closed)',
|
||||
test('readConsentStore: oversized store is refused (returns empty), never read whole', () => {
|
||||
const home = tmpDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
const big = '{"version":"1","records":{}' + ' '.repeat(16 * 1024 * 1024) + '}';
|
||||
fs.writeFileSync(consent.consentStorePath(home), big, 'utf8');
|
||||
assert.deepStrictEqual(consent.readConsentStore(home), { records: {} });
|
||||
@@ -492,8 +492,8 @@ test('TV-12: a store of EXACTLY CONSENT_MAX_BYTES is accepted (parsed); MAX+1 is
|
||||
const homeAccept = tmpDir();
|
||||
const homeRefuse = tmpDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(homeAccept, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(homeRefuse, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(homeAccept, '.msd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(homeRefuse, '.msd'), { recursive: true });
|
||||
const atMax = consentStoreOfExactBytes(CONSENT_MAX_BYTES);
|
||||
assert.strictEqual(Buffer.byteLength(atMax, 'utf8'), CONSENT_MAX_BYTES, 'precondition: exactly MAX bytes');
|
||||
fs.writeFileSync(consent.consentStorePath(homeAccept), atMax, 'utf8');
|
||||
@@ -513,7 +513,7 @@ test('TV-12: a store of EXACTLY CONSENT_MAX_BYTES is accepted (parsed); MAX+1 is
|
||||
test('readConsentStore: a FIFO at the store path does not block; returns empty', { skip: process.platform === 'win32' }, () => {
|
||||
const home = tmpDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
mkfifo(consent.consentStorePath(home));
|
||||
assert.deepStrictEqual(consent.readConsentStore(home), { records: {} });
|
||||
} finally {
|
||||
@@ -524,7 +524,7 @@ test('readConsentStore: a FIFO at the store path does not block; returns empty',
|
||||
test('readConsentStore: caps the number of records (a hostile store with too many is refused)', () => {
|
||||
const home = tmpDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
const records = {};
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
records[`{"r":"/p${i}","i":"cap${i}"}`] = { projectRoot: `/p${i}`, id: `cap${i}`, scope: 'project', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-x', consentedAt: '2026-01-01T00:00:00Z' };
|
||||
@@ -539,7 +539,7 @@ test('readConsentStore: caps the number of records (a hostile store with too man
|
||||
|
||||
// Build a store on disk with exactly `n` valid records (distinct kebab ids + roots).
|
||||
function seedStoreWithRecords(home, n) {
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
const records = {};
|
||||
for (let i = 0; i < n; i++) {
|
||||
records[`{"r":"/p${i}","i":"cap-${i}"}`] = { projectRoot: `/p${i}`, id: `cap-${i}`, scope: 'project', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-x', consentedAt: '2026-01-01T00:00:00Z' };
|
||||
@@ -573,9 +573,9 @@ test('record then has: a recorded consent matches on the EXACT contentHash', ()
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'deploy-gate', integrity: 'sha512-abc', disclosureSignature: 'sig-1', contentHash: 'sha512-bundle-1' });
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'deploy-gate', integrity: 'sha512-abc', disclosureSignature: 'sig-1', contentHash: 'sha512-bundle-1' });
|
||||
assert.strictEqual(
|
||||
consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'deploy-gate', contentHash: 'sha512-bundle-1' }),
|
||||
consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'deploy-gate', contentHash: 'sha512-bundle-1' }),
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
@@ -590,7 +590,7 @@ test('record requires a non-empty contentHash (the security binding) — throws
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
assert.throws(() => consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: '' }), /contentHash/);
|
||||
assert.throws(() => consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: '' }), /contentHash/);
|
||||
assert.deepStrictEqual(consent.readConsentStore(home), { records: {} }, 'nothing written');
|
||||
} finally {
|
||||
cleanup(home);
|
||||
@@ -598,13 +598,13 @@ test('record requires a non-empty contentHash (the security binding) — throws
|
||||
}
|
||||
});
|
||||
|
||||
test('record writes a well-formed record + lands the store under GSD_HOME (never the project)', () => {
|
||||
test('record writes a well-formed record + lands the store under MSD_HOME (never the project)', () => {
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'deploy-gate', integrity: 'sha512-abc', disclosureSignature: 'sig-1', contentHash: 'sha512-bundle-1' });
|
||||
assert.ok(fs.existsSync(consent.consentStorePath(home)), 'store written under GSD_HOME');
|
||||
assert.ok(!fs.existsSync(path.join(projectRoot, '.gsd', 'consent.json')), 'NOT written under the project root');
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'deploy-gate', integrity: 'sha512-abc', disclosureSignature: 'sig-1', contentHash: 'sha512-bundle-1' });
|
||||
assert.ok(fs.existsSync(consent.consentStorePath(home)), 'store written under MSD_HOME');
|
||||
assert.ok(!fs.existsSync(path.join(projectRoot, '.msd', 'consent.json')), 'NOT written under the project root');
|
||||
const onDisk = JSON.parse(fs.readFileSync(consent.consentStorePath(home), 'utf8'));
|
||||
assert.strictEqual(onDisk.version, '1');
|
||||
// WIN-3: the on-disk key is the unambiguous JSON-object form {"r":<root>,"i":<id>}.
|
||||
@@ -628,9 +628,9 @@ test('has: a contentHash mismatch is rejected (the binding is the bundle hash)',
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'sha512-good', disclosureSignature: 'sig-good', contentHash: 'sha512-bundle-good' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-bundle-DIFFERENT' }), false, 'contentHash mismatch rejected');
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-bundle-good' }), true);
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'sha512-good', disclosureSignature: 'sig-good', contentHash: 'sha512-bundle-good' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-bundle-DIFFERENT' }), false, 'contentHash mismatch rejected');
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-bundle-good' }), true);
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projectRoot);
|
||||
@@ -642,8 +642,8 @@ test('has: a different project root does NOT match (consent is per-project, on T
|
||||
const projectRoot = realProject();
|
||||
const otherProject = realProject();
|
||||
try {
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot: otherProject, id: 'cap', contentHash: 'sha512-h' }), false);
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot: otherProject, id: 'cap', contentHash: 'sha512-h' }), false);
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projectRoot);
|
||||
@@ -656,7 +656,7 @@ test('has: returns false (never throws) for an unsafe capability id', () => {
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
for (const bad of ['__proto__', 'constructor', 'prototype', 'Not-Kebab', 'with space', '../escape']) {
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: bad, contentHash: 'sha512-h' }), false, `unsafe id ${bad} → false`);
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: bad, contentHash: 'sha512-h' }), false, `unsafe id ${bad} → false`);
|
||||
}
|
||||
} finally {
|
||||
cleanup(home);
|
||||
@@ -668,7 +668,7 @@ test('record: rejects an unsafe capability id (prototype-pollution-safe), nothin
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
assert.throws(() => consent.recordProjectConsent({ gsdHome: home, projectRoot, id: '__proto__', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' }));
|
||||
assert.throws(() => consent.recordProjectConsent({ msdHome: home, projectRoot, id: '__proto__', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' }));
|
||||
const store = consent.readConsentStore(home);
|
||||
assert.deepStrictEqual(Object.keys(store.records), []);
|
||||
assert.strictEqual({}.polluted, undefined);
|
||||
@@ -682,14 +682,14 @@ test('record is idempotent: re-recording the same key overwrites in place (one r
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i1', disclosureSignature: 's1', contentHash: 'sha512-h1' });
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i2', disclosureSignature: 's2', contentHash: 'sha512-h2' });
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i1', disclosureSignature: 's1', contentHash: 'sha512-h1' });
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i2', disclosureSignature: 's2', contentHash: 'sha512-h2' });
|
||||
const onDisk = JSON.parse(fs.readFileSync(consent.consentStorePath(home), 'utf8'));
|
||||
assert.strictEqual(Object.keys(onDisk.records).length, 1);
|
||||
const key = JSON.stringify({ r: projectRoot, i: 'cap' });
|
||||
assert.strictEqual(onDisk.records[key].contentHash, 'sha512-h2');
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h2' }), true);
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h1' }), false);
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h2' }), true);
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h1' }), false);
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projectRoot);
|
||||
@@ -700,10 +700,10 @@ test('record preserves OTHER existing records (atomic round-trip across multiple
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap-a', integrity: 'ia', disclosureSignature: 'sa', contentHash: 'sha512-a' });
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap-b', integrity: 'ib', disclosureSignature: 'sb', contentHash: 'sha512-b' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap-a', contentHash: 'sha512-a' }), true);
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap-b', contentHash: 'sha512-b' }), true);
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap-a', integrity: 'ia', disclosureSignature: 'sa', contentHash: 'sha512-a' });
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap-b', integrity: 'ib', disclosureSignature: 'sb', contentHash: 'sha512-b' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap-a', contentHash: 'sha512-a' }), true);
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap-b', contentHash: 'sha512-b' }), true);
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projectRoot);
|
||||
@@ -714,12 +714,12 @@ test('revoke removes a record (has → false afterward); no-op when absent', ()
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h' }), true);
|
||||
consent.revokeProjectConsent({ gsdHome: home, projectRoot, id: 'cap' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h' }), false, 'record removed by revoke');
|
||||
assert.doesNotThrow(() => consent.revokeProjectConsent({ gsdHome: home, projectRoot, id: 'cap' }));
|
||||
assert.doesNotThrow(() => consent.revokeProjectConsent({ gsdHome: home, projectRoot, id: 'never' }));
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h' }), true);
|
||||
consent.revokeProjectConsent({ msdHome: home, projectRoot, id: 'cap' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h' }), false, 'record removed by revoke');
|
||||
assert.doesNotThrow(() => consent.revokeProjectConsent({ msdHome: home, projectRoot, id: 'cap' }));
|
||||
assert.doesNotThrow(() => consent.revokeProjectConsent({ msdHome: home, projectRoot, id: 'never' }));
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projectRoot);
|
||||
@@ -730,11 +730,11 @@ test('revoke leaves OTHER records intact', () => {
|
||||
const home = tmpDir();
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap-a', integrity: 'ia', disclosureSignature: 'sa', contentHash: 'sha512-a' });
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap-b', integrity: 'ib', disclosureSignature: 'sb', contentHash: 'sha512-b' });
|
||||
consent.revokeProjectConsent({ gsdHome: home, projectRoot, id: 'cap-a' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap-a', contentHash: 'sha512-a' }), false);
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap-b', contentHash: 'sha512-b' }), true, 'sibling record preserved');
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap-a', integrity: 'ia', disclosureSignature: 'sa', contentHash: 'sha512-a' });
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap-b', integrity: 'ib', disclosureSignature: 'sb', contentHash: 'sha512-b' });
|
||||
consent.revokeProjectConsent({ msdHome: home, projectRoot, id: 'cap-a' });
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap-a', contentHash: 'sha512-a' }), false);
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap-b', contentHash: 'sha512-b' }), true, 'sibling record preserved');
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projectRoot);
|
||||
@@ -754,12 +754,12 @@ test('two concurrent cross-project consent writes both survive (CONSENT-CONCURRE
|
||||
const projA = realProject();
|
||||
const projB = realProject();
|
||||
try {
|
||||
const modPath = path.resolve('gsd-core/bin/lib/capability-consent.cjs');
|
||||
const modPath = path.resolve('msd-core/bin/lib/capability-consent.cjs');
|
||||
// Run the two record writes in genuinely separate processes that hit the cross-process O_EXCL
|
||||
// lock concurrently (an in-process Promise.all would not exercise the file lock at all).
|
||||
const writeIn = (proj, id, hash) => new Promise((resolve, reject) => {
|
||||
const code = `require(${JSON.stringify(modPath)}).recordProjectConsent(` +
|
||||
`{gsdHome:${JSON.stringify(home)},projectRoot:${JSON.stringify(proj)},id:${JSON.stringify(id)},` +
|
||||
`{msdHome:${JSON.stringify(home)},projectRoot:${JSON.stringify(proj)},id:${JSON.stringify(id)},` +
|
||||
`integrity:'i',disclosureSignature:'s',contentHash:${JSON.stringify(hash)}})`;
|
||||
const child = spawn(process.execPath, ['-e', code], { stdio: 'ignore' });
|
||||
child.on('error', reject);
|
||||
@@ -769,8 +769,8 @@ test('two concurrent cross-project consent writes both survive (CONSENT-CONCURRE
|
||||
writeIn(projA, 'cap-a', 'sha512-a'),
|
||||
writeIn(projB, 'cap-b', 'sha512-b'),
|
||||
]);
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot: projA, id: 'cap-a', contentHash: 'sha512-a' }), true, 'project A record survived');
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot: projB, id: 'cap-b', contentHash: 'sha512-b' }), true, 'project B record survived (no lost update)');
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot: projA, id: 'cap-a', contentHash: 'sha512-a' }), true, 'project A record survived');
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot: projB, id: 'cap-b', contentHash: 'sha512-b' }), true, 'project B record survived (no lost update)');
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projA);
|
||||
@@ -809,7 +809,7 @@ test('finding-3: recordProjectConsent THROWS when the consent lock cannot be acq
|
||||
try {
|
||||
plantFreshConsentLock(home);
|
||||
assert.throws(
|
||||
() => consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' }),
|
||||
() => consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' }),
|
||||
/lock/i,
|
||||
'record must throw (lock-acquire failure) rather than write unlocked',
|
||||
);
|
||||
@@ -829,15 +829,15 @@ test('finding-3: revokeProjectConsent THROWS when the consent lock cannot be acq
|
||||
const projectRoot = realProject();
|
||||
try {
|
||||
// Seed a real record FIRST (under a free lock), then plant the fresh lock to block the revoke.
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' });
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' });
|
||||
plantFreshConsentLock(home);
|
||||
assert.throws(
|
||||
() => consent.revokeProjectConsent({ gsdHome: home, projectRoot, id: 'cap' }),
|
||||
() => consent.revokeProjectConsent({ msdHome: home, projectRoot, id: 'cap' }),
|
||||
/lock/i,
|
||||
'revoke must throw (lock-acquire failure) rather than delete unlocked',
|
||||
);
|
||||
// The record must STILL be present — the blocked revoke did not mutate the store.
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h' }), true, 'record preserved (revoke blocked, no unlocked delete)');
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h' }), true, 'record preserved (revoke blocked, no unlocked delete)');
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projectRoot);
|
||||
@@ -883,7 +883,7 @@ test('finding-4: the consent lock does NOT stale-steal a VERIFIED-LIVE same-host
|
||||
ageConsentLock(lockPath, 2 * 60 * 1000, body); // 2 min old (past the 60s stale window)
|
||||
const original = fs.readFileSync(lockPath, 'utf8');
|
||||
assert.throws(
|
||||
() => consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' }),
|
||||
() => consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' }),
|
||||
/lock/i,
|
||||
'a verified-live holder must NOT be stolen (record cannot acquire → throws)',
|
||||
);
|
||||
@@ -907,10 +907,10 @@ test('finding-4: the consent lock RECLAIMS a dead same-host holder (fast local r
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
ageConsentLock(lockPath, 2 * 60 * 1000, consentLockBody({ startTime: 'CSTART' })); // stale (>60s), dead pid
|
||||
assert.doesNotThrow(
|
||||
() => consent.recordProjectConsent({ gsdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' }),
|
||||
() => consent.recordProjectConsent({ msdHome: home, projectRoot, id: 'cap', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-h' }),
|
||||
'a dead holder must be reclaimed so record proceeds',
|
||||
);
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h' }), true, 'record written after reclaiming the dead holder');
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot, id: 'cap', contentHash: 'sha512-h' }), true, 'record written after reclaiming the dead holder');
|
||||
} finally {
|
||||
cleanup(home);
|
||||
cleanup(projectRoot);
|
||||
@@ -933,7 +933,7 @@ test('exactly MAX_RECORDS records can be written; the (MAX+1)th NEW key is refus
|
||||
// applies via consentProjectRoot (realpathSync fallback → path.resolve). On Windows,
|
||||
// path.resolve('/p0') === 'C:\\p0', so a raw '/p0' key would NOT match the production
|
||||
// lookup and the re-record below would be treated as a NEW key → false cap-full throw.
|
||||
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
||||
fs.mkdirSync(path.join(home, '.msd'), { recursive: true });
|
||||
const records = {};
|
||||
for (let i = 0; i < MAX; i++) {
|
||||
const r = path.resolve(`/p${i}`);
|
||||
@@ -943,11 +943,11 @@ test('exactly MAX_RECORDS records can be written; the (MAX+1)th NEW key is refus
|
||||
assert.strictEqual(Object.keys(consent.readConsentStore(home).records).length, MAX, 'store seeded at the cap');
|
||||
// A re-record of an EXISTING key does NOT grow the store → allowed even at the cap.
|
||||
const existingProj = path.resolve('/p0');
|
||||
assert.doesNotThrow(() => consent.recordProjectConsent({ gsdHome: home, projectRoot: existingProj, id: 'cap0', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-new' }));
|
||||
assert.doesNotThrow(() => consent.recordProjectConsent({ msdHome: home, projectRoot: existingProj, id: 'cap0', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-new' }));
|
||||
// Adding a NEW key when already at the cap is refused with a clear 'full' error.
|
||||
const fresh = realProject();
|
||||
try {
|
||||
assert.throws(() => consent.recordProjectConsent({ gsdHome: home, projectRoot: fresh, id: 'overflow', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-of' }), /full|maximum/i);
|
||||
assert.throws(() => consent.recordProjectConsent({ msdHome: home, projectRoot: fresh, id: 'overflow', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-of' }), /full|maximum/i);
|
||||
} finally {
|
||||
cleanup(fresh);
|
||||
}
|
||||
@@ -971,15 +971,15 @@ test('WIN-3: roots containing spaces are keyed unambiguously on disk (no collisi
|
||||
try {
|
||||
const r1 = '/tmp/space root one'; // path containing spaces (Windows-style)
|
||||
const r2 = '/tmp/space root one x'; // a DIFFERENT root extending r1 past a space boundary
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot: r1, id: 'cap-a', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-1' });
|
||||
consent.recordProjectConsent({ gsdHome: home, projectRoot: r2, id: 'cap-b', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-2' });
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot: r1, id: 'cap-a', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-1' });
|
||||
consent.recordProjectConsent({ msdHome: home, projectRoot: r2, id: 'cap-b', integrity: 'i', disclosureSignature: 's', contentHash: 'sha512-2' });
|
||||
const onDisk = JSON.parse(fs.readFileSync(consent.consentStorePath(home), 'utf8'));
|
||||
assert.strictEqual(Object.keys(onDisk.records).length, 2, 'two distinct records, no disk-key collision');
|
||||
assert.ok(onDisk.records[JSON.stringify({ r: path.resolve(r1), i: 'cap-a' })], 'r1 (space path) record keyed unambiguously');
|
||||
assert.ok(onDisk.records[JSON.stringify({ r: path.resolve(r2), i: 'cap-b' })], 'r2 (space path) record keyed unambiguously');
|
||||
// Both are independently retrievable (the lookup re-keys via the canonical NUL key).
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot: r1, id: 'cap-a', contentHash: 'sha512-1' }), true);
|
||||
assert.strictEqual(consent.hasProjectConsent({ gsdHome: home, projectRoot: r2, id: 'cap-b', contentHash: 'sha512-2' }), true);
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot: r1, id: 'cap-a', contentHash: 'sha512-1' }), true);
|
||||
assert.strictEqual(consent.hasProjectConsent({ msdHome: home, projectRoot: r2, id: 'cap-b', contentHash: 'sha512-2' }), true);
|
||||
} finally {
|
||||
cleanup(home);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user