import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test" import type { PluginInput } from "@opencode-ai/plugin" import type { ExperimentalConfig } from "../../config" import * as originalDeduplicationRecovery from "./deduplication-recovery" const attemptDeduplicationRecoveryMock = mock(async () => {}) mock.module("./deduplication-recovery", () => ({ attemptDeduplicationRecovery: attemptDeduplicationRecoveryMock, })) afterAll(() => { mock.module("./deduplication-recovery", () => originalDeduplicationRecovery) }) function createImmediateTimeouts(): () => void { const originalSetTimeout = globalThis.setTimeout const originalClearTimeout = globalThis.clearTimeout globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number, ...args: unknown[]) => { callback(...args) return 0 as unknown as ReturnType }) as typeof setTimeout globalThis.clearTimeout = ((_: ReturnType) => {}) as typeof clearTimeout return () => { globalThis.setTimeout = originalSetTimeout globalThis.clearTimeout = originalClearTimeout } } describe("createAnthropicContextWindowLimitRecoveryHook", () => { beforeEach(() => { attemptDeduplicationRecoveryMock.mockClear() }) test("calls deduplication recovery when compaction is already in progress", async () => { //#given const restoreTimeouts = createImmediateTimeouts() const experimental = { dynamic_context_pruning: { enabled: true, strategies: { deduplication: { enabled: true }, }, }, } satisfies ExperimentalConfig let resolveSummarize: (() => void) | null = null const summarizePromise = new Promise((resolve) => { resolveSummarize = resolve }) const mockClient = { session: { messages: mock(() => Promise.resolve({ data: [] })), summarize: mock(() => summarizePromise), revert: mock(() => Promise.resolve()), promptAsync: mock(() => Promise.resolve()), }, tui: { showToast: mock(() => Promise.resolve()), }, } try { const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook") const ctx = { client: mockClient, directory: "/tmp" } as PluginInput const hook = createAnthropicContextWindowLimitRecoveryHook(ctx, { experimental }) // first error triggers compaction (setTimeout runs immediately due to mock) await hook.event({ event: { type: "session.error", properties: { sessionID: "session-96", error: "prompt is too long" }, }, }) //#when - second error while compaction is in progress await hook.event({ event: { type: "session.error", properties: { sessionID: "session-96", error: "prompt is too long" }, }, }) //#then - deduplication recovery was called for the second error expect(attemptDeduplicationRecoveryMock).toHaveBeenCalledTimes(1) expect(attemptDeduplicationRecoveryMock.mock.calls[0]![0]).toBe("session-96") } finally { if (resolveSummarize) resolveSummarize() restoreTimeouts() } }) test("does not call deduplication when compaction is not in progress", async () => { //#given const mockClient = { session: { messages: mock(() => Promise.resolve({ data: [] })), summarize: mock(() => Promise.resolve()), revert: mock(() => Promise.resolve()), promptAsync: mock(() => Promise.resolve()), }, tui: { showToast: mock(() => Promise.resolve()), }, } const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook") const ctx = { client: mockClient, directory: "/tmp" } as PluginInput const hook = createAnthropicContextWindowLimitRecoveryHook(ctx) //#when - single error (no compaction in progress) await hook.event({ event: { type: "session.error", properties: { sessionID: "session-no-dedup", error: "some other error" }, }, }) //#then expect(attemptDeduplicationRecoveryMock).not.toHaveBeenCalled() }) })