Files
AgentHub/client/src/pages/__tests__/CcConfig.profilesUsage.test.tsx
T
nntrivi2001 1aee080fca fix(cc-config): read the live account's quota from the CLI, and ask for every account at once
The quota check failed with `invalid_grant` for the account the user was
logged into - the one profile they would try first. An OAuth refresh token is
single use, so every refresh Claude Code performs on its own account retires
the copy sitting in that account's saved profile: the stored credentials of
the LIVE account are the one set guaranteed to be rejected, however recently
the profile was saved. Confirmed on a real install - the saved profile held a
refresh token three rotations behind `.credentials.json`.

`cc-profiles.liveCredentialsFor` now matches a profile's accountUuid against
the live credentials and, on a hit, the usage call uses what the CLI is
holding. That token is never refreshed and that file is never written:
rotating it would log the user out of their own terminal. An expired live
token answers ELIVEEXPIRED - using Claude Code once makes the CLI refresh it -
and a genuinely spent refresh token is now reported as ESTALE with what to do
about it, instead of echoing Anthropic's raw 400.

Opening the Profiles modal also asks for every saved account at once, rather
than one button per row: comparing accounts is the only reason the panel
exists. The guard against rewriting credentials over and over is no longer
"one row at a time" but the rules above - the live account costs a plain read,
and any other profile refreshes only once its token has actually expired,
which then leaves it valid for hours of further opens. Each row resolves on
its own, so a superseded account still lets its neighbours report.
2026-09-17 23:17:06 +07:00

147 lines
4.6 KiB
TypeScript

/**
* @file Covers the quota lines in the Claude Code auth-profile modal: opening
* it asks for EVERY saved account's quota, not one row at a time.
*
* The one-at-a-time version made the user click through their own accounts to
* answer the only question the panel exists for — which account still has room.
* The other half is that each row fails on its own: an account whose saved
* login has been superseded must not stop its neighbours from reporting.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
const ccConfigMock = vi.hoisted(() => {
const r = (value: unknown) => vi.fn().mockResolvedValue(value);
const window = (utilization: number) => ({
utilization,
resetsAt: "2026-09-18T08:00:00Z",
});
return {
window,
overview: r({
roots: {
claudeHome: "/home/.claude",
projectClaudeDir: "/repo/.claude",
projectRoot: "/repo",
},
counts: {
skills: { user: 0, project: 0 },
agents: { user: 0, project: 0 },
commands: { user: 0, project: 0 },
outputStyles: { user: 0, project: 0 },
mcpServers: { user: 0, project: 0 },
hooks: {},
plugins: 0,
marketplaces: 0,
keybindings: 0,
settingsFiles: 0,
memory: 0,
},
}),
skills: r({ items: [] }),
agents: r({ items: [] }),
commands: r({ items: [] }),
outputStyles: r({ items: [] }),
memory: r({ items: [] }),
plugins: r({ manifestPath: "", manifestExists: false, plugins: [] }),
marketplaces: r({ marketplaces: [] }),
mcp: r({ mcp: [] }),
hooks: r({ hooks: [] }),
settings: r({ sources: [] }),
keybindings: r({ groups: [] }),
statusline: r({}),
hookScripts: r({ items: [] }),
backups: r({ items: [] }),
file: r({ ok: true, file: "", size: 0, mtime: 0, truncated: false, text: "" }),
pluginsAvailable: r({ installed: [], available: [] }),
updateAllStatus: r({ batch: null }),
// The modal's own list: two saved accounts, one of them live.
profiles: r({
items: [
{ name: "work", label: "Work", active: true, accountUuid: "u1", email: "a@b.c" },
{ name: "spare", label: "Spare", active: false, accountUuid: "u2", email: "d@e.f" },
],
active: { accountUuid: "u1", email: "a@b.c" },
}),
profileUsage: vi.fn().mockImplementation(async (name: string) => {
if (name === "spare") {
throw new Error("this profile's saved login has been superseded");
}
return {
name,
refreshed: false,
fiveHour: window(8),
sevenDay: window(70),
checkedAt: "2026-09-17T16:00:00Z",
};
}),
};
});
vi.mock("../../lib/api", async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
return {
...actual,
api: { ...(actual.api as Record<string, unknown>), ccConfig: ccConfigMock },
};
});
vi.mock("../../lib/eventBus", () => ({
eventBus: {
subscribe: () => () => {},
publish: () => {},
onConnection: () => () => {},
connected: true,
setConnected: () => {},
},
}));
import { CcConfig } from "../CcConfig";
async function settle() {
await act(async () => {
await new Promise((res) => setTimeout(res, 0));
await new Promise((res) => setTimeout(res, 0));
});
}
async function openProfiles() {
render(<CcConfig />);
await settle();
await userEvent.click(screen.getByRole("button", { name: /profile/i }));
await settle();
await settle();
}
afterEach(() => vi.clearAllMocks());
describe("CcConfig — auth profile quotas", () => {
it("asks for every saved account's quota when the modal opens", async () => {
await openProfiles();
expect(ccConfigMock.profileUsage).toHaveBeenCalledWith("work");
expect(ccConfigMock.profileUsage).toHaveBeenCalledWith("spare");
expect(await screen.findByText("8%")).toBeInTheDocument();
expect(await screen.findByText("70%")).toBeInTheDocument();
});
it("reports the account that failed without costing the others their numbers", async () => {
await openProfiles();
expect(await screen.findByText(/saved login has been superseded/i)).toBeInTheDocument();
expect(screen.getByText("8%")).toBeInTheDocument();
});
it("asks once per profile, however many times the panel re-renders", async () => {
await openProfiles();
await settle();
await settle();
const asked = ccConfigMock.profileUsage.mock.calls.filter((c) => c[0] === "work");
expect(asked).toHaveLength(1);
});
});