diff --git a/src/shared/index.ts b/src/shared/index.ts index 93163dcc..44d6b1f5 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -41,3 +41,4 @@ export * from "./tmux" export * from "./model-suggestion-retry" export * from "./opencode-server-auth" export * from "./port-utils" +export * from "./safe-create-hook" diff --git a/src/shared/safe-create-hook.test.ts b/src/shared/safe-create-hook.test.ts new file mode 100644 index 00000000..72c326a6 --- /dev/null +++ b/src/shared/safe-create-hook.test.ts @@ -0,0 +1,73 @@ +import { describe, test, expect, spyOn, afterEach } from "bun:test" +import * as shared from "./logger" +import { safeCreateHook } from "./safe-create-hook" + +afterEach(() => { + ;(shared.log as any)?.mockRestore?.() +}) + +describe("safeCreateHook", () => { + test("returns hook object when factory succeeds", () => { + //#given + const hook = { handler: () => {} } + const factory = () => hook + + //#when + const result = safeCreateHook("test-hook", factory) + + //#then + expect(result).toBe(hook) + }) + + test("returns null when factory throws", () => { + //#given + spyOn(shared, "log").mockImplementation(() => {}) + const factory = () => { + throw new Error("boom") + } + + //#when + const result = safeCreateHook("test-hook", factory) + + //#then + expect(result).toBeNull() + }) + + test("logs error when factory throws", () => { + //#given + const logSpy = spyOn(shared, "log").mockImplementation(() => {}) + const factory = () => { + throw new Error("boom") + } + + //#when + safeCreateHook("my-hook", factory) + + //#then + expect(logSpy).toHaveBeenCalled() + const callArgs = logSpy.mock.calls[0] + expect(callArgs[0]).toContain("my-hook") + expect(callArgs[0]).toContain("Hook creation failed") + }) + + test("propagates error when enabled is false", () => { + //#given + const factory = () => { + throw new Error("boom") + } + + //#when + #then + expect(() => safeCreateHook("test-hook", factory, { enabled: false })).toThrow("boom") + }) + + test("returns null for factory returning undefined", () => { + //#given + const factory = () => undefined as any + + //#when + const result = safeCreateHook("test-hook", factory) + + //#then + expect(result).toBeNull() + }) +}) diff --git a/src/shared/safe-create-hook.ts b/src/shared/safe-create-hook.ts new file mode 100644 index 00000000..1ef3c9ee --- /dev/null +++ b/src/shared/safe-create-hook.ts @@ -0,0 +1,24 @@ +import { log } from "./logger" + +interface SafeCreateHookOptions { + enabled?: boolean +} + +export function safeCreateHook( + name: string, + factory: () => T, + options?: SafeCreateHookOptions, +): T | null { + const enabled = options?.enabled ?? true + + if (!enabled) { + return factory() ?? null + } + + try { + return factory() ?? null + } catch (error) { + log(`[safe-create-hook] Hook creation failed: ${name}`, { error }) + return null + } +}