Files
openclaw/src/agents/auth-profiles.cooldown-auto-expiry.test.ts
2026-02-18 01:34:35 +00:00

160 lines
5.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { resolveAuthProfileOrder } from "./auth-profiles/order.js";
import type { AuthProfileStore } from "./auth-profiles/types.js";
import { isProfileInCooldown } from "./auth-profiles/usage.js";
/**
* Integration tests for cooldown auto-expiry through resolveAuthProfileOrder.
* Verifies that profiles with expired cooldowns are treated as available and
* have their error state reset, preventing the escalation loop described in
* #3604, #13623, #15851, and #11972.
*/
function makeStoreWithProfiles(): AuthProfileStore {
return {
version: 1,
profiles: {
"anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-1" },
"anthropic:secondary": { type: "api_key", provider: "anthropic", key: "sk-2" },
"openai:default": { type: "api_key", provider: "openai", key: "sk-oi" },
},
usageStats: {},
};
}
describe("resolveAuthProfileOrder — cooldown auto-expiry", () => {
it("places profile with expired cooldown in available list (round-robin path)", () => {
const store = makeStoreWithProfiles();
store.usageStats = {
"anthropic:default": {
cooldownUntil: Date.now() - 10_000,
errorCount: 4,
failureCounts: { rate_limit: 4 },
lastFailureAt: Date.now() - 70_000,
},
};
const order = resolveAuthProfileOrder({ store, provider: "anthropic" });
// Profile should be in the result (available, not skipped)
expect(order).toContain("anthropic:default");
// Should no longer report as in cooldown
expect(isProfileInCooldown(store, "anthropic:default")).toBe(false);
// Error state should have been reset
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(0);
expect(store.usageStats?.["anthropic:default"]?.cooldownUntil).toBeUndefined();
});
it("places profile with expired cooldown in available list (explicit-order path)", () => {
const store = makeStoreWithProfiles();
store.order = { anthropic: ["anthropic:secondary", "anthropic:default"] };
store.usageStats = {
"anthropic:default": {
cooldownUntil: Date.now() - 5_000,
errorCount: 3,
},
};
const order = resolveAuthProfileOrder({ store, provider: "anthropic" });
// Both profiles available — explicit order respected
expect(order[0]).toBe("anthropic:secondary");
expect(order).toContain("anthropic:default");
// Expired cooldown cleared
expect(store.usageStats?.["anthropic:default"]?.cooldownUntil).toBeUndefined();
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(0);
});
it("keeps profile with active cooldown in cooldown list", () => {
const futureMs = Date.now() + 300_000;
const store = makeStoreWithProfiles();
store.usageStats = {
"anthropic:default": {
cooldownUntil: futureMs,
errorCount: 3,
},
};
const order = resolveAuthProfileOrder({ store, provider: "anthropic" });
// Profile is still in the result (appended after available profiles)
expect(order).toContain("anthropic:default");
// Should still be in cooldown
expect(isProfileInCooldown(store, "anthropic:default")).toBe(true);
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(3);
});
it("expired cooldown resets error count — prevents escalation on next failure", () => {
const store = makeStoreWithProfiles();
store.usageStats = {
"anthropic:default": {
cooldownUntil: Date.now() - 1_000,
errorCount: 4, // Would cause 1-hour cooldown on next failure
failureCounts: { rate_limit: 4 },
lastFailureAt: Date.now() - 3_700_000,
},
};
resolveAuthProfileOrder({ store, provider: "anthropic" });
// After clearing, errorCount is 0. If the profile fails again,
// the next cooldown will be 60 seconds (errorCount 1) instead of
// 1 hour (errorCount 5). This is the core fix for #3604.
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(0);
expect(store.usageStats?.["anthropic:default"]?.failureCounts).toBeUndefined();
});
it("mixed active and expired cooldowns across profiles", () => {
const store = makeStoreWithProfiles();
store.usageStats = {
"anthropic:default": {
cooldownUntil: Date.now() - 1_000,
errorCount: 3,
},
"anthropic:secondary": {
cooldownUntil: Date.now() + 300_000,
errorCount: 2,
},
};
const order = resolveAuthProfileOrder({ store, provider: "anthropic" });
// anthropic:default should be available (expired, cleared)
expect(store.usageStats?.["anthropic:default"]?.cooldownUntil).toBeUndefined();
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(0);
// anthropic:secondary should still be in cooldown
expect(store.usageStats?.["anthropic:secondary"]?.cooldownUntil).toBeGreaterThan(Date.now());
expect(store.usageStats?.["anthropic:secondary"]?.errorCount).toBe(2);
// Available profile should come first
expect(order[0]).toBe("anthropic:default");
});
it("does not affect profiles from other providers", () => {
const store = makeStoreWithProfiles();
store.usageStats = {
"anthropic:default": {
cooldownUntil: Date.now() - 1_000,
errorCount: 4,
},
"openai:default": {
cooldownUntil: Date.now() - 1_000,
errorCount: 3,
},
};
// Resolve only anthropic
resolveAuthProfileOrder({ store, provider: "anthropic" });
// Both should be cleared since clearExpiredCooldowns sweeps all profiles
// in the store — this is intentional for correctness.
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(0);
expect(store.usageStats?.["openai:default"]?.errorCount).toBe(0);
});
});