declare const require: (name: string) => any const { afterEach, describe, expect, spyOn, test } = require("bun:test") import { createEventHandler } from "./event" import { _resetForTesting, setMainSession } from "../features/claude-code-session-state" import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook" import * as connectedProvidersCache from "../shared/connected-providers-cache" import { unsafeTestValue } from "../../test-support/unsafe-test-value" let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined function setupConnectedProviderCacheMocks(): void { readConnectedProvidersCacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null) readProviderModelsCacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null) } type PromptBody = { path: { id: string } body: { parts: Array<{ type: "text" text: string synthetic?: boolean metadata?: Record }> agent?: string model?: { providerID: string; modelID: string } variant?: string noReply?: boolean } query: { directory: string } } function expectSyntheticContinuation(body: PromptBody["body"]): void { expect(body.noReply).toBeUndefined() expect(body.parts[0]?.synthetic).toBe(true) expect(body.parts[0]?.metadata?.compaction_continue).toBe(true) } describe("createEventHandler - model-fallback auto-continuation pins agent/model/variant", () => { const createHandler = (args?: { hooks?: any pluginConfig?: any withPromptAsync?: boolean }) => { setupConnectedProviderCacheMocks() const promptAsyncBodies: PromptBody[] = [] const promptBodies: PromptBody[] = [] const sessionClient: Record = { abort: async () => ({}), prompt: async (input: PromptBody) => { promptBodies.push(input) return {} }, } if (args?.withPromptAsync ?? true) { sessionClient.promptAsync = async (input: PromptBody) => { promptAsyncBodies.push(input) return {} } } const handler = createEventHandler({ ctx: unsafeTestValue({ directory: "/tmp", client: { session: sessionClient }, }), pluginConfig: unsafeTestValue((args?.pluginConfig ?? {})), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, managers: unsafeTestValue({ tmuxSessionManager: { onSessionCreated: async () => {}, onSessionDeleted: async () => {}, }, skillMcpManager: { disconnectSession: async () => {}, }, }), hooks: args?.hooks ?? (unsafeTestValue({})), }) return { handler, promptAsyncBodies, promptBodies } } afterEach(() => { readConnectedProvidersCacheSpy?.mockRestore() readProviderModelsCacheSpy?.mockRestore() readConnectedProvidersCacheSpy = undefined readProviderModelsCacheSpy = undefined _resetForTesting() }) test("pins agent/model on promptAsync body when continuing after message.updated fallback", async () => { // given const sessionID = "ses_pin_message_updated" setMainSession(sessionID) const modelFallback = createModelFallbackHook() clearPendingModelFallback(modelFallback, sessionID) const { handler, promptAsyncBodies } = createHandler({ hooks: { modelFallback } }) // when await handler({ event: { type: "message.updated", properties: { info: { id: "msg_err_pin_1", sessionID, role: "assistant", time: { created: 1, completed: 2 }, error: { name: "APIError", data: { message: "Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}", isRetryable: true, }, }, parentID: "msg_user_pin_1", modelID: "claude-opus-4-7-thinking", providerID: "anthropic", agent: "Sisyphus - Ultraworker", }, }, }, }) // then expect(promptAsyncBodies.length).toBe(1) const body = promptAsyncBodies[0]!.body expect(body.agent).toBeDefined() expect(body.agent).toContain("Sisyphus") expect(body.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-7", }) expectSyntheticContinuation(body) }) test("pins agent/model on promptAsync body when continuing after session.error fallback", async () => { // given const sessionID = "ses_pin_session_error" setMainSession(sessionID) const modelFallback = createModelFallbackHook() clearPendingModelFallback(modelFallback, sessionID) const { handler, promptAsyncBodies } = createHandler({ hooks: { modelFallback } }) // when await handler({ event: { type: "session.error", properties: { sessionID, providerID: "anthropic", modelID: "claude-opus-4-7-thinking", error: { name: "UnknownError", data: { error: { message: "Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}", }, }, }, }, }, }) // then expect(promptAsyncBodies.length).toBe(1) const body = promptAsyncBodies[0]!.body expect(body.agent).toBeDefined() expect(body.agent?.toLowerCase()).toContain("sisyphus") expect(body.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-7", }) expectSyntheticContinuation(body) }) test("pins agent/model on fallback prompt() body when promptAsync is not available (session.status)", async () => { // given const sessionID = "ses_pin_session_status_noasync" setMainSession(sessionID) const modelFallback = createModelFallbackHook() clearPendingModelFallback(modelFallback, sessionID) const { handler, promptBodies, promptAsyncBodies } = createHandler({ hooks: { modelFallback }, withPromptAsync: false, }) await handler({ event: { type: "message.updated", properties: { info: { id: "msg_user_status_noasync", sessionID, role: "user", modelID: "claude-opus-4-7-thinking", providerID: "anthropic", agent: "Sisyphus - Ultraworker", }, }, }, }) // when await handler({ event: { type: "session.status", properties: { sessionID, status: { type: "retry", attempt: 1, message: "Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}", next: 1234, }, }, }, }) // then expect(promptAsyncBodies.length).toBe(0) expect(promptBodies.length).toBe(1) const body = promptBodies[0]!.body expect(body.agent).toBeDefined() expect(body.agent).toContain("Sisyphus") expect(body.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-7", }) expectSyntheticContinuation(body) }) test("pins variant from agent config when present", async () => { // given const sessionID = "ses_pin_variant" setMainSession(sessionID) const modelFallback = createModelFallbackHook() clearPendingModelFallback(modelFallback, sessionID) const pluginConfig = { agents: { sisyphus: { variant: "thinking", }, }, } const { handler, promptAsyncBodies } = createHandler({ hooks: { modelFallback }, pluginConfig, }) // when await handler({ event: { type: "session.error", properties: { sessionID, providerID: "anthropic", modelID: "claude-opus-4-7-thinking", error: { name: "UnknownError", data: { error: { message: "Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}", }, }, }, }, }, }) // then expect(promptAsyncBodies.length).toBe(1) const body = promptAsyncBodies[0]!.body expect(body.variant).toBe("thinking") expectSyntheticContinuation(body) }) })