diff --git a/shared/hooks/use-accounts.ts b/shared/hooks/use-accounts.ts index 95df676a..1e142f2b 100644 --- a/shared/hooks/use-accounts.ts +++ b/shared/hooks/use-accounts.ts @@ -24,11 +24,10 @@ export function useAccounts() { const [persistenceHealth, setPersistenceHealth] = useState({ ok: true }); const addCleanupRef = useRef<(() => void) | null>(null); - const loadAccounts = useCallback(async (fresh = false) => { + const loadAccounts = useCallback(async () => { setRefreshing(true); try { - const url = fresh ? "/auth/accounts?quota=fresh" : "/auth/accounts?quota=true"; - const resp = await fetch(url); + const resp = await fetch("/auth/accounts?quota=true"); const data = await resp.json(); setList(data.accounts || []); if (data.persistence_health && typeof data.persistence_health === "object") { @@ -318,7 +317,7 @@ export function useAccounts() { addInfo, addError, persistenceHealth, - refresh: useCallback(() => loadAccounts(true), [loadAccounts]), + refresh: loadAccounts, patchLocal, startAdd, cancelAdd, diff --git a/src/routes/accounts.ts b/src/routes/accounts.ts index 7f7fbadd..3631a9ae 100644 --- a/src/routes/accounts.ts +++ b/src/routes/accounts.ts @@ -163,7 +163,7 @@ export function createAccountRoutes(pool: AccountPool, scheduler: RefreshSchedul return c.json({ success: true }); }); - app.get("/auth/accounts", async (c) => { + app.get("/auth/accounts", (c) => { const accounts = querySvc.listFresh(); return c.json({ accounts, diff --git a/tests/e2e/quota-refresh.test.ts b/tests/e2e/quota-refresh.test.ts index 8c729cb8..b9c94f4d 100644 --- a/tests/e2e/quota-refresh.test.ts +++ b/tests/e2e/quota-refresh.test.ts @@ -3,12 +3,12 @@ * * Tests: * - GET /auth/accounts returns cached quota from background refresh - * - GET /auth/accounts?quota=fresh forces live upstream fetch + * - GET /auth/accounts?quota=fresh returns cached quota without live upstream fetch * - GET /auth/quota/warnings returns active warnings * - Accounts with exhausted quota are skipped by acquire() */ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { describe, it, expect, beforeAll, afterAll, vi } from "vitest"; import "@helpers/e2e-setup.js"; import { createValidJwt } from "@helpers/jwt.js"; @@ -20,6 +20,7 @@ import { AccountPool } from "@src/auth/account-pool.js"; import { RefreshScheduler } from "@src/auth/refresh-scheduler.js"; import type { CodexQuota } from "@src/auth/types.js"; import { updateWarnings, clearWarnings, getActiveWarnings } from "@src/auth/quota-warnings.js"; +import { CodexApi } from "@src/proxy/codex-api.js"; let app: Hono; let pool: AccountPool; @@ -103,6 +104,29 @@ describe("E2E: quota auto-refresh", () => { pool.removeAccount(id); }); + it("GET /auth/accounts?quota=fresh returns cached quota without upstream call", async () => { + const id = pool.addAccount(createValidJwt({ + accountId: "acct-quota-fresh", + email: "quotafresh@test.com", + planType: "plus", + })); + pool.updateCachedQuota(id, makeQuota(77)); + const getUsageSpy = vi.spyOn(CodexApi.prototype, "getUsage").mockRejectedValue(new Error("unexpected usage call")); + + try { + const res = await app.request("/auth/accounts?quota=fresh"); + expect(res.status).toBe(200); + + const body = await res.json() as { accounts: Array<{ id: string; quota?: CodexQuota }> }; + const acct = body.accounts.find((a) => a.id === id); + expect(acct?.quota?.rate_limit.used_percent).toBe(77); + expect(getUsageSpy).not.toHaveBeenCalled(); + } finally { + getUsageSpy.mockRestore(); + pool.removeAccount(id); + } + }); + it("GET /auth/quota/warnings returns empty when no warnings", async () => { const res = await app.request("/auth/quota/warnings"); expect(res.status).toBe(200); diff --git a/tests/unit/web/account-list-quota-refresh.test.ts b/tests/unit/web/account-list-quota-refresh.test.ts new file mode 100644 index 00000000..6e8e9d3c --- /dev/null +++ b/tests/unit/web/account-list-quota-refresh.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "fs"; +import { resolve } from "path"; + +describe("AccountList quota refresh", () => { + it("uses the explicit quota endpoint instead of token refresh", () => { + const source = readFileSync( + resolve(__dirname, "../../../web/src/components/AccountList.tsx"), + "utf-8", + ); + + expect(source).toContain("`/auth/accounts/${encodeURIComponent(id)}/quota`"); + expect(source).not.toContain("`/auth/accounts/${encodeURIComponent(id)}/refresh`"); + }); +}); diff --git a/web/src/components/AccountList.tsx b/web/src/components/AccountList.tsx index e5036649..4f4ef133 100644 --- a/web/src/components/AccountList.tsx +++ b/web/src/components/AccountList.tsx @@ -390,7 +390,7 @@ export function AccountList({ accounts, loading, onDelete, onRefresh, refreshing ) : ( displayAccounts.slice(0, visibleCount).map((acct, i) => ( - { await fetch(`/auth/accounts/${encodeURIComponent(id)}/refresh`, { method: "POST" }); onRefresh(); }} onToggleStatus={onToggleStatus} onUpdateLabel={onUpdateLabel} /> + { await fetch(`/auth/accounts/${encodeURIComponent(id)}/quota`); onRefresh(); }} onToggleStatus={onToggleStatus} onUpdateLabel={onUpdateLabel} /> )) )}