From 2891c6c93c8118cb5e0e4eb05af4396ab38ff922 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 17:20:31 +0000 Subject: [PATCH] refactor(agents): dedupe model fallback probe failure tests --- src/agents/model-fallback.probe.test.ts | 119 ++++++++++-------------- 1 file changed, 47 insertions(+), 72 deletions(-) diff --git a/src/agents/model-fallback.probe.test.ts b/src/agents/model-fallback.probe.test.ts index 8dafd6533..bcb66628d 100644 --- a/src/agents/model-fallback.probe.test.ts +++ b/src/agents/model-fallback.probe.test.ts @@ -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); + + 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", () => { let realDateNow: () => number; 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 () => { - const cfg = makeCfg({ - agents: { - defaults: { - model: { - primary: "openai/gpt-4.1-mini", - fallbacks: ["anthropic/claude-haiku-3-5", "google/gemini-2-flash"], - }, - }, - }, - } as Partial); - - // 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, + await expectProbeFailureFallsBack({ + reason: "rate_limit", + probeError: Object.assign(new Error("rate limited"), { status: 429 }), }); }); it("attempts non-primary fallbacks during overloaded cooldown after primary probe failure", async () => { - const cfg = makeCfg({ - agents: { - defaults: { - model: { - primary: "openai/gpt-4.1-mini", - fallbacks: ["anthropic/claude-haiku-3-5", "google/gemini-2-flash"], - }, - }, - }, - } as Partial); - - 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, + await expectProbeFailureFallsBack({ + reason: "overloaded", + probeError: Object.assign(new Error("service overloaded"), { status: 503 }), }); });