codex-proxy / src /auth /__tests__ /plan-routing-acquire.test.ts
icebear0828
test: add plan-based model routing tests (14 cases)
9c1c54e
raw
history blame
4.89 kB
/**
* 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);
});
});