import { describe, expect, it } from "bun:test" import type { HookDeps, RuntimeFallbackPluginInput } from "./types" import type { AutoRetryHelpers } from "./auto-retry" import { createFallbackState } from "./fallback-state" type MessageUpdateHandlerModule = typeof import("./message-update-handler") async function importFreshMessageUpdateHandlerModule(): Promise { return import(`./message-update-handler?success-retry-key-${Date.now()}-${Math.random()}`) } function createContext(messagesResponse: unknown): RuntimeFallbackPluginInput { return { client: { session: { abort: async () => ({}), messages: async () => messagesResponse, promptAsync: async () => ({}), }, tui: { showToast: async () => ({}), }, }, directory: "/test/dir", } } function createDeps(messagesResponse: unknown): HookDeps { return { ctx: createContext(messagesResponse), config: { enabled: true, retry_on_errors: [429, 503, 529], max_fallback_attempts: 3, cooldown_seconds: 60, timeout_seconds: 30, notify_on_fallback: false, }, options: undefined, pluginConfig: {}, sessionStates: new Map(), sessionLastAccess: new Map(), sessionRetryInFlight: new Set(), sessionAwaitingFallbackResult: new Set(), sessionFallbackTimeouts: new Map(), sessionStatusRetryKeys: new Map(), } } function createHelpers(clearCalls: string[]): AutoRetryHelpers { return { abortSessionRequest: async () => {}, clearSessionFallbackTimeout: (sessionID: string) => { clearCalls.push(sessionID) }, scheduleSessionFallbackTimeout: () => {}, autoRetryWithFallback: async () => {}, resolveAgentForSessionFromContext: async () => undefined, cleanupStaleSessions: () => {}, } } describe("createMessageUpdateHandler retry-key cleanup", () => { it("#given a visible assistant reply after the latest user turn #when a non-error assistant update arrives #then the retry dedupe key is cleared with the fallback watchdog", async () => { // given const { createMessageUpdateHandler } = await importFreshMessageUpdateHandlerModule() const sessionID = "session-visible-assistant" const clearCalls: string[] = [] const deps = createDeps({ data: [ { info: { role: "user" }, parts: [{ type: "text", text: "latest question" }] }, { info: { role: "assistant" }, parts: [{ type: "text", text: "visible answer" }] }, ], }) const state = createFallbackState("google/gemini-2.5-pro") state.pendingFallbackModel = "openai/gpt-5.4" deps.sessionStates.set(sessionID, state) deps.sessionAwaitingFallbackResult.add(sessionID) deps.sessionStatusRetryKeys.set(sessionID, "retry:1") const handler = createMessageUpdateHandler(deps, createHelpers(clearCalls)) // when await handler({ info: { sessionID, role: "assistant", model: "openai/gpt-5.4", }, }) // then expect(deps.sessionAwaitingFallbackResult.has(sessionID)).toBe(false) expect(deps.sessionStatusRetryKeys.has(sessionID)).toBe(false) expect(state.pendingFallbackModel).toBe(undefined) expect(clearCalls).toEqual([sessionID]) }) })