Spaces:
Paused
Paused
| /** | |
| * Tests that account-pool correctly routes requests based on model→plan mapping. | |
| * | |
| * Verifies the critical path: when a model is available to both free and team, | |
| * free accounts should be selected. When only team has it, free accounts must | |
| * NOT be used (return null instead of wrong account). | |
| */ | |
| import { describe, it, expect, vi, beforeEach } from "vitest"; | |
| // Mock getModelPlanTypes to control plan routing | |
| const mockGetModelPlanTypes = vi.fn<(id: string) => string[]>(() => []); | |
| vi.mock("../../models/model-store.js", () => ({ | |
| getModelPlanTypes: (...args: unknown[]) => mockGetModelPlanTypes(args[0] as string), | |
| })); | |
| vi.mock("../../config.js", () => ({ | |
| getConfig: vi.fn(() => ({ | |
| server: { account_strategy: "round_robin" }, | |
| auth: { jwt_token: "" }, | |
| })), | |
| })); | |
| // Control planType by returning it from extractUserProfile | |
| let profileForToken: Record<string, { chatgpt_plan_type: string; email: string }> = {}; | |
| vi.mock("../../auth/jwt-utils.js", () => ({ | |
| isTokenExpired: vi.fn(() => false), | |
| decodeJwtPayload: vi.fn(() => ({})), | |
| extractChatGptAccountId: vi.fn((token: string) => `aid-${token}`), | |
| extractUserProfile: vi.fn((token: string) => profileForToken[token] ?? null), | |
| })); | |
| vi.mock("../../utils/jitter.js", () => ({ | |
| jitter: vi.fn((val: number) => val), | |
| })); | |
| vi.mock("fs", () => ({ | |
| readFileSync: vi.fn(() => JSON.stringify({ accounts: [] })), | |
| writeFileSync: vi.fn(), | |
| existsSync: vi.fn(() => false), | |
| mkdirSync: vi.fn(), | |
| })); | |
| import { AccountPool } from "../account-pool.js"; | |
| function createPool(...accounts: Array<{ token: string; planType: string; email: string }>) { | |
| // Set up profile mocks before creating pool | |
| profileForToken = {}; | |
| for (const a of accounts) { | |
| profileForToken[a.token] = { chatgpt_plan_type: a.planType, email: a.email }; | |
| } | |
| const pool = new AccountPool(); | |
| for (const a of accounts) { | |
| pool.addAccount(a.token); | |
| } | |
| return pool; | |
| } | |
| describe("account-pool plan-based routing", () => { | |
| beforeEach(() => { | |
| vi.clearAllMocks(); | |
| profileForToken = {}; | |
| }); | |
| it("returns null when model only supports team but only free accounts exist", () => { | |
| mockGetModelPlanTypes.mockReturnValue(["team"]); | |
| const pool = createPool( | |
| { token: "tok-free", planType: "free", email: "free@test.com" }, | |
| ); | |
| const acquired = pool.acquire({ model: "gpt-5.4" }); | |
| expect(acquired).toBeNull(); | |
| }); | |
| it("acquires team account when model only supports team", () => { | |
| mockGetModelPlanTypes.mockReturnValue(["team"]); | |
| const pool = createPool( | |
| { token: "tok-free", planType: "free", email: "free@test.com" }, | |
| { token: "tok-team", planType: "team", email: "team@test.com" }, | |
| ); | |
| const acquired = pool.acquire({ model: "gpt-5.4" }); | |
| expect(acquired).not.toBeNull(); | |
| expect(acquired!.token).toBe("tok-team"); | |
| }); | |
| it("uses any account when model has no known plan requirements", () => { | |
| mockGetModelPlanTypes.mockReturnValue([]); | |
| const pool = createPool( | |
| { token: "tok-free", planType: "free", email: "free@test.com" }, | |
| ); | |
| const acquired = pool.acquire({ model: "unknown-model" }); | |
| expect(acquired).not.toBeNull(); | |
| }); | |
| it("after plan map update, free account can access previously team-only model", () => { | |
| const pool = createPool( | |
| { token: "tok-free", planType: "free", email: "free@test.com" }, | |
| ); | |
| // Initially: gpt-5.4 only for team | |
| mockGetModelPlanTypes.mockReturnValue(["team"]); | |
| const before = pool.acquire({ model: "gpt-5.4" }); | |
| expect(before).toBeNull(); // blocked | |
| // Backend updates: gpt-5.4 now available for free too | |
| mockGetModelPlanTypes.mockReturnValue(["free", "team"]); | |
| const after = pool.acquire({ model: "gpt-5.4" }); | |
| expect(after).not.toBeNull(); | |
| expect(after!.token).toBe("tok-free"); | |
| }); | |
| it("prefers plan-matched accounts over others", () => { | |
| mockGetModelPlanTypes.mockReturnValue(["team"]); | |
| const pool = createPool( | |
| { token: "tok-free1", planType: "free", email: "free1@test.com" }, | |
| { token: "tok-free2", planType: "free", email: "free2@test.com" }, | |
| { token: "tok-team", planType: "team", email: "team@test.com" }, | |
| ); | |
| const acquired = pool.acquire({ model: "gpt-5.4" }); | |
| expect(acquired).not.toBeNull(); | |
| expect(acquired!.token).toBe("tok-team"); | |
| }); | |
| it("acquires free account when model supports both free and team", () => { | |
| mockGetModelPlanTypes.mockReturnValue(["free", "team"]); | |
| const pool = createPool( | |
| { token: "tok-free", planType: "free", email: "free@test.com" }, | |
| { token: "tok-team", planType: "team", email: "team@test.com" }, | |
| ); | |
| const acquired = pool.acquire({ model: "gpt-5.4" }); | |
| expect(acquired).not.toBeNull(); | |
| // Both are valid candidates, should get one of them | |
| expect(["tok-free", "tok-team"]).toContain(acquired!.token); | |
| }); | |
| }); | |