diff --git a/src/agents/auth-profiles.auth-profile-cooldowns.test.ts b/src/agents/auth-profiles.auth-profile-cooldowns.test.ts index e63910250..01f0722f5 100644 --- a/src/agents/auth-profiles.auth-profile-cooldowns.test.ts +++ b/src/agents/auth-profiles.auth-profile-cooldowns.test.ts @@ -20,7 +20,6 @@ import { clearAuthProfileCooldown, cooldownKey, isProfileInCooldown, - markAuthProfileCooldown, markAuthProfileFailure, markAuthProfileUsed, saveAuthProfileStore, @@ -28,6 +27,24 @@ import { import type { AuthProfileStore } from "./auth-profiles.js"; import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js"; +// Test helpers +const makeStore = (usageStats?: AuthProfileStore["usageStats"]): AuthProfileStore => ({ + version: AUTH_STORE_VERSION, + profiles: { + "openai:default": { type: "api_key", provider: "openai", key: "test" }, + }, + ...(usageStats && { usageStats }), +}); + +async function withTempDir(fn: (tempDir: string) => Promise): Promise { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); + try { + return await fn(tempDir); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } +} + describe("auth profile cooldowns", () => { it("applies exponential backoff with a 1h cap", () => { expect(calculateAuthProfileCooldownMs(1)).toBe(60_000); @@ -39,9 +56,11 @@ describe("auth profile cooldowns", () => { }); describe("cooldownKey", () => { - it("returns profileId when model is not provided", () => { + it("returns profileId when model is not provided or empty", () => { expect(cooldownKey("openai:default")).toBe("openai:default"); expect(cooldownKey("openai:default", undefined)).toBe("openai:default"); + expect(cooldownKey("openai:default", "")).toBe("openai:default"); + expect(cooldownKey("openai:default", " ")).toBe("openai:default"); }); it("returns composite key when model is provided", () => { @@ -52,44 +71,20 @@ describe("cooldownKey", () => { describe("isProfileInCooldown with per-model support", () => { it("returns false when no cooldown exists", () => { - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - }; + const store = makeStore(); expect(isProfileInCooldown(store, "openai:default")).toBe(false); expect(isProfileInCooldown(store, "openai:default", "gpt-4")).toBe(false); }); it("checks profile-level cooldown when model not provided", () => { - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { - "openai:default": { cooldownUntil: Date.now() + 60_000 }, - }, - }; + const store = makeStore({ "openai:default": { cooldownUntil: Date.now() + 60_000 } }); expect(isProfileInCooldown(store, "openai:default")).toBe(true); }); it("checks per-model cooldown when model is provided", () => { - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { - "openai:default:gpt-4": { cooldownUntil: Date.now() + 60_000 }, - }, - }; - // model-specific cooldown exists + const store = makeStore({ "openai:default:gpt-4": { cooldownUntil: Date.now() + 60_000 } }); expect(isProfileInCooldown(store, "openai:default", "gpt-4")).toBe(true); - // different model is not in cooldown expect(isProfileInCooldown(store, "openai:default", "gpt-3.5")).toBe(false); - // profile-level is not in cooldown expect(isProfileInCooldown(store, "openai:default")).toBe(false); }); @@ -97,55 +92,31 @@ describe("isProfileInCooldown with per-model support", () => { const store: AuthProfileStore = { version: AUTH_STORE_VERSION, profiles: { - "github-copilot:default": { - type: "api_key", - provider: "github-copilot", - key: "test", - }, - }, - usageStats: { - // gpt-5.2 is in cooldown (rate limited) - "github-copilot:default:gpt-5.2": { cooldownUntil: Date.now() + 60_000 }, - // gpt-5-mini has no cooldown (unlimited quota) + "github-copilot:default": { type: "api_key", provider: "github-copilot", key: "test" }, }, + usageStats: { "github-copilot:default:gpt-5.2": { cooldownUntil: Date.now() + 60_000 } }, }; expect(isProfileInCooldown(store, "github-copilot:default", "gpt-5.2")).toBe(true); expect(isProfileInCooldown(store, "github-copilot:default", "gpt-5-mini")).toBe(false); }); it("returns false when cooldown has expired", () => { - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { - "openai:default:gpt-4": { cooldownUntil: Date.now() - 1000 }, // expired - }, - }; + const store = makeStore({ "openai:default:gpt-4": { cooldownUntil: Date.now() - 1000 } }); expect(isProfileInCooldown(store, "openai:default", "gpt-4")).toBe(false); }); }); describe("markAuthProfileUsed with per-model support", () => { it("clears per-model cooldown when model is provided", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); - const cooldownTime = Date.now() + 60_000; - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { + await withTempDir(async (tempDir) => { + const cooldownTime = Date.now() + 60_000; + const store = makeStore({ "openai:default": { cooldownUntil: cooldownTime }, "openai:default:gpt-4": { cooldownUntil: cooldownTime, errorCount: 3 }, "openai:default:gpt-3.5": { cooldownUntil: cooldownTime }, - }, - }; - saveAuthProfileStore(store, tempDir); + }); + saveAuthProfileStore(store, tempDir); - try { - // Mark gpt-4 as used (successful) await markAuthProfileUsed({ store, profileId: "openai:default", @@ -153,84 +124,41 @@ describe("markAuthProfileUsed with per-model support", () => { agentDir: tempDir, }); - // Profile-level cooldown should be cleared expect(store.usageStats?.["openai:default"]?.cooldownUntil).toBeUndefined(); - // Per-model cooldown for gpt-4 should be cleared expect(store.usageStats?.["openai:default:gpt-4"]?.cooldownUntil).toBeUndefined(); expect(store.usageStats?.["openai:default:gpt-4"]?.errorCount).toBe(0); - // Per-model cooldown for gpt-3.5 should remain (different model) expect(store.usageStats?.["openai:default:gpt-3.5"]?.cooldownUntil).toBe(cooldownTime); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + }); }); it("only clears profile-level cooldown when model is not provided", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); - const cooldownTime = Date.now() + 60_000; - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { + await withTempDir(async (tempDir) => { + const cooldownTime = Date.now() + 60_000; + const store = makeStore({ "openai:default": { cooldownUntil: cooldownTime }, "openai:default:gpt-4": { cooldownUntil: cooldownTime }, - }, - }; - saveAuthProfileStore(store, tempDir); + }); + saveAuthProfileStore(store, tempDir); - try { - // Mark profile as used without specifying model await markAuthProfileUsed({ store, profileId: "openai:default", agentDir: tempDir }); - // Profile-level cooldown should be cleared expect(store.usageStats?.["openai:default"]?.cooldownUntil).toBeUndefined(); - // Per-model cooldown should remain (no model specified) expect(store.usageStats?.["openai:default:gpt-4"]?.cooldownUntil).toBe(cooldownTime); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } - }); -}); - -describe("cooldownKey edge cases", () => { - it("treats empty string model the same as undefined", () => { - // Empty string should be treated as "no model" to avoid trailing colon - expect(cooldownKey("openai:default", "")).toBe("openai:default"); - expect(cooldownKey("openai:default", " ")).toBe("openai:default"); + }); }); }); describe("isProfileInCooldown backward compatibility", () => { it("returns true for any model when profile-level cooldown exists", () => { - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { - "openai:default": { cooldownUntil: Date.now() + 60_000 }, // profile-level only - }, - }; - // Any model should be blocked when profile-level cooldown exists + const store = makeStore({ "openai:default": { cooldownUntil: Date.now() + 60_000 } }); expect(isProfileInCooldown(store, "openai:default", "gpt-4")).toBe(true); expect(isProfileInCooldown(store, "openai:default", "gpt-3.5")).toBe(true); expect(isProfileInCooldown(store, "openai:default", "o1-preview")).toBe(true); - // Profile-level check also works expect(isProfileInCooldown(store, "openai:default")).toBe(true); }); it("checks disabledUntil for per-model cooldowns (billing failures)", () => { - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { - "openai:default:gpt-4": { disabledUntil: Date.now() + 60_000 }, // billing failure - }, - }; + const store = makeStore({ "openai:default:gpt-4": { disabledUntil: Date.now() + 60_000 } }); expect(isProfileInCooldown(store, "openai:default", "gpt-4")).toBe(true); expect(isProfileInCooldown(store, "openai:default", "gpt-3.5")).toBe(false); }); @@ -238,16 +166,10 @@ describe("isProfileInCooldown backward compatibility", () => { describe("markAuthProfileFailure with per-model support", () => { it("tracks failure per model when model is provided", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - }; - saveAuthProfileStore(store, tempDir); + await withTempDir(async (tempDir) => { + const store = makeStore(); + saveAuthProfileStore(store, tempDir); - try { await markAuthProfileFailure({ store, profileId: "openai:default", @@ -256,29 +178,18 @@ describe("markAuthProfileFailure with per-model support", () => { agentDir: tempDir, }); - // Per-model key should have cooldown expect(store.usageStats?.["openai:default:gpt-4"]?.cooldownUntil).toBeGreaterThan(Date.now()); expect(store.usageStats?.["openai:default:gpt-4"]?.errorCount).toBe(1); - // Profile-level should NOT have cooldown (only model-specific) expect(store.usageStats?.["openai:default"]?.cooldownUntil).toBeUndefined(); - // Other models should not be affected expect(store.usageStats?.["openai:default:gpt-3.5"]).toBeUndefined(); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + }); }); it("tracks failure at profile level when model is not provided", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - }; - saveAuthProfileStore(store, tempDir); + await withTempDir(async (tempDir) => { + const store = makeStore(); + saveAuthProfileStore(store, tempDir); - try { await markAuthProfileFailure({ store, profileId: "openai:default", @@ -286,25 +197,16 @@ describe("markAuthProfileFailure with per-model support", () => { agentDir: tempDir, }); - // Profile-level key should have cooldown expect(store.usageStats?.["openai:default"]?.cooldownUntil).toBeGreaterThan(Date.now()); expect(store.usageStats?.["openai:default"]?.errorCount).toBe(1); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + }); }); it("tracks billing failures with disabledUntil per model", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - }; - saveAuthProfileStore(store, tempDir); + await withTempDir(async (tempDir) => { + const store = makeStore(); + saveAuthProfileStore(store, tempDir); - try { await markAuthProfileFailure({ store, profileId: "openai:default", @@ -313,62 +215,23 @@ describe("markAuthProfileFailure with per-model support", () => { agentDir: tempDir, }); - // Billing failures use disabledUntil instead of cooldownUntil expect(store.usageStats?.["openai:default:gpt-4"]?.disabledUntil).toBeGreaterThan(Date.now()); expect(store.usageStats?.["openai:default:gpt-4"]?.disabledReason).toBe("billing"); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } - }); -}); - -describe("markAuthProfileCooldown with per-model support", () => { - it("marks cooldown per model when model is provided", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - }; - saveAuthProfileStore(store, tempDir); - - try { - await markAuthProfileCooldown({ - store, - profileId: "openai:default", - model: "gpt-4", - agentDir: tempDir, - }); - - // Per-model key should have cooldown - expect(store.usageStats?.["openai:default:gpt-4"]?.cooldownUntil).toBeGreaterThan(Date.now()); - // Profile-level should NOT have cooldown - expect(store.usageStats?.["openai:default"]?.cooldownUntil).toBeUndefined(); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + }); }); }); describe("clearAuthProfileCooldown with per-model support", () => { it("clears per-model cooldown when model is provided", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); - const cooldownTime = Date.now() + 60_000; - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { + await withTempDir(async (tempDir) => { + const cooldownTime = Date.now() + 60_000; + const store = makeStore({ "openai:default": { cooldownUntil: cooldownTime }, "openai:default:gpt-4": { cooldownUntil: cooldownTime, errorCount: 3 }, "openai:default:gpt-3.5": { cooldownUntil: cooldownTime }, - }, - }; - saveAuthProfileStore(store, tempDir); + }); + saveAuthProfileStore(store, tempDir); - try { await clearAuthProfileCooldown({ store, profileId: "openai:default", @@ -376,47 +239,27 @@ describe("clearAuthProfileCooldown with per-model support", () => { agentDir: tempDir, }); - // Per-model cooldown for gpt-4 should be cleared expect(store.usageStats?.["openai:default:gpt-4"]?.cooldownUntil).toBeUndefined(); expect(store.usageStats?.["openai:default:gpt-4"]?.errorCount).toBe(0); - // Profile-level cooldown should remain (different key) expect(store.usageStats?.["openai:default"]?.cooldownUntil).toBe(cooldownTime); - // Other model cooldown should remain expect(store.usageStats?.["openai:default:gpt-3.5"]?.cooldownUntil).toBe(cooldownTime); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + }); }); it("clears profile-level cooldown when model is not provided", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-")); - const cooldownTime = Date.now() + 60_000; - const store: AuthProfileStore = { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { type: "api_key", provider: "openai", key: "test" }, - }, - usageStats: { + await withTempDir(async (tempDir) => { + const cooldownTime = Date.now() + 60_000; + const store = makeStore({ "openai:default": { cooldownUntil: cooldownTime, errorCount: 2 }, "openai:default:gpt-4": { cooldownUntil: cooldownTime }, - }, - }; - saveAuthProfileStore(store, tempDir); - - try { - await clearAuthProfileCooldown({ - store, - profileId: "openai:default", - agentDir: tempDir, }); + saveAuthProfileStore(store, tempDir); + + await clearAuthProfileCooldown({ store, profileId: "openai:default", agentDir: tempDir }); - // Profile-level cooldown should be cleared expect(store.usageStats?.["openai:default"]?.cooldownUntil).toBeUndefined(); expect(store.usageStats?.["openai:default"]?.errorCount).toBe(0); - // Per-model cooldown should remain (different key) expect(store.usageStats?.["openai:default:gpt-4"]?.cooldownUntil).toBe(cooldownTime); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + }); }); });