openclaw/src/auto-reply/fallback-state.test.ts

110 lines
3.6 KiB
TypeScript

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<Parameters<typeof resolveFallbackTransition>[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();
});
});