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 { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import {
|
||||||
import * as originalExecutor from "./executor"
|
createRecoveryHook,
|
||||||
import * as originalParser from "./parser"
|
executeCompactMock,
|
||||||
import * as originalLogger from "../../shared/logger"
|
getLastAssistantMock,
|
||||||
|
parseAnthropicTokenLimitErrorMock,
|
||||||
const executeCompactMock = mock(async () => {})
|
setupDelayedTimeoutMocks,
|
||||||
const getLastAssistantMock = mock(async () => ({
|
} from "./recovery-hook.test-support"
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -90,9 +20,12 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
|||||||
|
|
||||||
test("cancels pending timer when session.idle handles compaction first", async () => {
|
test("cancels pending timer when session.idle handles compaction first", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { restore, getClearTimeoutCalls } = setupDelayedTimeoutMocks()
|
const { restore, getClearTimeoutCalls, getScheduledTimeouts } = setupDelayedTimeoutMocks()
|
||||||
const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook")
|
let compactedSessionID: unknown
|
||||||
const hook = createAnthropicContextWindowLimitRecoveryHook(createMockContext())
|
executeCompactMock.mockImplementationOnce(async (...args: unknown[]) => {
|
||||||
|
compactedSessionID = args[0]
|
||||||
|
})
|
||||||
|
const hook = createRecoveryHook()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//#when
|
//#when
|
||||||
@@ -111,9 +44,9 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(getClearTimeoutCalls()).toEqual([1 as ReturnType<typeof setTimeout>])
|
expect(getClearTimeoutCalls()).toEqual([getScheduledTimeouts()[0]])
|
||||||
expect(executeCompactMock).toHaveBeenCalledTimes(1)
|
expect(executeCompactMock).toHaveBeenCalledTimes(1)
|
||||||
expect(executeCompactMock.mock.calls[0]?.[0]).toBe("session-race")
|
expect(compactedSessionID).toBe("session-race")
|
||||||
} finally {
|
} finally {
|
||||||
restore()
|
restore()
|
||||||
}
|
}
|
||||||
@@ -121,7 +54,11 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
|||||||
|
|
||||||
test("does not treat empty summary assistant messages as successful compaction", async () => {
|
test("does not treat empty summary assistant messages as successful compaction", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { restore, getClearTimeoutCalls } = setupDelayedTimeoutMocks()
|
const { restore, getClearTimeoutCalls, getScheduledTimeouts } = setupDelayedTimeoutMocks()
|
||||||
|
let compactedSessionID: unknown
|
||||||
|
executeCompactMock.mockImplementationOnce(async (...args: unknown[]) => {
|
||||||
|
compactedSessionID = args[0]
|
||||||
|
})
|
||||||
getLastAssistantMock.mockResolvedValueOnce({
|
getLastAssistantMock.mockResolvedValueOnce({
|
||||||
info: {
|
info: {
|
||||||
summary: true,
|
summary: true,
|
||||||
@@ -130,8 +67,7 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
|||||||
},
|
},
|
||||||
hasContent: false,
|
hasContent: false,
|
||||||
})
|
})
|
||||||
const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook")
|
const hook = createRecoveryHook()
|
||||||
const hook = createAnthropicContextWindowLimitRecoveryHook(createMockContext())
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//#when
|
//#when
|
||||||
@@ -150,11 +86,12 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(getClearTimeoutCalls()).toEqual([1 as ReturnType<typeof setTimeout>])
|
expect(getClearTimeoutCalls()).toEqual([getScheduledTimeouts()[0]])
|
||||||
expect(executeCompactMock).toHaveBeenCalledTimes(1)
|
expect(executeCompactMock).toHaveBeenCalledTimes(1)
|
||||||
expect(executeCompactMock.mock.calls[0]?.[0]).toBe("session-empty-summary")
|
expect(compactedSessionID).toBe("session-empty-summary")
|
||||||
} finally {
|
} finally {
|
||||||
restore()
|
restore()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user