import { describe, expect, it } from "vitest"; import { resolveActiveFallbackState, resolveFallbackTransition, type FallbackNoticeState, } from "./fallback-state.js"; const baseAttempt = { provider: "demo-primary", model: "demo-primary/model-a", error: "Provider demo-primary is in cooldown (all profiles unavailable)", reason: "rate_limit" as const, }; const activeFallbackState: FallbackNoticeState = { fallbackNoticeSelectedModel: "demo-primary/model-a", fallbackNoticeActiveModel: "demo-fallback/model-b", fallbackNoticeReason: "rate limit", }; function resolveDemoFallbackTransition( overrides: Partial[0]> = {}, ) { return resolveFallbackTransition({ selectedProvider: "demo-primary", selectedModel: "model-a", activeProvider: "demo-fallback", activeModel: "model-b", attempts: [baseAttempt], state: {}, ...overrides, }); } describe("fallback-state", () => { it.each([ { name: "treats fallback as active only when state matches selected and active refs", state: activeFallbackState, expected: { active: true, reason: "rate limit" }, }, { name: "does not treat runtime drift as fallback when persisted state does not match", state: { fallbackNoticeSelectedModel: "other-provider/other-model", fallbackNoticeActiveModel: "demo-fallback/model-b", fallbackNoticeReason: "rate limit", } satisfies FallbackNoticeState, expected: { active: false, reason: undefined }, }, ])("$name", ({ state, expected }) => { const resolved = resolveActiveFallbackState({ selectedModelRef: "demo-primary/model-a", activeModelRef: "demo-fallback/model-b", state, }); expect(resolved).toEqual(expected); }); it("marks fallback transition when selected->active pair changes", () => { const resolved = resolveDemoFallbackTransition(); expect(resolved.fallbackActive).toBe(true); expect(resolved.fallbackTransitioned).toBe(true); expect(resolved.fallbackCleared).toBe(false); expect(resolved.stateChanged).toBe(true); expect(resolved.reasonSummary).toBe("rate limit"); expect(resolved.nextState.selectedModel).toBe("demo-primary/model-a"); expect(resolved.nextState.activeModel).toBe("demo-fallback/model-b"); }); it("normalizes fallback reason whitespace for summaries", () => { const resolved = resolveDemoFallbackTransition({ attempts: [{ ...baseAttempt, reason: "rate_limit\n\tburst" }], }); expect(resolved.reasonSummary).toBe("rate limit burst"); }); it("refreshes reason when fallback remains active with same model pair", () => { const resolved = resolveDemoFallbackTransition({ attempts: [{ ...baseAttempt, reason: "timeout" }], state: activeFallbackState, }); expect(resolved.fallbackTransitioned).toBe(false); expect(resolved.stateChanged).toBe(true); expect(resolved.nextState.reason).toBe("timeout"); }); it("marks fallback as cleared when runtime returns to selected model", () => { const resolved = resolveDemoFallbackTransition({ activeProvider: "demo-primary", selectedModel: "model-a", activeModel: "model-a", attempts: [], state: activeFallbackState, }); expect(resolved.fallbackActive).toBe(false); expect(resolved.fallbackCleared).toBe(true); expect(resolved.fallbackTransitioned).toBe(false); expect(resolved.stateChanged).toBe(true); expect(resolved.nextState.selectedModel).toBeUndefined(); expect(resolved.nextState.activeModel).toBeUndefined(); expect(resolved.nextState.reason).toBeUndefined(); }); });