import { describe, expect, it } from "vitest"; import { type AuthProfileStore, calculateAuthProfileCooldownMs, resolveAuthProfileOrder, } from "./auth-profiles.js"; describe("resolveAuthProfileOrder", () => { const store: AuthProfileStore = { version: 1, profiles: { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-default", }, "anthropic:work": { type: "api_key", provider: "anthropic", key: "sk-work", }, }, }; const cfg = { auth: { profiles: { "anthropic:default": { provider: "anthropic", mode: "api_key" }, "anthropic:work": { provider: "anthropic", mode: "api_key" }, }, }, }; it("uses stored profiles when no config exists", () => { const order = resolveAuthProfileOrder({ store, provider: "anthropic", }); expect(order).toEqual(["anthropic:default", "anthropic:work"]); }); it("prioritizes preferred profiles", () => { const order = resolveAuthProfileOrder({ cfg, store, provider: "anthropic", preferredProfile: "anthropic:work", }); expect(order[0]).toBe("anthropic:work"); expect(order).toContain("anthropic:default"); }); it("prioritizes last-good profile when no preferred override", () => { const order = resolveAuthProfileOrder({ cfg, store: { ...store, lastGood: { anthropic: "anthropic:work" } }, provider: "anthropic", }); expect(order[0]).toBe("anthropic:work"); }); it("uses explicit profiles when order is missing", () => { const order = resolveAuthProfileOrder({ cfg, store, provider: "anthropic", }); expect(order).toEqual(["anthropic:default", "anthropic:work"]); }); it("uses configured order when provided", () => { const order = resolveAuthProfileOrder({ cfg: { auth: { order: { anthropic: ["anthropic:work", "anthropic:default"] }, profiles: cfg.auth.profiles, }, }, store, provider: "anthropic", }); expect(order).toEqual(["anthropic:work", "anthropic:default"]); }); it("prioritizes oauth profiles when order missing", () => { const mixedStore: AuthProfileStore = { version: 1, profiles: { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-default", }, "anthropic:oauth": { type: "oauth", provider: "anthropic", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, }, }, }; const order = resolveAuthProfileOrder({ store: mixedStore, provider: "anthropic", }); expect(order).toEqual(["anthropic:oauth", "anthropic:default"]); }); it("orders by lastUsed when no explicit order exists", () => { const order = resolveAuthProfileOrder({ store: { version: 1, profiles: { "anthropic:a": { type: "oauth", provider: "anthropic", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, }, "anthropic:b": { type: "api_key", provider: "anthropic", key: "sk-b", }, "anthropic:c": { type: "api_key", provider: "anthropic", key: "sk-c", }, }, usageStats: { "anthropic:a": { lastUsed: 200 }, "anthropic:b": { lastUsed: 100 }, "anthropic:c": { lastUsed: 300 }, }, }, provider: "anthropic", }); expect(order).toEqual(["anthropic:b", "anthropic:a", "anthropic:c"]); }); it("pushes cooldown profiles to the end, ordered by cooldown expiry", () => { const now = Date.now(); const order = resolveAuthProfileOrder({ store: { version: 1, profiles: { "anthropic:ready": { type: "api_key", provider: "anthropic", key: "sk-ready", }, "anthropic:cool1": { type: "oauth", provider: "anthropic", access: "access-token", refresh: "refresh-token", expires: now + 60_000, }, "anthropic:cool2": { type: "api_key", provider: "anthropic", key: "sk-cool", }, }, usageStats: { "anthropic:ready": { lastUsed: 50 }, "anthropic:cool1": { cooldownUntil: now + 5_000 }, "anthropic:cool2": { cooldownUntil: now + 1_000 }, }, }, provider: "anthropic", }); expect(order).toEqual([ "anthropic:ready", "anthropic:cool2", "anthropic:cool1", ]); }); }); describe("auth profile cooldowns", () => { it("applies exponential backoff with a 1h cap", () => { expect(calculateAuthProfileCooldownMs(1)).toBe(60_000); expect(calculateAuthProfileCooldownMs(2)).toBe(5 * 60_000); expect(calculateAuthProfileCooldownMs(3)).toBe(25 * 60_000); expect(calculateAuthProfileCooldownMs(4)).toBe(60 * 60_000); expect(calculateAuthProfileCooldownMs(5)).toBe(60 * 60_000); }); });