diff --git a/tests/unit/combo/execute-target-gates.test.ts b/tests/unit/combo/execute-target-gates.test.ts index 062909cbff..e25dfe5110 100644 --- a/tests/unit/combo/execute-target-gates.test.ts +++ b/tests/unit/combo/execute-target-gates.test.ts @@ -197,6 +197,82 @@ test("proceed stamps fallbackAttempts from the ordered-target index", async () = } }); +/** + * #13694 — combos 503d with ALL_TARGETS_SKIPPED after the 3.8.49→3.8.50 + * upgrade, with zero upstream attempts and no recovery after reboot. + * + * The 3.8.50 persisted-connection-cooldown gate (#11360) read SQLite rows + * that 3.8.49 had written but never consulted pre-dispatch, so upgrade-shaped + * state (stale `unavailable` labels) skipped every target before dispatch. + * #12168 bounded the bare-label skip with a grace window; these tests pin the + * gate — not just the predicate — so a whole pool of stale/orphan rows can + * never again produce a zero-attempt 503, while a genuinely fresh cooldown + * still skips (burst protection intact). + */ +async function seedCooldownConnection(input: { + provider: string; + testStatus: string; + lastErrorAt?: string | null; + rateLimitedUntil?: string | null; +}): Promise { + const providersDb = await import("../../../src/lib/db/providers.ts"); + const readCache = await import("../../../src/lib/db/readCache.ts"); + const connection = (await providersDb.createProviderConnection({ + provider: input.provider, + authType: "apikey", + name: `gates-13694-${Date.now()}-${Math.random().toString(16).slice(2)}`, + apiKey: "[REDACTED]", + testStatus: input.testStatus, + lastErrorAt: input.lastErrorAt ?? null, + rateLimitedUntil: input.rateLimitedUntil ?? null, + } as unknown as Record)) as { id: string }; + readCache.invalidateDbCache("connections"); + return connection.id; +} + +test("#13694: stale persisted unavailable label proceeds through pre-dispatch gates", async () => { + const { evaluateExecuteTargetGates } = + await import("../../../open-sse/services/combo/executeTargetGates.ts"); + const provider = `openai-gates-13694-stale-${Date.now()}`; + const connectionId = await seedCooldownConnection({ + provider, + testStatus: "unavailable", + // Well past the 60s unavailable-label grace window (#12168): the label is + // upgrade-shaped stale state, not a live cooldown. + lastErrorAt: new Date(Date.now() - 10 * 60 * 1000).toISOString(), + }); + const target = modelTarget({ provider, modelStr: `${provider}/gpt-4o-mini`, connectionId }); + const state = emptyState({ orderedTargets: [target] }); + const decision = await evaluateExecuteTargetGates({ index: 0, state, deps: baseDeps() }); + assert.equal(decision.kind, "proceed"); +}); + +test("#13694: orphan persisted unavailable label (no timestamps) proceeds", async () => { + const { evaluateExecuteTargetGates } = + await import("../../../open-sse/services/combo/executeTargetGates.ts"); + const provider = `openai-gates-13694-orphan-${Date.now()}`; + const connectionId = await seedCooldownConnection({ provider, testStatus: "unavailable" }); + const target = modelTarget({ provider, modelStr: `${provider}/gpt-4o-mini`, connectionId }); + const state = emptyState({ orderedTargets: [target] }); + const decision = await evaluateExecuteTargetGates({ index: 0, state, deps: baseDeps() }); + assert.equal(decision.kind, "proceed"); +}); + +test("#13694 control: recent persisted unavailable label still skips", async () => { + const { evaluateExecuteTargetGates } = + await import("../../../open-sse/services/combo/executeTargetGates.ts"); + const provider = `openai-gates-13694-fresh-${Date.now()}`; + const connectionId = await seedCooldownConnection({ + provider, + testStatus: "unavailable", + lastErrorAt: new Date().toISOString(), + }); + const target = modelTarget({ provider, modelStr: `${provider}/gpt-4o-mini`, connectionId }); + const state = emptyState({ orderedTargets: [target] }); + const decision = await evaluateExecuteTargetGates({ index: 0, state, deps: baseDeps() }); + assert.equal(decision.kind, "skip"); +}); + test("injection: dropping fallbackAttempts from targetForAttempt goes red", async () => { const { evaluateExecuteTargetGates } = await import("../../../open-sse/services/combo/executeTargetGates.ts");