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.
177 lines
6.0 KiB
JavaScript
177 lines
6.0 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* cursor hook-bus UPGRADE — ADR-1239 / #2089 AC4a.
|
|
*
|
|
* Proves the expanded hook-bus coverage: MSD registers for subagentStart,
|
|
* subagentStop, preToolUse, and stop IN ADDITION to the baseline sessionStart
|
|
* and postToolUse. Cite: https://cursor.com/docs/hooks
|
|
*
|
|
* Tests the descriptor-driven adapter module (pure) + the reconcile behavior
|
|
* (all 6 managed events in the generated hooks.json).
|
|
*/
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
|
|
const {
|
|
CURSOR_HOOK_EVENTS,
|
|
CURSOR_EVENT_SCRIPT_MAP,
|
|
resolveManagedHookEvents,
|
|
resolveHookScripts,
|
|
} = require('../msd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs');
|
|
|
|
const {
|
|
MSD_CURSOR_HOOK_MARKER,
|
|
} = require('../bin/install.js');
|
|
|
|
const {
|
|
reconcileCursorHooksJson,
|
|
} = require('../msd-core/bin/lib/runtime-hooks-surface.cjs');
|
|
|
|
const { cleanup } = require('./helpers.cjs');
|
|
|
|
const CUR_CAP = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'cursor', 'capability.json'), 'utf8'),
|
|
);
|
|
|
|
const EXPECTED_EVENTS = [
|
|
'sessionStart',
|
|
'postToolUse',
|
|
'preToolUse',
|
|
'stop',
|
|
'subagentStart',
|
|
'subagentStop',
|
|
];
|
|
|
|
// -- AC4a: the adapter declares all 6 managed events -------------------------
|
|
|
|
test('CURSOR_HOOK_EVENTS contains all 6 managed events', () => {
|
|
for (const ev of EXPECTED_EVENTS) {
|
|
assert.ok(CURSOR_HOOK_EVENTS.includes(ev),
|
|
`CURSOR_HOOK_EVENTS must include ${ev}`);
|
|
}
|
|
assert.equal(CURSOR_HOOK_EVENTS.length, 6,
|
|
'exactly 6 managed events (no extras)');
|
|
});
|
|
|
|
test('CURSOR_EVENT_SCRIPT_MAP maps every event to a script', () => {
|
|
for (const ev of EXPECTED_EVENTS) {
|
|
const script = CURSOR_EVENT_SCRIPT_MAP[ev];
|
|
assert.ok(typeof script === 'string' && script.endsWith('.js'),
|
|
`${ev} must map to a .js script, got: ${script}`);
|
|
}
|
|
});
|
|
|
|
test('descriptor managedHookEvents matches the adapter event list', () => {
|
|
const declared = CUR_CAP.runtime.hostBehaviors.managedHookEvents;
|
|
assert.deepEqual(declared.sort(), [...EXPECTED_EVENTS].sort(),
|
|
'descriptor managedHookEvents must match the 6-event managed set');
|
|
});
|
|
|
|
// -- AC4a: resolveManagedHookEvents + resolveHookScripts ---------------------
|
|
|
|
test('resolveManagedHookEvents returns all 6 from the descriptor list', () => {
|
|
const resolved = resolveManagedHookEvents(CUR_CAP.runtime.hostBehaviors.managedHookEvents);
|
|
assert.equal(resolved.length, 6);
|
|
for (const ev of EXPECTED_EVENTS) {
|
|
assert.ok(resolved.includes(ev), `resolveManagedHookEvents must include ${ev}`);
|
|
}
|
|
});
|
|
|
|
test('resolveManagedHookEvents filters unknown events (fail-closed)', () => {
|
|
const resolved = resolveManagedHookEvents(['sessionStart', 'bogusEvent', 'stop']);
|
|
assert.deepEqual([...resolved].sort(), ['sessionStart', 'stop']);
|
|
});
|
|
|
|
test('resolveManagedHookEvents falls back to full set when descriptor is empty', () => {
|
|
const resolved = resolveManagedHookEvents(null);
|
|
assert.equal(resolved.length, 6);
|
|
});
|
|
|
|
test('resolveHookScripts returns a script for every managed event', () => {
|
|
const scripts = resolveHookScripts(EXPECTED_EVENTS);
|
|
assert.equal(scripts.length, 6);
|
|
for (const s of scripts) {
|
|
assert.ok(s.startsWith('msd-cursor-') && s.endsWith('.js'),
|
|
`script must follow msd-cursor-*.js convention: ${s}`);
|
|
}
|
|
});
|
|
|
|
// -- AC4a: hook scripts exist on disk ---------------------------------------
|
|
|
|
test('all 6 hook scripts exist under hooks/', () => {
|
|
for (const ev of EXPECTED_EVENTS) {
|
|
const script = CURSOR_EVENT_SCRIPT_MAP[ev];
|
|
const scriptPath = path.join(__dirname, '..', 'hooks', script);
|
|
assert.ok(fs.existsSync(scriptPath),
|
|
`hook script must exist: hooks/${script} (event: ${ev})`);
|
|
}
|
|
});
|
|
|
|
// -- AC4a: reconcile generates hooks.json with all 6 events ------------------
|
|
|
|
test('reconcileCursorHooksJson writes all 6 managed events into hooks.json', (t) => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-cursor-hook-bus-'));
|
|
t.after(() => cleanup(tmpDir));
|
|
const hooksJsonPath = path.join(tmpDir, 'hooks.json');
|
|
const managedEntries = {};
|
|
for (const ev of EXPECTED_EVENTS) {
|
|
managedEntries[ev] = {
|
|
type: 'command',
|
|
command: `node /fake/${ev}.js`,
|
|
[MSD_CURSOR_HOOK_MARKER]: true,
|
|
};
|
|
}
|
|
const result = reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
|
assert.ok(result.changed, 'first write must report changed=true');
|
|
|
|
const written = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
|
const hookTable = written.hooks;
|
|
assert.ok(hookTable && typeof hookTable === 'object');
|
|
for (const ev of EXPECTED_EVENTS) {
|
|
assert.ok(Array.isArray(hookTable[ev]),
|
|
`hooks.json must have a ${ev} array`);
|
|
assert.equal(hookTable[ev].length, 1,
|
|
`${ev} must have exactly 1 managed entry`);
|
|
assert.equal(hookTable[ev][0][MSD_CURSOR_HOOK_MARKER], true,
|
|
`${ev} entry must carry the MSD managed marker`);
|
|
}
|
|
});
|
|
|
|
test('reconcileCursorHooksJson preserves user entries across all 6 events', (t) => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-cursor-hook-bus-'));
|
|
t.after(() => cleanup(tmpDir));
|
|
const hooksJsonPath = path.join(tmpDir, 'hooks.json');
|
|
// Seed with user-owned entries in two events.
|
|
const seed = {
|
|
version: 1,
|
|
hooks: {
|
|
sessionStart: [{ type: 'command', command: 'user-start.sh' }],
|
|
preToolUse: [{ type: 'command', command: 'user-pre.sh' }],
|
|
},
|
|
};
|
|
fs.writeFileSync(hooksJsonPath, JSON.stringify(seed, null, 2) + '\n');
|
|
|
|
const managedEntries = {};
|
|
for (const ev of EXPECTED_EVENTS) {
|
|
managedEntries[ev] = {
|
|
type: 'command',
|
|
command: `node /msd/${ev}.js`,
|
|
[MSD_CURSOR_HOOK_MARKER]: true,
|
|
};
|
|
}
|
|
reconcileCursorHooksJson(hooksJsonPath, managedEntries);
|
|
|
|
const written = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
|
// sessionStart: 1 user + 1 managed
|
|
assert.equal(written.hooks.sessionStart.length, 2);
|
|
// preToolUse: 1 user + 1 managed
|
|
assert.equal(written.hooks.preToolUse.length, 2);
|
|
// postToolUse: 1 managed only
|
|
assert.equal(written.hooks.postToolUse.length, 1);
|
|
});
|