From 9fa1ea38e84b2d08355fb47845a16fea54284f37 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 17:50:24 +0900 Subject: [PATCH] test: isolate recovery hook mocks from full suite --- .../recovery-hook.test-support.ts | 113 ++++++++++++++++++ .../recovery-hook.test.ts | 113 ++++-------------- 2 files changed, 138 insertions(+), 88 deletions(-) create mode 100644 src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test-support.ts diff --git a/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test-support.ts b/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test-support.ts new file mode 100644 index 000000000..e394a0040 --- /dev/null +++ b/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test-support.ts @@ -0,0 +1,113 @@ +import { mock } from "bun:test" +import type { PluginInput } from "@opencode-ai/plugin" +import type { OhMyOpenCodeConfig } from "../../config" +import { createAnthropicContextWindowLimitRecoveryHook } from "./recovery-hook" + +type ExecuteCompactFn = typeof import("./executor").executeCompact +type GetLastAssistantFn = typeof import("./executor").getLastAssistant +type ParseAnthropicTokenLimitErrorFn = typeof import("./parser").parseAnthropicTokenLimitError + +export type MockLastAssistant = { + info: { + summary?: boolean + providerID: string + modelID: string + } + hasContent: boolean +} + +export const executeCompactMock = mock(async () => {}) +export const getLastAssistantMock = mock(async (): Promise => ({ + info: { + providerID: "anthropic", + modelID: "claude-sonnet-4-6", + }, + hasContent: true, +})) +export const parseAnthropicTokenLimitErrorMock = mock(() => ({ + currentTokens: 250000, + maxTokens: 200000, + errorType: "token_limit_exceeded", + providerID: "anthropic", + modelID: "claude-sonnet-4-6", +})) + +const pluginConfig = { + git_master: { + commit_footer: false, + include_co_authored_by: false, + git_env_prefix: "", + }, +} satisfies OhMyOpenCodeConfig + +export function createRecoveryHook() { + return createAnthropicContextWindowLimitRecoveryHook( + createMockContext(), + { + pluginConfig, + dependencies: { + executeCompact: executeCompactMock, + getLastAssistant: getLastAssistantMock, + log: () => {}, + parseAnthropicTokenLimitError: parseAnthropicTokenLimitErrorMock, + }, + } as never, + ) +} + +export function createMockContext(): PluginInput { + return { + client: { + session: { + messages: mock(() => Promise.resolve({ data: [] })), + }, + tui: { + showToast: mock(() => Promise.resolve()), + }, + }, + project: {} as never, + directory: "/tmp", + worktree: "/tmp", + serverUrl: new URL("http://localhost"), + $: {} as never, + } as never +} + +export function setupDelayedTimeoutMocks(): { + createUntrackedTimeout: () => ReturnType + restore: () => void + getClearTimeoutCalls: () => Array> + getScheduledTimeouts: () => Array> +} { + const originalSetTimeout = globalThis.setTimeout + const originalClearTimeout = globalThis.clearTimeout + const clearTimeoutCalls: Array> = [] + const scheduledTimeouts: Array> = [] + + function createTimeoutHandle(): ReturnType { + const timeoutID = originalSetTimeout(() => {}, 60_000) + originalClearTimeout(timeoutID) + return timeoutID + } + + globalThis.setTimeout = ((_: () => void, _delay?: number) => { + const timeoutID = createTimeoutHandle() + scheduledTimeouts.push(timeoutID) + return timeoutID + }) as typeof setTimeout + + globalThis.clearTimeout = ((timeoutID: ReturnType) => { + clearTimeoutCalls.push(timeoutID) + originalClearTimeout(timeoutID) + }) as typeof clearTimeout + + return { + createUntrackedTimeout: createTimeoutHandle, + restore: () => { + globalThis.setTimeout = originalSetTimeout + globalThis.clearTimeout = originalClearTimeout + }, + getClearTimeoutCalls: () => clearTimeoutCalls, + getScheduledTimeouts: () => scheduledTimeouts, + } +} diff --git a/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts b/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts index f30046962..4291bb754 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts @@ -1,81 +1,11 @@ -import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from "bun:test" -import type { PluginInput } from "@opencode-ai/plugin" -import * as originalExecutor from "./executor" -import * as originalParser from "./parser" -import * as originalLogger from "../../shared/logger" - -const executeCompactMock = mock(async () => {}) -const getLastAssistantMock = mock(async () => ({ - info: { - providerID: "anthropic", - modelID: "claude-sonnet-4-6", - }, - hasContent: true, -})) -const parseAnthropicTokenLimitErrorMock = mock(() => ({ - providerID: "anthropic", - modelID: "claude-sonnet-4-6", -})) - -mock.module("./executor", () => ({ - executeCompact: executeCompactMock, - getLastAssistant: getLastAssistantMock, -})) - -mock.module("./parser", () => ({ - parseAnthropicTokenLimitError: parseAnthropicTokenLimitErrorMock, -})) - -mock.module("../../shared/logger", () => ({ - log: () => {}, -})) - -afterAll(() => { - mock.module("./executor", () => originalExecutor) - mock.module("./parser", () => originalParser) - mock.module("../../shared/logger", () => originalLogger) -}) - -function createMockContext(): PluginInput { - return { - client: { - session: { - messages: mock(() => Promise.resolve({ data: [] })), - }, - tui: { - showToast: mock(() => Promise.resolve()), - }, - }, - directory: "/tmp", - } as PluginInput -} - -function setupDelayedTimeoutMocks(): { - restore: () => void - getClearTimeoutCalls: () => Array> -} { - const originalSetTimeout = globalThis.setTimeout - const originalClearTimeout = globalThis.clearTimeout - const clearTimeoutCalls: Array> = [] - let timeoutCounter = 0 - - globalThis.setTimeout = ((_: () => void, _delay?: number) => { - timeoutCounter += 1 - return timeoutCounter as ReturnType - }) as typeof setTimeout - - globalThis.clearTimeout = ((timeoutID: ReturnType) => { - clearTimeoutCalls.push(timeoutID) - }) as typeof clearTimeout - - return { - restore: () => { - globalThis.setTimeout = originalSetTimeout - globalThis.clearTimeout = originalClearTimeout - }, - getClearTimeoutCalls: () => clearTimeoutCalls, - } -} +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { + createRecoveryHook, + executeCompactMock, + getLastAssistantMock, + parseAnthropicTokenLimitErrorMock, + setupDelayedTimeoutMocks, +} from "./recovery-hook.test-support" describe("createAnthropicContextWindowLimitRecoveryHook", () => { beforeEach(() => { @@ -90,9 +20,12 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => { test("cancels pending timer when session.idle handles compaction first", async () => { //#given - const { restore, getClearTimeoutCalls } = setupDelayedTimeoutMocks() - const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook") - const hook = createAnthropicContextWindowLimitRecoveryHook(createMockContext()) + const { restore, getClearTimeoutCalls, getScheduledTimeouts } = setupDelayedTimeoutMocks() + let compactedSessionID: unknown + executeCompactMock.mockImplementationOnce(async (...args: unknown[]) => { + compactedSessionID = args[0] + }) + const hook = createRecoveryHook() try { //#when @@ -111,9 +44,9 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => { }) //#then - expect(getClearTimeoutCalls()).toEqual([1 as ReturnType]) + expect(getClearTimeoutCalls()).toEqual([getScheduledTimeouts()[0]]) expect(executeCompactMock).toHaveBeenCalledTimes(1) - expect(executeCompactMock.mock.calls[0]?.[0]).toBe("session-race") + expect(compactedSessionID).toBe("session-race") } finally { restore() } @@ -121,7 +54,11 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => { test("does not treat empty summary assistant messages as successful compaction", async () => { //#given - const { restore, getClearTimeoutCalls } = setupDelayedTimeoutMocks() + const { restore, getClearTimeoutCalls, getScheduledTimeouts } = setupDelayedTimeoutMocks() + let compactedSessionID: unknown + executeCompactMock.mockImplementationOnce(async (...args: unknown[]) => { + compactedSessionID = args[0] + }) getLastAssistantMock.mockResolvedValueOnce({ info: { summary: true, @@ -130,8 +67,7 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => { }, hasContent: false, }) - const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook") - const hook = createAnthropicContextWindowLimitRecoveryHook(createMockContext()) + const hook = createRecoveryHook() try { //#when @@ -150,11 +86,12 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => { }) //#then - expect(getClearTimeoutCalls()).toEqual([1 as ReturnType]) + expect(getClearTimeoutCalls()).toEqual([getScheduledTimeouts()[0]]) expect(executeCompactMock).toHaveBeenCalledTimes(1) - expect(executeCompactMock.mock.calls[0]?.[0]).toBe("session-empty-summary") + expect(compactedSessionID).toBe("session-empty-summary") } finally { restore() } }) + })