fix: make Cursor hooks transactional

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 00:48:33 -04:00
1 parent 6d36f557b1
commit 6a7fdb7595
15 files changed
+1686 -665

No files matched your search

+79 -288
View File
@@ -1,9 +1,7 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import {
lstat,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
@@ -11,140 +9,18 @@ import {
symlink,
writeFile,
} from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/cursor.js')).href;
const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
const ioModuleUrl = pathToFileURL(path.resolve('dist/plugins/cursor-io.js')).href;
function runNode(args, env, cwd, input = '') {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, {
cwd,
env: { ...process.env, ...env },
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
function runHook(handler, input, environment = {}) {
const executable = process.platform === 'win32' ? 'powershell.exe' : '/bin/sh';
const args = process.platform === 'win32'
? ['-NoProfile', '-NonInteractive', '-Command', handler.command]
: ['-c', handler.command];
return new Promise((resolve, reject) => {
const child = spawn(executable, args, {
env: { ...process.env, ...environment },
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.once('error', reject);
child.once('close', (code) => resolve({ code, stdout, stderr }));
child.stdin.end(input);
});
}
async function writeJson(filePath, value) {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`);
}
async function runPlugin(fixture, method, argument) {
const source = `
import { cursorPlugin } from ${JSON.stringify(pluginModuleUrl)};
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
const result = await cursorPlugin[process.env.ELYDORA_TEST_METHOD](argument);
if (result !== undefined) console.log(JSON.stringify(result));
`;
return runNode(
['--input-type=module', '--eval', source],
{
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
ELYDORA_TEST_METHOD: method,
},
fixture.projectDir,
);
}
async function createFixture({ existingConfig, createRuntimes = true, hookSource } = {}) {
const rootDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-cursor-'));
const homeDir = path.join(rootDir, "home with spaces and 'quote");
const projectDir = path.join(rootDir, 'project with spaces');
const configPath = path.join(homeDir, '.cursor', 'hooks.json');
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
const guardScriptPath = path.join(agentDir, 'guard.js');
const hookScriptPath = path.join(agentDir, 'hook.js');
await mkdir(projectDir, { recursive: true });
if (createRuntimes) {
await mkdir(agentDir, { recursive: true });
await writeFile(
guardScriptPath,
"process.stdin.resume(); process.stdin.once('end', () => { process.stderr.write('Agent is frozen by Elydora.'); process.exit(2); });\n",
);
await writeFile(hookScriptPath, hookSource ?? 'process.stdin.resume();\n');
await writeFile(path.join(agentDir, 'config.json'), JSON.stringify({
agent_id: 'agent-1',
agent_name: 'cursor',
}));
}
if (existingConfig !== undefined) await writeJson(configPath, existingConfig);
return {
agentDir,
configPath,
guardScriptPath,
homeDir,
hookScriptPath,
projectDir,
rootDir,
async install() {
return runPlugin(this, 'install', {
agentName: 'cursor',
agentId: 'agent-1',
guardScriptPath,
hookScriptPath,
});
},
async close() {
await rm(rootDir, { recursive: true, force: true });
},
};
}
function managedHandler(config, event, scriptName) {
return config.hooks?.[event]?.find((handler) => handler.command?.includes(scriptName));
}
function assertNativeHandler(handler) {
assert.deepEqual(Object.keys(handler).sort(), ['command', 'failClosed', 'timeout']);
assert.equal(handler.failClosed, true);
assert.equal(handler.timeout, 10);
assert.match(handler.command, /node(?:\.exe)?/i);
if (process.platform === 'win32') {
assert.match(handler.command, /^& /);
assert.match(handler.command, /; exit \$LASTEXITCODE$/);
} else {
assert.match(handler.command, /^'/);
}
}
function legacyHandler(scriptPath) {
return { command: `node "${scriptPath}"` };
}
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);
@@ -155,11 +31,10 @@ test('Cursor is registered with the native user hook file', async () => {
});
});
test('Cursor install preserves user hooks, migrates legacy entries, and is idempotent', async () => {
test('Cursor install preserves hooks, migrates legacy entries, and is idempotent', async () => {
const fixture = await createFixture();
try {
await writeJson(fixture.configPath, {
version: 1,
description: 'user-owned',
hooks: {
sessionStart: [{ command: 'user-session' }],
@@ -168,9 +43,11 @@ test('Cursor install preserves user hooks, migrates legacy entries, and is idemp
legacyHandler(fixture.guardScriptPath),
],
postToolUse: [legacyHandler(fixture.hookScriptPath)],
postToolUseFailure: [legacyHandler(fixture.hookScriptPath)],
},
});
assert.equal((await fixture.install()).code, 0);
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
const second = await fixture.install();
assert.equal(second.code, 0, second.stderr);
@@ -181,63 +58,16 @@ test('Cursor install preserves user hooks, migrates legacy entries, and is idemp
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 hooks block freezes and forward official input byte-for-byte', async () => {
const capturePath = path.join(os.tmpdir(), `elydora-cursor-event-${process.pid}-${Date.now()}.json`);
const fixture = await createFixture({
hookSource: `
const fs = require('node:fs');
const chunks = [];
process.stdin.on('data', (chunk) => chunks.push(chunk));
process.stdin.on('end', () => fs.writeFileSync(process.env.ELYDORA_CAPTURE, Buffer.concat(chunks)));
`,
});
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 audit = managedHandler(config, 'postToolUse', 'hook.js');
const prePayload = `${JSON.stringify({
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,
})}\n`;
const guardResult = await runHook(guard, prePayload);
assert.equal(guardResult.code, 2, guardResult.stderr);
assert.match(guardResult.stderr, /Agent is frozen by Elydora/);
const postPayload = `${JSON.stringify({
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,
})}\n`;
const auditResult = await runHook(audit, postPayload, { ELYDORA_CAPTURE: capturePath });
assert.equal(auditResult.code, 0, auditResult.stderr);
assert.equal(await readFile(capturePath, 'utf-8'), postPayload);
} finally {
await fixture.close();
await rm(capturePath, { force: true });
}
});
test('Cursor status requires an exact pair, matching identity, and physical runtimes', async () => {
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);
@@ -245,25 +75,27 @@ test('Cursor status requires an exact pair, matching identity, and physical runt
assert.equal(status.code, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).installed, true);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
config.hooks.preToolUse[0].failClosed = false;
await writeJson(fixture.configPath, config);
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 auditSource = await readFile(fixture.hookScriptPath);
await rm(fixture.hookScriptPath);
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
await writeFile(fixture.hookScriptPath, auditSource);
const runtimeConfigPath = path.join(fixture.agentDir, 'config.json');
await writeJson(runtimeConfigPath, { agent_id: 'another-agent', agent_name: 'cursor' });
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(runtimeConfigPath, '{ malformed');
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);
@@ -279,14 +111,16 @@ test('Cursor uninstall removes exact ownership and preserves user entries', asyn
try {
assert.equal((await fixture.install()).code, 0);
const config = JSON.parse(await readFile(fixture.configPath, 'utf-8'));
const otherGuard = structuredClone(managedHandler(config, 'preToolUse', 'guard.js'));
const otherAudit = structuredClone(managedHandler(config, 'postToolUse', 'hook.js'));
otherGuard.command = otherGuard.command.replaceAll('agent-1', 'agent-10');
otherAudit.command = otherAudit.command.replaceAll('agent-1', 'agent-10');
config.hooks.preToolUse.push(otherGuard, {
command: 'echo elydora agent-1 guard.js', timeout: 10, failClosed: true,
});
config.hooks.postToolUse.push(otherAudit);
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');
@@ -295,17 +129,19 @@ test('Cursor uninstall removes exact ownership and preserves user entries', asyn
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, and invalid configs before writes', async () => {
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',
@@ -320,54 +156,20 @@ test('Cursor rejects malformed, duplicate, and invalid configs before writes', a
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 rejects missing and symbolic-link files before config writes', async (t) => {
const missing = await createFixture({ createRuntimes: false });
try {
const result = await missing.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /runtime is missing/i);
await assert.rejects(readFile(missing.configPath), { code: 'ENOENT' });
} finally {
await missing.close();
}
const linkedRuntime = await createFixture();
try {
const target = path.join(linkedRuntime.homeDir, 'guard-target.js');
await writeFile(target, 'process.exit(2);\n');
await rm(linkedRuntime.guardScriptPath);
try {
await symlink(target, linkedRuntime.guardScriptPath);
} catch (error) {
if (error?.code === 'EPERM') t.skip(`symbolic links unavailable: ${error.message}`);
throw error;
}
const result = await linkedRuntime.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /physical file/i);
await assert.rejects(readFile(linkedRuntime.configPath), { code: 'ENOENT' });
} finally {
await linkedRuntime.close();
}
});
test('Cursor confines both runtimes to the managed agent directory', async () => {
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', {
agentName: 'cursor',
agentId: 'agent-1',
guardScriptPath: fixture.guardScriptPath,
hookScriptPath: fixture.hookScriptPath,
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' });
@@ -377,17 +179,21 @@ test('Cursor confines both runtimes to the managed agent directory', async () =>
}
});
test('Cursor rejects symbolic-link config and preserves its target', async (t) => {
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}`);
if (error?.code === 'EPERM') {
t.skip(`symbolic links unavailable: ${error.message}`);
return;
}
throw error;
}
const result = await fixture.install();
@@ -401,12 +207,13 @@ test('Cursor rejects symbolic-link config and preserves its target', async (t) =
});
test('Cursor status rejects symbolic-link runtime files', async (t) => {
for (const name of ['config', 'guard', 'audit']) {
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];
@@ -417,7 +224,10 @@ test('Cursor status rejects symbolic-link runtime files', async (t) => {
try {
await symlink(target, filePath);
} catch (error) {
if (error?.code === 'EPERM') t.skip(`symbolic links unavailable: ${error.message}`);
if (error?.code === 'EPERM') {
t.skip(`symbolic links unavailable: ${error.message}`);
return;
}
throw error;
}
const status = await runPlugin(fixture, 'status', null);
@@ -429,7 +239,7 @@ test('Cursor status rejects symbolic-link runtime files', async (t) => {
}
});
test('Cursor removes an entirely managed config and leaves absent config absent', async () => {
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);
@@ -443,48 +253,29 @@ test('Cursor removes an entirely managed config and leaves absent config absent'
}
});
test('Cursor atomic writes are private and leave no staging files', async () => {
test('Cursor installation writes private files and leaves no transaction files', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const files = await readdir(path.dirname(fixture.configPath));
assert.equal(files.some((name) => name.endsWith('.tmp') || name.endsWith('.rollback')), false);
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') {
assert.equal((await stat(fixture.configPath)).mode & 0o777, 0o600);
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();
}
});
test('Cursor atomic write detects a concurrent source change', async () => {
const fixture = await createFixture({
existingConfig: { version: 1, hooks: { sessionStart: [{ command: 'original' }] } },
});
const concurrent = '{"version":1,"hooks":{"sessionStart":[{"command":"concurrent"}]}}\n';
try {
const source = `
import { writeFile } from 'node:fs/promises';
import { readDocument, writeDocument } from ${JSON.stringify(ioModuleUrl)};
const document = await readDocument();
await writeFile(document.filePath, process.env.ELYDORA_CONCURRENT, 'utf-8');
await writeDocument({
document,
changed: true,
next: '{"version":1,"hooks":{}}\\n',
});
`;
const result = await runNode(
['--input-type=module', '--eval', source],
{ HOME: fixture.homeDir, USERPROFILE: fixture.homeDir, ELYDORA_CONCURRENT: concurrent },
fixture.projectDir,
);
assert.equal(result.code, 1);
assert.match(result.stderr, /changed during update/i);
assert.equal(await readFile(fixture.configPath, 'utf-8'), concurrent);
const files = await readdir(path.dirname(fixture.configPath));
assert.equal(files.some((name) => name.endsWith('.tmp')), false);
} 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();
}
});