From 6a960a855e9efc5596d05bb1e9fd6ee7b4bf185b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 15 May 2026 21:31:43 -0700 Subject: [PATCH] Add Cursor Agent hook installer coverage --- src/main/cursor/hook-service.test.ts | 105 +++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/main/cursor/hook-service.test.ts diff --git a/src/main/cursor/hook-service.test.ts b/src/main/cursor/hook-service.test.ts new file mode 100644 index 000000000..036205235 --- /dev/null +++ b/src/main/cursor/hook-service.test.ts @@ -0,0 +1,105 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { dirname, join } from 'path' + +const { homedirMock } = vi.hoisted(() => ({ + homedirMock: vi.fn<() => string>() +})) + +vi.mock('os', async () => { + const actual = (await vi.importActual('os')) as Record + return { + ...actual, + homedir: homedirMock + } +}) + +import { CursorHookService } from './hook-service' + +const CURSOR_EVENTS = [ + 'beforeSubmitPrompt', + 'stop', + 'preToolUse', + 'postToolUse', + 'postToolUseFailure', + 'beforeShellExecution', + 'beforeMCPExecution', + 'afterAgentResponse' +] + +describe('CursorHookService', () => { + let homeDir: string + + beforeEach(() => { + homeDir = mkdtempSync(join(tmpdir(), 'orca-cursor-home-')) + homedirMock.mockReturnValue(homeDir) + }) + + afterEach(() => { + vi.clearAllMocks() + rmSync(homeDir, { recursive: true, force: true }) + }) + + it('installs Cursor Agent hooks with the documented top-level command schema', () => { + const status = new CursorHookService().install() + + expect(status.state).toBe('installed') + expect(status.configPath).toBe(join(homeDir, '.cursor', 'hooks.json')) + expect(status.managedHooksPresent).toBe(true) + + const config = JSON.parse(readFileSync(join(homeDir, '.cursor', 'hooks.json'), 'utf8')) as { + version?: number + hooks: Record + } + expect(config.version).toBe(1) + expect(Object.keys(config.hooks).sort()).toEqual([...CURSOR_EVENTS].sort()) + for (const eventName of CURSOR_EVENTS) { + const definition = config.hooks[eventName]?.[0] + expect(definition?.command).toContain('cursor-hook') + expect(definition?.command).toContain(join(homeDir, '.orca')) + expect(definition?.hooks).toBeUndefined() + } + + const script = readFileSync(join(homeDir, '.orca', 'agent-hooks', 'cursor-hook.sh'), 'utf8') + expect(script).toContain('/hook/cursor') + expect(script).toContain('payload=$(cat)') + }) + + it('preserves user-authored Cursor hook entries and removes stale managed entries', () => { + const configPath = join(homeDir, '.cursor', 'hooks.json') + mkdirSync(dirname(configPath), { recursive: true }) + writeFileSync( + configPath, + `${JSON.stringify( + { + version: 1, + hooks: { + beforeSubmitPrompt: [ + { command: '/usr/local/bin/user-hook' }, + { command: '/old/path/.orca/agent-hooks/cursor-hook.sh' } + ], + retiredEvent: [ + { command: '/old/path/.orca/agent-hooks/cursor-hook.sh' }, + { command: '/usr/local/bin/retired-user-hook' } + ] + } + }, + null, + 2 + )}\n` + ) + + new CursorHookService().install() + + const config = JSON.parse(readFileSync(configPath, 'utf8')) as { + hooks: Record + } + const promptCommands = config.hooks.beforeSubmitPrompt.map((definition) => definition.command) + expect(promptCommands).toContain('/usr/local/bin/user-hook') + expect(promptCommands.filter((command) => command?.includes('cursor-hook.sh'))).toHaveLength(1) + expect(config.hooks.retiredEvent.map((definition) => definition.command)).toEqual([ + '/usr/local/bin/retired-user-hook' + ]) + }) +})