124 lines
4.3 KiB
TypeScript
124 lines
4.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
resolveActiveFallbackState,
|
|
resolveFallbackTransition,
|
|
type FallbackNoticeState,
|
|
} from "./fallback-state.js";
|
|
|
|
const baseAttempt = {
|
|
provider: "fireworks",
|
|
model: "fireworks/minimax-m2p5",
|
|
error: "Provider fireworks is in cooldown (all profiles unavailable)",
|
|
reason: "rate_limit" as const,
|
|
};
|
|
|
|
describe("fallback-state", () => {
|
|
it("treats fallback as active only when state matches selected and active refs", () => {
|
|
const state: FallbackNoticeState = {
|
|
fallbackNoticeSelectedModel: "fireworks/minimax-m2p5",
|
|
fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5",
|
|
fallbackNoticeReason: "rate limit",
|
|
};
|
|
|
|
const resolved = resolveActiveFallbackState({
|
|
selectedModelRef: "fireworks/minimax-m2p5",
|
|
activeModelRef: "deepinfra/moonshotai/Kimi-K2.5",
|
|
state,
|
|
});
|
|
|
|
expect(resolved.active).toBe(true);
|
|
expect(resolved.reason).toBe("rate limit");
|
|
});
|
|
|
|
it("does not treat runtime drift as fallback when persisted state does not match", () => {
|
|
const state: FallbackNoticeState = {
|
|
fallbackNoticeSelectedModel: "anthropic/claude",
|
|
fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5",
|
|
fallbackNoticeReason: "rate limit",
|
|
};
|
|
|
|
const resolved = resolveActiveFallbackState({
|
|
selectedModelRef: "fireworks/minimax-m2p5",
|
|
activeModelRef: "deepinfra/moonshotai/Kimi-K2.5",
|
|
state,
|
|
});
|
|
|
|
expect(resolved.active).toBe(false);
|
|
expect(resolved.reason).toBeUndefined();
|
|
});
|
|
|
|
it("marks fallback transition when selected->active pair changes", () => {
|
|
const resolved = resolveFallbackTransition({
|
|
selectedProvider: "fireworks",
|
|
selectedModel: "fireworks/minimax-m2p5",
|
|
activeProvider: "deepinfra",
|
|
activeModel: "moonshotai/Kimi-K2.5",
|
|
attempts: [baseAttempt],
|
|
state: {},
|
|
});
|
|
|
|
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("fireworks/minimax-m2p5");
|
|
expect(resolved.nextState.activeModel).toBe("deepinfra/moonshotai/Kimi-K2.5");
|
|
});
|
|
|
|
it("normalizes fallback reason whitespace for summaries", () => {
|
|
const resolved = resolveFallbackTransition({
|
|
selectedProvider: "fireworks",
|
|
selectedModel: "fireworks/minimax-m2p5",
|
|
activeProvider: "deepinfra",
|
|
activeModel: "moonshotai/Kimi-K2.5",
|
|
attempts: [{ ...baseAttempt, reason: "rate_limit\n\tburst" }],
|
|
state: {},
|
|
});
|
|
|
|
expect(resolved.reasonSummary).toBe("rate limit burst");
|
|
});
|
|
|
|
it("refreshes reason when fallback remains active with same model pair", () => {
|
|
const resolved = resolveFallbackTransition({
|
|
selectedProvider: "fireworks",
|
|
selectedModel: "fireworks/minimax-m2p5",
|
|
activeProvider: "deepinfra",
|
|
activeModel: "moonshotai/Kimi-K2.5",
|
|
attempts: [{ ...baseAttempt, reason: "timeout" }],
|
|
state: {
|
|
fallbackNoticeSelectedModel: "fireworks/minimax-m2p5",
|
|
fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5",
|
|
fallbackNoticeReason: "rate limit",
|
|
},
|
|
});
|
|
|
|
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 = resolveFallbackTransition({
|
|
selectedProvider: "fireworks",
|
|
selectedModel: "fireworks/minimax-m2p5",
|
|
activeProvider: "fireworks",
|
|
activeModel: "fireworks/minimax-m2p5",
|
|
attempts: [],
|
|
state: {
|
|
fallbackNoticeSelectedModel: "fireworks/minimax-m2p5",
|
|
fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5",
|
|
fallbackNoticeReason: "rate limit",
|
|
},
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|