6715fbd971
- EditorModal loads once per path (a parent re-render no longer reloads the file over the user's typing), refuses Save after a failed load, opens a truncated (>256 KB) file read-only, and asks before overwriting a file that changed on disk or could not be re-read. - Settings/Hooks editors unset only keys from the snapshot taken at Edit, warn when the file changed underneath, keep a renamed key's value, and refuse a new key named like a redacted one (env) or a read-only hook event, which used to replace the secret/hooks. - Project scope shows its resolved folder and is disabled inside the plugin cache. Account-profile, pricing-row and pet deletes confirm; two-click actions disarm after 3 s; unsaved drafts ask before closing. - Search indexes every Claude Code / Codex sub-tab, opens it via ?tab= and scrolls to + highlights the row; the active config tab is in the URL. - General splits into General / Data / About; Pricing moves to a Cost group with one shared load; config tabs group as Content / Extensions / Config with arrow-key roving; Promise.allSettled per tab. - Data: browser-preferences export/import (allowlisted keys), per-section reset, housekeeping dry-run first, clear-all behind a typed phrase. Server: purge days must be a whole number >= 1 (INVALID_PURGE_DAYS) and dry_run returns exactly what the real run would delete. - Alerts can be edited; webhooks/alerts/remote-source failures toast; copies go through copyText; modals get dialog roles and useDialog. - Sagi: Ctrl/Cmd+B only when shown and not typing, default spot clear of row controls, arrow keys move it, reduced motion honoured; the dead /run launch link goes to the workspace.
152 lines
4.7 KiB
TypeScript
152 lines
4.7 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";
|
|
import { MemoryRouter } from "react-router-dom";
|
|
|
|
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(
|
|
<MemoryRouter>
|
|
<CcConfig />
|
|
</MemoryRouter>
|
|
);
|
|
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);
|
|
});
|
|
});
|