test: isolate recovery hook mocks from full suite
This commit is contained in:
@@ -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<ExecuteCompactFn>(async () => {})
|
||||
export const getLastAssistantMock = mock<GetLastAssistantFn>(async (): Promise<MockLastAssistant> => ({
|
||||
info: {
|
||||
providerID: "anthropic",
|
||||
modelID: "claude-sonnet-4-6",
|
||||
},
|
||||
hasContent: true,
|
||||
}))
|
||||
export const parseAnthropicTokenLimitErrorMock = mock<ParseAnthropicTokenLimitErrorFn>(() => ({
|
||||
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<typeof setTimeout>
|
||||
restore: () => void
|
||||
getClearTimeoutCalls: () => Array<ReturnType<typeof setTimeout>>
|
||||
getScheduledTimeouts: () => Array<ReturnType<typeof setTimeout>>
|
||||
} {
|
||||
const originalSetTimeout = globalThis.setTimeout
|
||||
const originalClearTimeout = globalThis.clearTimeout
|
||||
const clearTimeoutCalls: Array<ReturnType<typeof setTimeout>> = []
|
||||
const scheduledTimeouts: Array<ReturnType<typeof setTimeout>> = []
|
||||
|
||||
function createTimeoutHandle(): ReturnType<typeof setTimeout> {
|
||||
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<typeof setTimeout>) => {
|
||||
clearTimeoutCalls.push(timeoutID)
|
||||
originalClearTimeout(timeoutID)
|
||||
}) as typeof clearTimeout
|
||||
|
||||
return {
|
||||
createUntrackedTimeout: createTimeoutHandle,
|
||||
restore: () => {
|
||||
globalThis.setTimeout = originalSetTimeout
|
||||
globalThis.clearTimeout = originalClearTimeout
|
||||
},
|
||||
getClearTimeoutCalls: () => clearTimeoutCalls,
|
||||
getScheduledTimeouts: () => scheduledTimeouts,
|
||||
}
|
||||
}
|
||||
@@ -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<ReturnType<typeof setTimeout>>
|
||||
} {
|
||||
const originalSetTimeout = globalThis.setTimeout
|
||||
const originalClearTimeout = globalThis.clearTimeout
|
||||
const clearTimeoutCalls: Array<ReturnType<typeof setTimeout>> = []
|
||||
let timeoutCounter = 0
|
||||
|
||||
globalThis.setTimeout = ((_: () => void, _delay?: number) => {
|
||||
timeoutCounter += 1
|
||||
return timeoutCounter as ReturnType<typeof setTimeout>
|
||||
}) as typeof setTimeout
|
||||
|
||||
globalThis.clearTimeout = ((timeoutID: ReturnType<typeof setTimeout>) => {
|
||||
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<typeof setTimeout>])
|
||||
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<typeof setTimeout>])
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user