Spaces:
Paused
Paused
icebear
fix: model list not updating at startup β fast-retry on auth race (#149)
56be298 unverified | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; | |
| vi.mock("../../config.js", () => ({ | |
| getConfig: vi.fn(() => ({ | |
| model: { default: "gpt-5.2-codex" }, | |
| })), | |
| })); | |
| vi.mock("../../paths.js", () => ({ | |
| getConfigDir: vi.fn(() => "/tmp/test-config"), | |
| getDataDir: vi.fn(() => "/tmp/test-data"), | |
| })); | |
| const mockGetModels = vi.fn<() => Promise<Array<{ slug: string }>>>(); | |
| vi.mock("../../proxy/codex-api.js", () => ({ | |
| CodexApi: vi.fn().mockImplementation(() => ({ | |
| getModels: mockGetModels, | |
| })), | |
| })); | |
| vi.mock("../model-store.js", () => ({ | |
| applyBackendModelsForPlan: vi.fn(), | |
| })); | |
| vi.mock("../../utils/jitter.js", () => ({ | |
| jitter: vi.fn((ms: number) => ms), | |
| })); | |
| import type { AccountPool } from "../../auth/account-pool.js"; | |
| import type { CookieJar } from "../../proxy/cookie-jar.js"; | |
| import { | |
| startModelRefresh, | |
| stopModelRefresh, | |
| hasFetchedModels, | |
| } from "../model-fetcher.js"; | |
| function createMockAccountPool(authenticated: boolean): AccountPool { | |
| return { | |
| isAuthenticated: vi.fn(() => authenticated), | |
| getDistinctPlanAccounts: vi.fn(() => | |
| authenticated | |
| ? [{ planType: "team", entryId: "e1", token: "t1", accountId: "a1" }] | |
| : [], | |
| ), | |
| release: vi.fn(), | |
| } as unknown as AccountPool; | |
| } | |
| const mockCookieJar = {} as CookieJar; | |
| describe("model-fetcher retry logic", () => { | |
| beforeEach(() => { | |
| vi.useFakeTimers(); | |
| vi.clearAllMocks(); | |
| stopModelRefresh(); | |
| }); | |
| afterEach(() => { | |
| stopModelRefresh(); | |
| vi.useRealTimers(); | |
| }); | |
| it("retries when accounts are not authenticated at startup", async () => { | |
| const pool = createMockAccountPool(false); | |
| startModelRefresh(pool, mockCookieJar); | |
| expect(hasFetchedModels()).toBe(false); | |
| // Advance past initial delay (1s) | |
| await vi.advanceTimersByTimeAsync(1_000); | |
| expect(pool.isAuthenticated).toHaveBeenCalled(); | |
| expect(hasFetchedModels()).toBe(false); | |
| // Should retry at 10s intervals β advance to first retry | |
| await vi.advanceTimersByTimeAsync(10_000); | |
| expect((pool.isAuthenticated as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThanOrEqual(2); | |
| }); | |
| it("succeeds on retry when accounts become ready", async () => { | |
| let authenticated = false; | |
| const pool = { | |
| isAuthenticated: vi.fn(() => authenticated), | |
| getDistinctPlanAccounts: vi.fn(() => | |
| authenticated | |
| ? [{ planType: "free", entryId: "e1", token: "t1", accountId: "a1" }] | |
| : [], | |
| ), | |
| release: vi.fn(), | |
| } as unknown as AccountPool; | |
| mockGetModels.mockResolvedValue([{ slug: "gpt-5.4" }]); | |
| startModelRefresh(pool, mockCookieJar); | |
| // Initial attempt β not authenticated | |
| await vi.advanceTimersByTimeAsync(1_000); | |
| expect(hasFetchedModels()).toBe(false); | |
| // Now accounts become active | |
| authenticated = true; | |
| // Advance to first retry (10s) | |
| await vi.advanceTimersByTimeAsync(10_000); | |
| expect(hasFetchedModels()).toBe(true); | |
| }); | |
| it("falls back to hourly after max retries", async () => { | |
| const pool = createMockAccountPool(false); | |
| startModelRefresh(pool, mockCookieJar); | |
| // Initial delay + 12 retries Γ 10s = 1s + 120s | |
| await vi.advanceTimersByTimeAsync(1_000 + 12 * 10_000); | |
| expect(hasFetchedModels()).toBe(false); | |
| // Should have logged max retries and scheduled hourly | |
| // Verify no more retries by advancing another 10s | |
| const callsBefore = (pool.isAuthenticated as ReturnType<typeof vi.fn>).mock.calls.length; | |
| await vi.advanceTimersByTimeAsync(10_000); | |
| const callsAfter = (pool.isAuthenticated as ReturnType<typeof vi.fn>).mock.calls.length; | |
| // No additional calls at 10s interval (hourly is much later) | |
| expect(callsAfter).toBe(callsBefore); | |
| }); | |
| it("succeeds immediately when accounts are ready at startup", async () => { | |
| const pool = createMockAccountPool(true); | |
| mockGetModels.mockResolvedValue([{ slug: "gpt-5.4" }]); | |
| startModelRefresh(pool, mockCookieJar); | |
| await vi.advanceTimersByTimeAsync(1_000); | |
| expect(hasFetchedModels()).toBe(true); | |
| expect(pool.release).toHaveBeenCalledWith("e1"); | |
| }); | |
| }); | |