describe("isProfileInCooldown", () => {
it("returns false when profile has no usage stats", () => { const store = makeStore(undefined);
expect(isProfileInCooldown(store, "anthropic:default")).toBe(false);
});
it("returns true when cooldownUntil is in the future", () => { const store = makeStore({ "anthropic:default": { cooldownUntil: Date.now() + 60_000 },
});
expect(isProfileInCooldown(store, "anthropic:default")).toBe(true);
});
it("returns false when cooldownUntil has passed", () => { const store = makeStore({ "anthropic:default": { cooldownUntil: Date.now() - 1_000 },
});
expect(isProfileInCooldown(store, "anthropic:default")).toBe(false);
});
it("returns true when disabledUntil is in the future (even if cooldownUntil expired)", () => { const store = makeStore({ "anthropic:default": {
cooldownUntil: Date.now() - 1_000,
disabledUntil: Date.now() + 60_000,
},
});
expect(isProfileInCooldown(store, "anthropic:default")).toBe(true);
});
it("returns false for OpenRouter even when cooldown fields exist", () => { const store = makeStore({ "openrouter:default": {
cooldownUntil: Date.now() + 60_000,
disabledUntil: Date.now() + 60_000,
disabledReason: "billing",
},
});
expect(isProfileInCooldown(store, "openrouter:default")).toBe(false);
});
it("returns false for Kilocode even when cooldown fields exist", () => { const store = makeStore({ "kilocode:default": {
cooldownUntil: Date.now() + 60_000,
disabledUntil: Date.now() + 60_000,
disabledReason: "billing",
},
});
expect(isProfileInCooldown(store, "kilocode:default")).toBe(false);
});
it("returns false for a different model when cooldown is model-scoped (rate_limit)", () => { const store = makeStore({ "github-copilot:github": {
cooldownUntil: Date.now() + 60_000,
cooldownReason: "rate_limit",
cooldownModel: "claude-sonnet-4.6",
},
}); // Different model bypasses the cooldown
expect(isProfileInCooldown(store, "github-copilot:github", undefined, "gpt-4.1")).toBe(false); // Same model is still blocked
expect(
isProfileInCooldown(store, "github-copilot:github", undefined, "claude-sonnet-4.6"),
).toBe(true); // No model specified — blocked (conservative)
expect(isProfileInCooldown(store, "github-copilot:github")).toBe(true);
});
it("returns true for all models when cooldownModel is undefined (profile-wide)", () => { const store = makeStore({ "github-copilot:github": {
cooldownUntil: Date.now() + 60_000,
cooldownReason: "rate_limit",
cooldownModel: undefined,
},
});
expect(
isProfileInCooldown(store, "github-copilot:github", undefined, "claude-sonnet-4.6"),
).toBe(true);
expect(isProfileInCooldown(store, "github-copilot:github", undefined, "gpt-4.1")).toBe(true);
});
it("does not bypass model-scoped cooldown when disabledUntil is active", () => { const store = makeStore({ "github-copilot:github": {
cooldownUntil: Date.now() + 60_000,
cooldownReason: "rate_limit",
cooldownModel: "claude-sonnet-4.6",
disabledUntil: Date.now() + 120_000,
disabledReason: "billing",
},
}); // Even though cooldownModel is for a different model, billing disable // should keep the profile blocked for all models.
expect(isProfileInCooldown(store, "github-copilot:github", undefined, "gpt-4.1")).toBe(true);
});
});
describe("resolveProfilesUnavailableReason", () => {
it("prefers active disabledReason when profiles are disabled", () => { const now = Date.now(); const store = makeStore({ "anthropic:default": {
disabledUntil: now + 60_000,
disabledReason: "billing",
},
});
it("falls back to unknown when active cooldown has no reason history", () => { const now = Date.now(); const store = makeStore({ "anthropic:default": {
cooldownUntil: now + 60_000,
},
});
it("ignores expired windows and returns null when no profile is actively unavailable", () => { const now = Date.now(); const store = makeStore({ "anthropic:default": {
cooldownUntil: now - 1_000,
failureCounts: { auth: 5 },
}, "anthropic:backup": {
disabledUntil: now - 500,
disabledReason: "billing",
},
});
const stats = store.usageStats?.["anthropic:default"]; // cooldownUntil cleared
expect(stats?.cooldownUntil).toBeUndefined(); // disabledUntil still active — not touched
expect(stats?.disabledUntil).toBe(future);
expect(stats?.disabledReason).toBe("billing"); // errorCount NOT reset because profile still has an active unusable window
expect(stats?.errorCount).toBe(5);
expect(stats?.failureCounts).toEqual({ rate_limit: 3, billing: 2 });
});
const stats = store.usageStats?.["anthropic:default"];
expect(stats?.cooldownUntil).toBe(future);
expect(stats?.disabledUntil).toBeUndefined();
expect(stats?.disabledReason).toBeUndefined(); // errorCount NOT reset because cooldown is still active
expect(stats?.errorCount).toBe(3);
});
it("resets errorCount only when both cooldown and disabled have expired", () => { const store = makeStore({ "anthropic:default": {
cooldownUntil: Date.now() - 2_000,
disabledUntil: Date.now() - 1_000,
disabledReason: "billing",
errorCount: 4,
failureCounts: { rate_limit: 2, billing: 2 },
},
});
// OpenAI: still active → untouched
expect(store.usageStats?.["openai:default"]?.cooldownUntil).toBeGreaterThan(Date.now());
expect(store.usageStats?.["openai:default"]?.errorCount).toBe(2);
});
it("accepts an explicit `now` timestamp for deterministic testing", () => { const fixedNow = 1_700_000_000_000; const store = makeStore({ "anthropic:default": {
cooldownUntil: fixedNow - 1,
errorCount: 2,
},
});
// ts >= cooldownUntil → should clear (cooldown "until" means the instant // at cooldownUntil the profile becomes available again).
expect(clearExpiredCooldowns(store, fixedNow)).toBe(true);
expect(store.usageStats?.["anthropic:default"]?.cooldownUntil).toBeUndefined();
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(0);
});
it("ignores NaN and Infinity cooldown values", () => { const store = makeStore({ "anthropic:default": {
cooldownUntil: Number.NaN,
errorCount: 2,
}, "openai:default": {
cooldownUntil: Infinity,
errorCount: 3,
},
});
describe("markAuthProfileFailure — active windows do not extend on retry", () => { // Regression for https://github.com/openclaw/openclaw/issues/23516 // When all providers are at saturation backoff (60 min) and retries fire every 30 min, // each retry was resetting cooldownUntil to now+60m, preventing recovery.
type WindowStats = ProfileUsageStats;
// When a cooldown/disabled window expires, the error count resets to prevent // stale counters from escalating the next cooldown (the root cause of // infinite cooldown loops — see #40989). The next failure should compute // backoff from errorCount=1, not from the accumulated stale count. const expiredWindowCases = [
{
label: "cooldownUntil",
reason: "rate_limit" as const,
buildUsageStats: (now: number): WindowStats => ({
cooldownUntil: now - 60_000,
errorCount: 3,
lastFailureAt: now - 60_000,
}), // errorCount resets → calculateAuthProfileCooldownMs(1) = 30_000 (stepped: 30s → 1m → 5m)
expectedUntil: (now: number) => now + 30_000,
readUntil: (stats: WindowStats | undefined) => stats?.cooldownUntil,
},
{
label: "disabledUntil",
reason: "billing" as const,
buildUsageStats: (now: number): WindowStats => ({
disabledUntil: now - 60_000,
disabledReason: "billing",
errorCount: 5,
failureCounts: { billing: 2 },
lastFailureAt: now - 60_000,
}), // errorCount resets, billing count resets to 1 → // calculateDisabledLaneBackoffMs(1, 5h, 24h) = 5h
expectedUntil: (now: number) => now + 5 * 60 * 60 * 1000,
readUntil: (stats: WindowStats | undefined) => stats?.disabledUntil,
},
{
label: "disabledUntil(auth_permanent)",
reason: "auth_permanent" as const,
buildUsageStats: (now: number): WindowStats => ({
disabledUntil: now - 60_000,
disabledReason: "auth_permanent",
errorCount: 5,
failureCounts: { auth_permanent: 2 },
lastFailureAt: now - 60_000,
}), // errorCount resets, auth_permanent count resets to 1 → // calculateDisabledLaneBackoffMs(1, 10m, 60m) = 10m
expectedUntil: (now: number) => now + 10 * 60 * 1000,
readUntil: (stats: WindowStats | undefined) => stats?.disabledUntil,
},
];
for (const testCase of expiredWindowCases) {
it(`recomputes ${testCase.label} after the previous window expires`, async () => { const now = 1_000_000; const store = makeStore({ "anthropic:default": testCase.buildUsageStats(now),
});
it("falls back to a 30s cooldown when the WHAM probe fails", async () => { const now = 1_700_000_000_000; const store = makeStore({});
fetchMock.mockRejectedValueOnce(new Error("network unavailable"));
it("records cooldownModel on first rate_limit failure", async () => { const now = 1_000_000; const store = makeStoreWithCopilot({});
await markFailure({ store, now, modelId: "claude-sonnet-4.6" }); const stats = store.usageStats?.["github-copilot:github"];
expect(stats?.cooldownReason).toBe("rate_limit");
expect(stats?.cooldownModel).toBe("claude-sonnet-4.6");
});
it("widens cooldownModel to undefined when a different model fails during active cooldown", async () => { const now = 1_000_000; const store = makeStoreWithCopilot({ "github-copilot:github": {
cooldownUntil: now + 30_000,
cooldownReason: "rate_limit",
cooldownModel: "claude-sonnet-4.6",
errorCount: 1,
lastFailureAt: now - 1000,
},
}); // Different model fails during active cooldown
await markFailure({ store, now, modelId: "gpt-4.1" }); const stats = store.usageStats?.["github-copilot:github"]; // Scope widened to all models
expect(stats?.cooldownModel).toBeUndefined();
expect(stats?.cooldownReason).toBe("rate_limit");
});
it("preserves cooldownModel when the same model fails again during active cooldown", async () => { const now = 1_000_000; const store = makeStoreWithCopilot({ "github-copilot:github": {
cooldownUntil: now + 30_000,
cooldownReason: "rate_limit",
cooldownModel: "claude-sonnet-4.6",
errorCount: 1,
lastFailureAt: now - 1000,
},
});
await markFailure({ store, now, modelId: "claude-sonnet-4.6" }); const stats = store.usageStats?.["github-copilot:github"];
expect(stats?.cooldownModel).toBe("claude-sonnet-4.6");
});
it("widens cooldownModel when rate_limit failure during active cooldown has no modelId", async () => { const now = 1_000_000; const store = makeStoreWithCopilot({ "github-copilot:github": {
cooldownUntil: now + 30_000,
cooldownReason: "rate_limit",
cooldownModel: "claude-sonnet-4.6",
errorCount: 1,
lastFailureAt: now - 1000,
},
});
await markFailure({ store, now, modelId: undefined }); const stats = store.usageStats?.["github-copilot:github"];
expect(stats?.cooldownReason).toBe("rate_limit");
expect(stats?.cooldownModel).toBeUndefined();
});
it("updates cooldownReason when auth failure occurs during active rate_limit window", async () => { const now = 1_000_000; const store = makeStoreWithCopilot({ "github-copilot:github": {
cooldownUntil: now + 30_000,
cooldownReason: "rate_limit",
cooldownModel: "claude-sonnet-4.6",
errorCount: 1,
lastFailureAt: now - 1000,
},
});
await markAuthProfileFailure({
store,
profileId: "github-copilot:github",
reason: "auth",
modelId: "claude-opus-4.6",
}); const stats = store.usageStats?.["github-copilot:github"]; // Reason should update to the new failure type, not stay as rate_limit
expect(stats?.cooldownReason).toBe("auth"); // Model scope should be cleared — auth failures are profile-wide
expect(stats?.cooldownModel).toBeUndefined();
});
it("clears cooldownModel when non-rate_limit failure hits same model during active window", async () => { const now = 1_000_000; const store = makeStoreWithCopilot({ "github-copilot:github": {
cooldownUntil: now + 30_000,
cooldownReason: "rate_limit",
cooldownModel: "claude-sonnet-4.6",
errorCount: 1,
lastFailureAt: now - 1000,
},
});
await markAuthProfileFailure({
store,
profileId: "github-copilot:github",
reason: "auth",
modelId: "claude-sonnet-4.6",
}); const stats = store.usageStats?.["github-copilot:github"]; // Even same-model auth failure should clear model scope (auth is profile-wide)
expect(stats?.cooldownReason).toBe("auth");
expect(stats?.cooldownModel).toBeUndefined();
});
});
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-10)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.