mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 13:05:01 +00:00
refactor(agents): dedupe model fallback probe failure tests
This commit is contained in:
@@ -57,6 +57,47 @@ function expectPrimaryProbeSuccess(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function expectProbeFailureFallsBack({
|
||||||
|
reason,
|
||||||
|
probeError,
|
||||||
|
}: {
|
||||||
|
reason: "rate_limit" | "overloaded";
|
||||||
|
probeError: Error & { status: number };
|
||||||
|
}) {
|
||||||
|
const cfg = makeCfg({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
primary: "openai/gpt-4.1-mini",
|
||||||
|
fallbacks: ["anthropic/claude-haiku-3-5", "google/gemini-2-flash"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Partial<OpenClawConfig>);
|
||||||
|
|
||||||
|
mockedIsProfileInCooldown.mockReturnValue(true);
|
||||||
|
mockedGetSoonestCooldownExpiry.mockReturnValue(1_700_000_000_000 + 30 * 1000);
|
||||||
|
mockedResolveProfilesUnavailableReason.mockReturnValue(reason);
|
||||||
|
|
||||||
|
const run = vi.fn().mockRejectedValueOnce(probeError).mockResolvedValue("fallback-ok");
|
||||||
|
|
||||||
|
const result = await runWithModelFallback({
|
||||||
|
cfg,
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-4.1-mini",
|
||||||
|
run,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.result).toBe("fallback-ok");
|
||||||
|
expect(run).toHaveBeenCalledTimes(2);
|
||||||
|
expect(run).toHaveBeenNthCalledWith(1, "openai", "gpt-4.1-mini", {
|
||||||
|
allowTransientCooldownProbe: true,
|
||||||
|
});
|
||||||
|
expect(run).toHaveBeenNthCalledWith(2, "anthropic", "claude-haiku-3-5", {
|
||||||
|
allowTransientCooldownProbe: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("runWithModelFallback – probe logic", () => {
|
describe("runWithModelFallback – probe logic", () => {
|
||||||
let realDateNow: () => number;
|
let realDateNow: () => number;
|
||||||
const NOW = 1_700_000_000_000;
|
const NOW = 1_700_000_000_000;
|
||||||
@@ -166,82 +207,16 @@ describe("runWithModelFallback – probe logic", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("attempts non-primary fallbacks during rate-limit cooldown after primary probe failure", async () => {
|
it("attempts non-primary fallbacks during rate-limit cooldown after primary probe failure", async () => {
|
||||||
const cfg = makeCfg({
|
await expectProbeFailureFallsBack({
|
||||||
agents: {
|
reason: "rate_limit",
|
||||||
defaults: {
|
probeError: Object.assign(new Error("rate limited"), { status: 429 }),
|
||||||
model: {
|
|
||||||
primary: "openai/gpt-4.1-mini",
|
|
||||||
fallbacks: ["anthropic/claude-haiku-3-5", "google/gemini-2-flash"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Partial<OpenClawConfig>);
|
|
||||||
|
|
||||||
// Override: ALL providers in cooldown for this test
|
|
||||||
mockedIsProfileInCooldown.mockReturnValue(true);
|
|
||||||
|
|
||||||
// All profiles in cooldown, cooldown just about to expire
|
|
||||||
const almostExpired = NOW + 30 * 1000; // 30s remaining
|
|
||||||
mockedGetSoonestCooldownExpiry.mockReturnValue(almostExpired);
|
|
||||||
|
|
||||||
// Primary probe fails with 429; fallback should still be attempted for rate_limit cooldowns.
|
|
||||||
const run = vi
|
|
||||||
.fn()
|
|
||||||
.mockRejectedValueOnce(Object.assign(new Error("rate limited"), { status: 429 }))
|
|
||||||
.mockResolvedValue("fallback-ok");
|
|
||||||
|
|
||||||
const result = await runWithModelFallback({
|
|
||||||
cfg,
|
|
||||||
provider: "openai",
|
|
||||||
model: "gpt-4.1-mini",
|
|
||||||
run,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.result).toBe("fallback-ok");
|
|
||||||
expect(run).toHaveBeenCalledTimes(2);
|
|
||||||
expect(run).toHaveBeenNthCalledWith(1, "openai", "gpt-4.1-mini", {
|
|
||||||
allowTransientCooldownProbe: true,
|
|
||||||
});
|
|
||||||
expect(run).toHaveBeenNthCalledWith(2, "anthropic", "claude-haiku-3-5", {
|
|
||||||
allowTransientCooldownProbe: true,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("attempts non-primary fallbacks during overloaded cooldown after primary probe failure", async () => {
|
it("attempts non-primary fallbacks during overloaded cooldown after primary probe failure", async () => {
|
||||||
const cfg = makeCfg({
|
await expectProbeFailureFallsBack({
|
||||||
agents: {
|
reason: "overloaded",
|
||||||
defaults: {
|
probeError: Object.assign(new Error("service overloaded"), { status: 503 }),
|
||||||
model: {
|
|
||||||
primary: "openai/gpt-4.1-mini",
|
|
||||||
fallbacks: ["anthropic/claude-haiku-3-5", "google/gemini-2-flash"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Partial<OpenClawConfig>);
|
|
||||||
|
|
||||||
mockedIsProfileInCooldown.mockReturnValue(true);
|
|
||||||
mockedGetSoonestCooldownExpiry.mockReturnValue(NOW + 30 * 1000);
|
|
||||||
mockedResolveProfilesUnavailableReason.mockReturnValue("overloaded");
|
|
||||||
|
|
||||||
const run = vi
|
|
||||||
.fn()
|
|
||||||
.mockRejectedValueOnce(Object.assign(new Error("service overloaded"), { status: 503 }))
|
|
||||||
.mockResolvedValue("fallback-ok");
|
|
||||||
|
|
||||||
const result = await runWithModelFallback({
|
|
||||||
cfg,
|
|
||||||
provider: "openai",
|
|
||||||
model: "gpt-4.1-mini",
|
|
||||||
run,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.result).toBe("fallback-ok");
|
|
||||||
expect(run).toHaveBeenCalledTimes(2);
|
|
||||||
expect(run).toHaveBeenNthCalledWith(1, "openai", "gpt-4.1-mini", {
|
|
||||||
allowTransientCooldownProbe: true,
|
|
||||||
});
|
|
||||||
expect(run).toHaveBeenNthCalledWith(2, "anthropic", "claude-haiku-3-5", {
|
|
||||||
allowTransientCooldownProbe: true,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user