mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 23:10:29 +00:00
160 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
});
|