fix(runtime-fallback): scope visible-assistant check to current turn and cleanup retry dedupe keys

This commit is contained in:
YeonGyu-Kim
2026-03-13 10:54:47 +09:00
parent cbe113ebab
commit 9bce6314b1
8 changed files with 435 additions and 40 deletions
@@ -0,0 +1,107 @@
import { describe, expect, it } from "bun:test"
import type { HookDeps, RuntimeFallbackPluginInput } from "./types"
import type { AutoRetryHelpers } from "./auto-retry"
import { createFallbackState } from "./fallback-state"
import { createEventHandler } from "./event-handler"
function createContext(): RuntimeFallbackPluginInput {
return {
client: {
session: {
abort: async () => ({}),
messages: async () => ({ data: [] }),
promptAsync: async () => ({}),
},
tui: {
showToast: async () => ({}),
},
},
directory: "/test/dir",
}
}
function createDeps(): HookDeps {
return {
ctx: createContext(),
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(deps: HookDeps, abortCalls: string[], clearCalls: string[]): AutoRetryHelpers {
return {
abortSessionRequest: async (sessionID: string) => {
abortCalls.push(sessionID)
},
clearSessionFallbackTimeout: (sessionID: string) => {
clearCalls.push(sessionID)
deps.sessionFallbackTimeouts.delete(sessionID)
},
scheduleSessionFallbackTimeout: () => {},
autoRetryWithFallback: async () => {},
resolveAgentForSessionFromContext: async () => undefined,
cleanupStaleSessions: () => {},
}
}
describe("createEventHandler", () => {
it("#given a session retry dedupe key #when session.stop fires #then the retry dedupe key is cleared", async () => {
// given
const sessionID = "session-stop"
const deps = createDeps()
const abortCalls: string[] = []
const clearCalls: string[] = []
const state = createFallbackState("google/gemini-2.5-pro")
state.pendingFallbackModel = "openai/gpt-5.4"
deps.sessionStates.set(sessionID, state)
deps.sessionRetryInFlight.add(sessionID)
deps.sessionStatusRetryKeys.set(sessionID, "retry:1")
const handler = createEventHandler(deps, createHelpers(deps, abortCalls, clearCalls))
// when
await handler({ event: { type: "session.stop", properties: { sessionID } } })
// then
expect(deps.sessionStatusRetryKeys.has(sessionID)).toBe(false)
expect(clearCalls).toEqual([sessionID])
expect(abortCalls).toEqual([sessionID])
})
it("#given a session retry dedupe key without a pending fallback result #when session.idle fires #then the retry dedupe key is cleared", async () => {
// given
const sessionID = "session-idle"
const deps = createDeps()
const abortCalls: string[] = []
const clearCalls: string[] = []
const state = createFallbackState("google/gemini-2.5-pro")
state.pendingFallbackModel = "openai/gpt-5.4"
deps.sessionStates.set(sessionID, state)
deps.sessionRetryInFlight.add(sessionID)
deps.sessionFallbackTimeouts.set(sessionID, 1)
deps.sessionStatusRetryKeys.set(sessionID, "retry:1")
const handler = createEventHandler(deps, createHelpers(deps, abortCalls, clearCalls))
// when
await handler({ event: { type: "session.idle", properties: { sessionID } } })
// then
expect(deps.sessionStatusRetryKeys.has(sessionID)).toBe(false)
expect(clearCalls).toEqual([sessionID])
expect(abortCalls).toEqual([])
expect(state.pendingFallbackModel).toBe(undefined)
})
})