Spaces:
Paused
Paused
File size: 7,552 Bytes
9c1c54e 7516302 9c1c54e 7516302 9c1c54e 7516302 9c1c54e 7516302 9c1c54e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | /**
* 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 and isPlanFetched to control plan routing
const mockGetModelPlanTypes = vi.fn<(id: string) => string[]>(() => []);
const mockIsPlanFetched = vi.fn<(planType: string) => boolean>(() => true);
vi.mock("../../models/model-store.js", () => ({
getModelPlanTypes: (...args: unknown[]) => mockGetModelPlanTypes(args[0] as string),
isPlanFetched: (...args: unknown[]) => mockIsPlanFetched(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);
});
it("includes accounts whose plan was never fetched (unfetched = possibly compatible)", () => {
// Model known to work on team, but free plan was never fetched
mockGetModelPlanTypes.mockReturnValue(["team"]);
mockIsPlanFetched.mockImplementation((plan: string) => plan === "team");
const pool = createPool(
{ token: "tok-free", planType: "free", email: "free@test.com" },
{ token: "tok-team", planType: "team", email: "team@test.com" },
);
// Both accounts are candidates (team is known, free is unfetched → possibly compatible)
const first = pool.acquire({ model: "gpt-5.4" });
expect(first).not.toBeNull();
// Second concurrent request should also succeed (two candidates available)
const second = pool.acquire({ model: "gpt-5.4" });
expect(second).not.toBeNull();
expect(second!.token).not.toBe(first!.token);
// Both tokens should be from our pool
const tokens = new Set([first!.token, second!.token]);
expect(tokens.has("tok-free")).toBe(true);
expect(tokens.has("tok-team")).toBe(true);
});
it("excludes accounts whose plan was fetched but lacks the model", () => {
// Model known to work on team; free plan was fetched and model is NOT in it
mockGetModelPlanTypes.mockReturnValue(["team"]);
mockIsPlanFetched.mockReturnValue(true); // both plans fetched
const pool = createPool(
{ token: "tok-free", planType: "free", email: "free@test.com" },
{ token: "tok-team", planType: "team", email: "team@test.com" },
);
// Lock the team account
const first = pool.acquire({ model: "gpt-5.4" });
expect(first).not.toBeNull();
expect(first!.token).toBe("tok-team");
// Second request — free plan was fetched and model is absent → null
const second = pool.acquire({ model: "gpt-5.4" });
expect(second).toBeNull();
});
it("unfetched plans are included even when model appears plan-locked", () => {
// Model supports team only, no plans have been fetched, but only free accounts exist
mockGetModelPlanTypes.mockReturnValue(["team"]);
mockIsPlanFetched.mockReturnValue(false); // no plans fetched yet
const pool = createPool(
{ token: "tok-free", planType: "free", email: "free@test.com" },
);
// Free plan is unfetched → included as possibly compatible
const acquired = pool.acquire({ model: "gpt-5.4" });
expect(acquired).not.toBeNull();
expect(acquired!.token).toBe("tok-free");
});
});
|