Files
AgentHub/client/src/components/users/__tests__/AccountPoolCard.test.tsx
T
nntrivi2001 7a88b74016 feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars
- Artifacts hub: upload a file from the computer; make a skill by uploading a
  Markdown file, files, a folder or a .zip/.skill, or by writing it in the
  dialog; add plugins by picking from the list a marketplace link answers
  (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub.
- Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid).
- Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB,
  <data dir>/avatars/), shown with name and role at the sidebar's foot and on
  the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at).
- Users page: per-user storage (GET /api/users/storage, measured in a worker)
  and account-pool spend/tokens per account (server/lib/pool-usage.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:21:33 +07:00

162 lines
5.8 KiB
TypeScript

/**
* @file AccountPoolCard.test.tsx
* @description The Users page's account pool card: lists the Claude and Codex
* accounts with their live panes and quota, says what an empty pool means, an
* admin adds an account only with their own password typed (the token goes
* in once and is not shown back) — a saved login moved in (the server's own
* included), or a setup-token — and removes one the same way, and an analyst
* sees no add or remove control.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { AccountPool } from "../../../lib/api";
const accountPool = vi.hoisted(() => ({
list: vi.fn(),
add: vi.fn(),
remove: vi.fn(),
profiles: vi.fn(),
}));
vi.mock("../../../lib/api", async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, api: { accountPool } };
});
import { AccountPoolCard } from "../AccountPoolCard";
const POOL: AccountPool = {
accounts: [
{
name: "team-1",
kind: "login",
email: "team1@example.com",
refreshError: null,
addedAt: "2026-10-01T00:00:00Z",
expiresAt: null,
expired: false,
livePanes: 2,
spend5h: 1.5,
spend7d: 12,
tokens5h: 250_000,
tokens7d: 3_400_000,
quotaPercent: 42,
full: false,
},
],
codex: [
{
name: "cx-1",
kind: "apikey",
addedAt: "2026-10-02T00:00:00Z",
livePanes: 0,
spend5h: 0,
spend7d: 0,
tokens5h: 0,
tokens7d: 0,
quotaPercent: null,
full: false,
},
],
blockPercent: 90,
};
beforeEach(() => {
for (const fn of Object.values(accountPool)) fn.mockReset();
accountPool.list.mockResolvedValue(POOL);
accountPool.profiles.mockResolvedValue({
profiles: [
{
name: "work",
label: "Work",
email: "w@example.com",
subscriptionType: "max",
active: false,
},
{ name: "me", label: "Me", email: "me@example.com", subscriptionType: "pro", active: true },
],
});
});
describe("AccountPoolCard", () => {
it("lists every account, and explains an empty pool", async () => {
const { unmount } = render(<AccountPoolCard isAdmin={false} />);
expect(await screen.findByText("team-1")).toBeInTheDocument();
expect(screen.getByText("cx-1")).toBeInTheDocument();
expect(screen.getByText("42%")).toBeInTheDocument();
expect(screen.getByText("250.0K tokens")).toBeInTheDocument();
expect(screen.getByText("$12.00 · 3.4M tokens in 7 days")).toBeInTheDocument();
expect(screen.getByText("team1@example.com")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /add account/i })).toBeNull();
expect(screen.queryByRole("button", { name: /remove/i })).toBeNull();
unmount();
accountPool.list.mockResolvedValue({ accounts: [], codex: [], blockPercent: 90 });
render(<AccountPoolCard isAdmin />);
expect(await screen.findByText(/no accounts yet/i)).toBeInTheDocument();
});
it("moves a saved login in, the server's own included", async () => {
accountPool.add.mockResolvedValue({ account: { name: "work" } });
const user = userEvent.setup();
render(<AccountPoolCard isAdmin />);
await screen.findByText("team-1");
await user.click(screen.getByRole("button", { name: /add account/i }));
const dialog = await screen.findByRole("dialog");
const select = await screen.findByLabelText("Profile");
await waitFor(() => expect(screen.getByRole("option", { name: /Work/ })).toBeInTheDocument());
expect(screen.getByRole("option", { name: /Me/ })).toBeEnabled();
await user.selectOptions(select, "work");
const confirm = Array.from(dialog.querySelectorAll("button")).find(
(b) => b.textContent === "Add account"
)!;
expect(confirm).toBeDisabled();
await user.type(screen.getByLabelText("Your password"), "boss-pass");
await user.click(confirm);
await waitFor(() =>
expect(accountPool.add).toHaveBeenCalledWith(
{ tool: "claude", name: "work", profile: "work" },
"boss-pass"
)
);
});
it("adds a setup-token only with the admin's own password", async () => {
accountPool.add.mockResolvedValue({ account: { name: "team-2" } });
const user = userEvent.setup();
render(<AccountPoolCard isAdmin />);
await screen.findByText("team-1");
await user.click(screen.getByRole("button", { name: /add account/i }));
const dialog = await screen.findByRole("dialog");
await user.selectOptions(screen.getByLabelText("From"), "token");
await user.type(screen.getByLabelText("Name"), "team-2");
await user.type(screen.getByLabelText("Token"), "sk-ant-oat01-xyz");
const confirm = Array.from(dialog.querySelectorAll("button")).find(
(b) => b.textContent === "Add account"
)!;
expect(confirm).toBeDisabled();
await user.type(screen.getByLabelText("Your password"), "boss-pass");
expect(confirm).toBeEnabled();
await user.click(confirm);
await waitFor(() =>
expect(accountPool.add).toHaveBeenCalledWith(
{ tool: "claude", name: "team-2", token: "sk-ant-oat01-xyz" },
"boss-pass"
)
);
});
it("removes an account with the admin's own password", async () => {
accountPool.remove.mockResolvedValue({ ok: true });
const user = userEvent.setup();
render(<AccountPoolCard isAdmin />);
await user.click(await screen.findByRole("button", { name: "Remove cx-1" }));
await user.type(await screen.findByLabelText("Your password"), "boss-pass");
await user.click(screen.getByRole("button", { name: "Remove" }));
await waitFor(() =>
expect(accountPool.remove).toHaveBeenCalledWith("codex", "cx-1", "boss-pass")
);
});
});