fix(node): sync transactional Cursor hooks

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 00:58:40 -04:00
1 parent 68918e22d7
commit 2e699c11e3
16 files changed
+2466 -462

No files matched your search

+281
View File
@@ -0,0 +1,281 @@
import assert from 'node:assert/strict';
import {
lstat,
mkdir,
readFile,
readdir,
rm,
stat,
symlink,
writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
assertNativeHandler,
createFixture,
installConfig,
legacyHandler,
managedHandler,
registryModuleUrl,
runPlugin,
writeJson,
} from '../test-support/cursor-test-helpers.mjs';
test('Cursor is registered with the native user hook file', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('cursor'), {
name: 'Cursor',
configDir: '~/.cursor',
configFile: 'hooks.json',
});
});
test('Cursor install preserves hooks, migrates legacy entries, and is idempotent', async () => {
const fixture = await createFixture();
try {
await writeJson(fixture.configPath, {
description: 'user-owned',
hooks: {
sessionStart: [{ command: 'user-session' }],
preToolUse: [
{ command: 'user-pre' },
legacyHandler(fixture.guardScriptPath),
],
postToolUse: [legacyHandler(fixture.hookScriptPath)],
postToolUseFailure: [legacyHandler(fixture.hookScriptPath)],
},
});
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
const second = await fixture.install();
assert.equal(second.code, 0, second.stderr);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
assert.equal(config.version, 1);
assert.equal(config.description, 'user-owned');
assert.deepEqual(config.hooks.sessionStart, [{ command: 'user-session' }]);
assert.deepEqual(config.hooks.preToolUse[0], { command: 'user-pre' });
assert.equal(config.hooks.preToolUse.length, 2);
assert.equal(config.hooks.postToolUse.length, 1);
assert.equal(config.hooks.postToolUseFailure.length, 1);
assertNativeHandler(managedHandler(config, 'preToolUse', 'guard.js'));
assertNativeHandler(managedHandler(config, 'postToolUse', 'hook.js'));
assertNativeHandler(managedHandler(config, 'postToolUseFailure', 'hook.js'));
} finally {
await fixture.close();
}
});
test('Cursor status requires three handlers, matching identity, and every runtime secret', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
let status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
const hooks = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
delete hooks.hooks.postToolUseFailure;
await writeJson(fixture.configPath, hooks);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
assert.equal((await fixture.install()).code, 0);
const keyPath = path.join(fixture.agentDir, 'private.key');
await rm(keyPath);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
await writeFile(keyPath, 'restored', { mode: 0o600 });
await writeJson(path.join(fixture.agentDir, 'config.json'), {
agent_id: 'another-agent',
agent_name: 'cursor',
});
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
await writeFile(path.join(fixture.agentDir, 'config.json'), '{ malformed');
status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /parse Elydora runtime config/i);
} finally {
await fixture.close();
}
});
test('Cursor uninstall removes exact ownership and preserves user entries', async () => {
const fixture = await createFixture({
existingConfig: { version: 1, hooks: { sessionStart: [{ command: 'keep' }] } },
});
try {
assert.equal((await fixture.install()).code, 0);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
for (const [event, script] of [
['preToolUse', 'guard.js'],
['postToolUse', 'hook.js'],
['postToolUseFailure', 'hook.js'],
]) {
const other = structuredClone(managedHandler(config, event, script));
other.command = other.command.replaceAll('agent-1', 'agent-10');
config.hooks[event].push(other);
}
config.hooks.preToolUse.push({ command: 'user-pre' });
await writeJson(fixture.configPath, config);
const result = await runPlugin(fixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
const remaining = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
assert.deepEqual(remaining.hooks.sessionStart, [{ command: 'keep' }]);
assert.equal(remaining.hooks.preToolUse.length, 2);
assert.equal(remaining.hooks.postToolUse.length, 1);
assert.equal(remaining.hooks.postToolUseFailure.length, 1);
assert.match(remaining.hooks.preToolUse[0].command, /agent-10/);
} finally {
await fixture.close();
}
});
test('Cursor rejects malformed, duplicate, versionless user, and invalid configs before writes', async () => {
const invalidConfigs = [
'{ malformed',
'[]\n',
'{"hooks":{}}\n',
'{"hooks":{"preToolUse":[{"command":"user"}]}}\n',
'{"version":2,"hooks":{}}\n',
'{"version":1,"hooks":null}\n',
'{"version":1,"hooks":{"preToolUse":null}}\n',
'{"version":1,"hooks":{"preToolUse":[null]}}\n',
'{"version":1,"version":1,"hooks":{}}\n',
'{"version":1,"hooks":{},}\n',
];
for (const existingConfig of invalidConfigs) {
const fixture = await createFixture({ existingConfig });
try {
const before = await readFile(fixture.configPath, 'utf-8');
const result = await fixture.install();
assert.equal(result.code, 1, `accepted ${existingConfig}`);
assert.equal(await readFile(fixture.configPath, 'utf-8'), before);
await assert.rejects(lstat(path.join(fixture.homeDir, '.elydora')), { code: 'ENOENT' });
} finally {
await fixture.close();
}
}
});
test('Cursor confines runtime scripts to the managed agent directory', async () => {
for (const field of ['guardScriptPath', 'hookScriptPath']) {
const fixture = await createFixture();
try {
const result = await runPlugin(fixture, 'install', installConfig(fixture, {
[field]: path.join(fixture.homeDir, `unmanaged-${field}.js`),
}));
assert.equal(result.code, 1);
assert.match(result.stderr, /managed agent directory/i);
await assert.rejects(readFile(fixture.configPath), { code: 'ENOENT' });
} finally {
await fixture.close();
}
}
});
test('Cursor rejects a symbolic-link config and preserves its target', async (t) => {
const fixture = await createFixture();
try {
const target = path.join(fixture.homeDir, 'cursor-hooks-target.json');
const original = '{"version":1,"hooks":{}}\n';
await mkdir(fixture.homeDir, { recursive: true });
await writeFile(target, original);
await mkdir(path.dirname(fixture.configPath), { recursive: true });
try {
await symlink(target, fixture.configPath);
} catch (error) {
if (error?.code === 'EPERM') {
t.skip(`symbolic links unavailable: ${error.message}`);
return;
}
throw error;
}
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /physical file/i);
assert.equal(await readFile(target, 'utf-8'), original);
assert.equal((await lstat(fixture.configPath)).isSymbolicLink(), true);
} finally {
await fixture.close();
}
});
test('Cursor status rejects symbolic-link runtime files', async (t) => {
for (const name of ['config', 'key', 'guard', 'audit']) {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const filePath = {
config: path.join(fixture.agentDir, 'config.json'),
key: path.join(fixture.agentDir, 'private.key'),
guard: fixture.guardScriptPath,
audit: fixture.hookScriptPath,
}[name];
const contents = await readFile(filePath);
const target = path.join(fixture.homeDir, `${name}-runtime-target`);
await writeFile(target, contents);
await rm(filePath);
try {
await symlink(target, filePath);
} catch (error) {
if (error?.code === 'EPERM') {
t.skip(`symbolic links unavailable: ${error.message}`);
return;
}
throw error;
}
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /physical file/i);
} finally {
await fixture.close();
}
}
});
test('Cursor removes an entirely managed config and leaves an absent config absent', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-1')).code, 0);
await assert.rejects(readFile(fixture.configPath), { code: 'ENOENT' });
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-1')).code, 0);
await assert.rejects(readFile(fixture.configPath), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Cursor installation writes private files and leaves no transaction files', async () => {
const fixture = await createFixture();
try {
const result = await fixture.install();
assert.equal(result.code, 0, result.stderr);
const names = [
...await readdir(fixture.agentDir),
...await readdir(path.dirname(fixture.configPath)),
];
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false);
if (process.platform !== 'win32') {
for (const filePath of [
fixture.configPath,
path.join(fixture.agentDir, 'config.json'),
path.join(fixture.agentDir, 'private.key'),
]) {
assert.equal((await stat(filePath)).mode & 0o777, 0o600);
}
for (const filePath of [fixture.guardScriptPath, fixture.hookScriptPath]) {
assert.equal((await stat(filePath)).mode & 0o777, 0o700);
}
}
} finally {
await fixture.close();
}
});
+176
View File
@@ -0,0 +1,176 @@
import assert from 'node:assert/strict';
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
createFixture,
managedHandler,
runHook,
startApiServer,
writeJson,
} from '../test-support/cursor-test-helpers.mjs';
test('Cursor guard and both audit events use native JSON contracts', async () => {
const api = await startApiServer();
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
const install = await fixture.install();
assert.equal(install.code, 0, install.stderr);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
const guard = managedHandler(config, 'preToolUse', 'guard.js');
const successAudit = managedHandler(config, 'postToolUse', 'hook.js');
const failureAudit = managedHandler(config, 'postToolUseFailure', 'hook.js');
const prePayload = {
conversation_id: 'conversation-1',
generation_id: 'generation-1',
hook_event_name: 'preToolUse',
tool_name: 'Shell',
tool_input: { command: 'Get-ChildItem', working_directory: fixture.projectDir },
tool_use_id: 'call-1',
cwd: fixture.projectDir,
};
const guardResult = await runHook(guard, `${JSON.stringify(prePayload)}\n`, fixture);
assert.equal(guardResult.code, 0, guardResult.stderr);
assert.deepEqual(JSON.parse(guardResult.stdout), { permission: 'allow' });
const postPayload = {
conversation_id: 'conversation-1',
generation_id: 'generation-1',
hook_event_name: 'postToolUse',
tool_name: 'Shell',
tool_input: { command: 'Get-ChildItem' },
tool_output: '{"exitCode":0,"stdout":"ok"}',
tool_use_id: 'call-1',
cwd: fixture.projectDir,
duration: 42,
};
const postResult = await runHook(successAudit, `${JSON.stringify(postPayload)}\n`, fixture);
assert.equal(postResult.code, 0, postResult.stderr);
assert.deepEqual(JSON.parse(postResult.stdout), {});
const failurePayload = {
conversation_id: 'conversation-1',
generation_id: 'generation-1',
hook_event_name: 'postToolUseFailure',
tool_name: 'Shell',
tool_input: { command: 'exit 7' },
tool_error: { message: 'Process exited with code 7', exit_code: 7 },
tool_use_id: 'call-2',
cwd: fixture.projectDir,
duration: 12,
};
const failureResult = await runHook(
failureAudit,
`${JSON.stringify(failurePayload)}\n`,
fixture,
);
assert.equal(failureResult.code, 0, failureResult.stderr);
assert.deepEqual(JSON.parse(failureResult.stdout), {});
const operations = api.requests
.filter((request) => request.method === 'POST')
.map((request) => JSON.parse(request.raw));
assert.equal(operations.length, 2);
assert.deepEqual(operations[0].payload, postPayload);
assert.deepEqual(operations[1].payload, failurePayload);
assert.equal(operations[0].subject.session_id, 'conversation-1');
assert.equal(operations[1].action.tool, 'Shell');
} finally {
await fixture.close();
await api.close();
}
});
test('Cursor guard emits a structured deny for frozen and revoked agents', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
const guard = managedHandler(config, 'preToolUse', 'guard.js');
for (const status of ['frozen', 'revoked']) {
await writeJson(path.join(fixture.agentDir, 'status-cache.json'), {
status,
cached_at: Date.now(),
});
const result = await runHook(guard, '{}\n', fixture);
assert.equal(result.code, 2, result.stderr);
const response = JSON.parse(result.stdout);
assert.equal(response.permission, 'deny');
assert.match(response.agentMessage, new RegExp(status));
}
} finally {
await fixture.close();
}
});
test('Cursor strict runtimes surface malformed input, config, key, cache, and API errors', async () => {
const api = await startApiServer({ operationStatus: 500 });
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const hooks = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
const guard = managedHandler(hooks, 'preToolUse', 'guard.js');
const audit = managedHandler(hooks, 'postToolUse', 'hook.js');
const runtimeConfigPath = path.join(fixture.agentDir, 'config.json');
const runtimeConfig = await readFile(runtimeConfigPath, 'utf-8');
const privateKeyPath = path.join(fixture.agentDir, 'private.key');
const privateKey = await readFile(privateKeyPath, 'utf-8');
let result = await runHook(audit, '{ malformed', fixture);
assert.equal(result.code, 1);
assert.equal(result.stdout, '');
assert.match(result.stderr, /invalid JSON/i);
await writeFile(runtimeConfigPath, '{ malformed', { mode: 0o600 });
result = await runHook(guard, '{}\n', fixture);
assert.equal(result.code, 1);
assert.match(result.stderr, /agent config/i);
await writeFile(runtimeConfigPath, runtimeConfig, { mode: 0o600 });
await writeFile(privateKeyPath, 'invalid-key', { mode: 0o600 });
result = await runHook(audit, '{}\n', fixture);
assert.equal(result.code, 1);
assert.match(result.stderr, /private key/i);
await writeFile(privateKeyPath, privateKey, { mode: 0o600 });
await writeFile(path.join(fixture.agentDir, 'status-cache.json'), '{ malformed', {
mode: 0o600,
});
result = await runHook(guard, '{}\n', fixture);
assert.equal(result.code, 1);
assert.match(result.stderr, /status cache/i);
await writeFile(path.join(fixture.agentDir, 'status-cache.json'), JSON.stringify({
status: 'active',
cached_at: 0,
}), { mode: 0o600 });
result = await runHook(audit, '{}\n', fixture);
assert.equal(result.code, 1);
assert.match(result.stderr, /HTTP 500/i);
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /HTTP 500/i);
} finally {
await fixture.close();
await api.close();
}
});
test('Cursor guard fails closed on invalid status API data', async () => {
const api = await startApiServer({ status: 'paused' });
const fixture = await createFixture({ baseUrl: api.baseUrl });
try {
assert.equal((await fixture.install()).code, 0);
const hooks = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
const result = await runHook(
managedHandler(hooks, 'preToolUse', 'guard.js'),
'{}\n',
fixture,
);
assert.equal(result.code, 1);
assert.equal(result.stdout, '');
assert.match(result.stderr, /invalid agent status/i);
} finally {
await fixture.close();
await api.close();
}
});
+262
View File
@@ -0,0 +1,262 @@
import assert from 'node:assert/strict';
import {
lstat,
mkdir,
readFile,
readdir,
symlink,
writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
VALID_PRIVATE_KEY,
createFixture,
installationModuleUrl,
installConfig,
ioModuleUrl,
runNode,
writeJson,
} from '../test-support/cursor-test-helpers.mjs';
const managedFileNames = ['guard.js', 'config.json', 'private.key', 'hook.js'];
async function snapshotInstallation(fixture) {
const entries = await Promise.all(managedFileNames.map(async (name) => [
path.join(fixture.agentDir, name),
await readFile(path.join(fixture.agentDir, name), 'utf-8'),
]));
entries.push([fixture.configPath, await readFile(fixture.configPath, 'utf-8')]);
return new Map(entries);
}
async function assertSnapshot(snapshot) {
for (const [filePath, expected] of snapshot) {
assert.equal(await readFile(filePath, 'utf-8'), expected, filePath);
}
}
async function assertNoTransactionFiles(fixture) {
const names = [
...await readdir(fixture.agentDir),
...await readdir(path.dirname(fixture.configPath)),
];
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false);
}
test('Cursor rolls back all five files when a transaction commit fails', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const before = await snapshotInstallation(fixture);
const source = `
import { rename } from 'node:fs/promises';
import { readDocument } from ${JSON.stringify(ioModuleUrl)};
import {
commitCursorInstallation,
prepareCursorInstallation,
} from ${JSON.stringify(installationModuleUrl)};
const config = JSON.parse(process.env.ELYDORA_INSTALL_CONFIG);
const document = await readDocument();
const prepared = await prepareCursorInstallation(config, { document, changed: false });
let calls = 0;
await commitCursorInstallation(prepared, async (from, to) => {
calls += 1;
if (calls === 3) throw new Error('injected rename failure');
await rename(from, to);
});
`;
const result = await runNode(
['--input-type=module', '--eval', source],
{
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
ELYDORA_INSTALL_CONFIG: JSON.stringify(installConfig(fixture, {
orgId: 'org-updated',
token: 'token-updated',
})),
},
fixture.projectDir,
);
assert.equal(result.code, 1);
assert.match(result.stderr, /injected rename failure/i);
await assertSnapshot(before);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});
test('Cursor detects concurrent hook config changes before committing runtime updates', async () => {
const fixture = await createFixture({
existingConfig: { version: 1, hooks: { sessionStart: [{ command: 'original' }] } },
});
try {
assert.equal((await fixture.install()).code, 0);
const before = await snapshotInstallation(fixture);
const concurrent = '{"version":1,"hooks":{"sessionStart":[{"command":"concurrent"}]}}\n';
const source = `
import { writeFile } from 'node:fs/promises';
import { readDocument } from ${JSON.stringify(ioModuleUrl)};
import {
commitCursorInstallation,
prepareCursorInstallation,
} from ${JSON.stringify(installationModuleUrl)};
const config = JSON.parse(process.env.ELYDORA_INSTALL_CONFIG);
const document = await readDocument();
const prepared = await prepareCursorInstallation(config, { document, changed: false });
await writeFile(document.filePath, process.env.ELYDORA_CONCURRENT, 'utf-8');
await commitCursorInstallation(prepared);
`;
const result = await runNode(
['--input-type=module', '--eval', source],
{
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
ELYDORA_CONCURRENT: concurrent,
ELYDORA_INSTALL_CONFIG: JSON.stringify(installConfig(fixture, { orgId: 'org-updated' })),
},
fixture.projectDir,
);
assert.equal(result.code, 1);
assert.match(result.stderr, /changed during Cursor installation/i);
for (const [filePath, expected] of before) {
if (filePath !== fixture.configPath) assert.equal(await readFile(filePath, 'utf-8'), expected);
}
assert.equal(await readFile(fixture.configPath, 'utf-8'), concurrent);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});
test('Cursor rejects a stale hook document before staging an installation', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const before = await snapshotInstallation(fixture);
const concurrent = '{"version":1,"hooks":{"sessionStart":[{"command":"new"}]}}\n';
const source = `
import { writeFile } from 'node:fs/promises';
import { readDocument } from ${JSON.stringify(ioModuleUrl)};
import { prepareCursorInstallation } from ${JSON.stringify(installationModuleUrl)};
const document = await readDocument();
await writeFile(document.filePath, process.env.ELYDORA_CONCURRENT, 'utf-8');
await prepareCursorInstallation(
JSON.parse(process.env.ELYDORA_INSTALL_CONFIG),
{ document, changed: false },
);
`;
const result = await runNode(
['--input-type=module', '--eval', source],
{
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
ELYDORA_CONCURRENT: concurrent,
ELYDORA_INSTALL_CONFIG: JSON.stringify(installConfig(fixture)),
},
fixture.projectDir,
);
assert.equal(result.code, 1);
assert.match(result.stderr, /changed before installation/i);
for (const [filePath, expected] of before) {
if (filePath !== fixture.configPath) assert.equal(await readFile(filePath, 'utf-8'), expected);
}
assert.equal(await readFile(fixture.configPath, 'utf-8'), concurrent);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});
test('Cursor rejects orphaned runtime artifacts before writing hook config', async () => {
const fixture = await createFixture();
try {
await mkdir(fixture.agentDir, { recursive: true });
await writeFile(fixture.guardScriptPath, 'orphaned guard\n');
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /identity cannot be verified/i);
assert.equal(await readFile(fixture.guardScriptPath, 'utf-8'), 'orphaned guard\n');
await assert.rejects(readFile(fixture.configPath), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Cursor rejects a mismatched runtime identity before changing files', async () => {
const fixture = await createFixture();
try {
await writeJson(path.join(fixture.agentDir, 'config.json'), {
agent_name: 'cursor',
agent_id: 'another-agent',
});
const before = await readFile(path.join(fixture.agentDir, 'config.json'), 'utf-8');
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /identity does not match/i);
assert.equal(await readFile(path.join(fixture.agentDir, 'config.json'), 'utf-8'), before);
await assert.rejects(readFile(fixture.configPath), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});
test('Cursor rejects symbolic-link config and runtime directories before file writes', async (t) => {
for (const kind of ['cursor', 'runtime']) {
const fixture = await createFixture();
try {
const target = path.join(fixture.rootDir, `${kind}-target`);
await mkdir(target, { recursive: true });
const link = kind === 'cursor'
? path.join(fixture.homeDir, '.cursor')
: path.join(fixture.homeDir, '.elydora');
await mkdir(fixture.homeDir, { recursive: true });
try {
await symlink(target, link, 'junction');
} catch (error) {
if (error?.code === 'EPERM') {
t.skip(`symbolic links unavailable: ${error.message}`);
return;
}
throw error;
}
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /physical directory/i);
assert.deepEqual(await readdir(target), []);
const untouched = kind === 'cursor'
? path.join(fixture.homeDir, '.elydora')
: path.join(fixture.homeDir, '.cursor', 'hooks.json');
await assert.rejects(lstat(untouched), { code: 'ENOENT' });
} finally {
await fixture.close();
}
}
});
test('Cursor CLI preflight rejects malformed hooks before creating runtime files', async () => {
const fixture = await createFixture({ existingConfig: '{ malformed' });
try {
const privateKeyPath = path.join(fixture.rootDir, 'private.key');
await writeFile(privateKeyPath, VALID_PRIVATE_KEY, { mode: 0o600 });
const result = await runNode([
path.resolve('dist/cli.js'),
'install',
'--agent', 'cursor',
'--org_id', 'org-1',
'--agent_id', 'agent-1',
'--private_key_file', privateKeyPath,
'--kid', 'kid-1',
'--base_url', fixture.baseUrl,
], {
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
}, fixture.projectDir);
assert.equal(result.code, 1);
assert.match(result.stderr, /Cursor user hooks/i);
await assert.rejects(lstat(path.join(fixture.homeDir, '.elydora')), { code: 'ENOENT' });
} finally {
await fixture.close();
}
});