/** * @file AccountPoolCard.test.tsx * @description The Users page's account pool card: the pools board with every * tool's accounts (live panes and quota) and members per pool, then a tab per * tool with no pool — Oh My Pi's login sharing, and a "no pool" note for any * other installed tool; 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. An account's Edit ticks the users who run on * it, saved with the admin's own password; the users show on the account. * The board moves members and accounts between pools (drag, or a column's * menu) — an account sits in one pool, so a move takes it out of the other — * splits an account off onto New pool, merges a pool by dragging its only * account into another, adds a saved profile to a pool, shows each account's * plan, and saves every change with ONE password. * @author Nguyễn Ngọc Trí Vĩ */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import type { AccountPool } from "../../../lib/api"; const accountPool = vi.hoisted(() => ({ list: vi.fn(), add: vi.fn(), remove: vi.fn(), profiles: vi.fn(), setUsers: vi.fn(), createGroup: vi.fn(), updateGroup: vi.fn(), deleteGroup: vi.fn(), })); const users = vi.hoisted(() => ({ list: vi.fn(), avatarUrl: () => null })); const aiTools = vi.hoisted(() => ({ list: vi.fn(), ompSharing: vi.fn(), setOmpSharing: vi.fn(), iconUrl: (id: string) => `/api/ai-tools/${id}/icon`, })); vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { accountPool, users, aiTools } }; }); import { AccountPoolCard } from "../AccountPoolCard"; /** The card reads `?poolAdd=` and links to Settings: it needs a router. */ const card = (isAdmin: boolean, url = "/admin") => ( ); 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, quota: { fiveHour: { utilization: 42, resetsAt: new Date(Date.now() + 2 * 3600e3 + 630e3).toISOString(), }, sevenDay: { utilization: 15, resetsAt: new Date(Date.now() + 3 * 86400e3 + 3630e3).toISOString(), }, checkedAt: "2026-10-01T00:00:00Z", error: null, }, quotaPercent: 42, full: false, }, { name: "team-2", kind: "token", email: null, refreshError: null, addedAt: "2026-10-01T00:00:00Z", expiresAt: null, expired: false, livePanes: 0, spend5h: 0, spend7d: 0, tokens5h: 0, tokens7d: 0, // A 5-hour window that has reset since the reading is full again. quota: { fiveHour: { utilization: 100, resetsAt: "2020-01-01T00:00:00Z" }, sevenDay: null, checkedAt: "2026-10-01T00:00:00Z", error: null, }, quotaPercent: null, 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, }; const tool = (id: string, label: string, installed = true) => ({ id, label, installed }); const OMP = { available: true, enabled: false, installed: true, broker: { running: false }, gateway: { running: false }, }; /** The account names a pool column holds (its move menu names others too). */ const held = (col: HTMLElement) => within(col) .queryAllByTestId("pool-group-account") .map((c) => within(c).queryByText(/^(team|cx)-\d$/)?.textContent); /** A drag-and-drop between two elements, through one shared DataTransfer. */ function drag(from: Element, to: Element) { const store: Record = {}; const dataTransfer = { setData: (k: string, v: string) => (store[k] = v), getData: (k: string) => store[k] ?? "", get types() { return Object.keys(store); }, effectAllowed: "", }; fireEvent.dragStart(from, { dataTransfer }); fireEvent.dragOver(to, { dataTransfer }); fireEvent.drop(to, { dataTransfer }); } beforeEach(() => { for (const fn of Object.values(accountPool)) fn.mockReset(); aiTools.list.mockResolvedValue({ tools: [ tool("claude", "Claude Code"), tool("codex", "Codex"), tool("cursor", "Cursor CLI"), tool("omp", "Oh My Pi"), tool("antigravity", "Antigravity"), tool("goose", "Goose", false), ], default: "claude", }); aiTools.ompSharing.mockResolvedValue(OMP); aiTools.setOmpSharing.mockResolvedValue({ ...OMP, enabled: true }); accountPool.list.mockResolvedValue(POOL); users.list.mockResolvedValue({ users: [ { id: 7, username: "mia", role: "developer", disabled: false }, { id: 8, username: "leo", role: "analyst", disabled: false }, ], }); 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("shows every tool's accounts on the board, and a tab per tool with no pool", async () => { const user = userEvent.setup(); render(card(false)); expect(await screen.findByText("team-1")).toBeInTheDocument(); // With no pool made yet, every account sits in the shared pool, by tool. const shared = screen.getByTestId("pool-shared"); expect(shared).toHaveTextContent("Claude Code2"); expect(shared).toHaveTextContent("Codex1"); expect(shared).toHaveTextContent("cx-1"); const [account, second] = screen.getAllByTestId("pool-account"); // Every account of the pool has its card; quota is shown as what is LEFT. expect(second).toHaveTextContent("team-2"); const quota = screen.getAllByTestId("pool-quota")[0]!; expect(quota).toHaveTextContent("5 hours"); expect(quota).toHaveTextContent("58%"); expect(quota).toHaveTextContent("resets in 2h 10m"); expect(quota).toHaveTextContent("Week"); expect(quota).toHaveTextContent("85%"); expect(quota).toHaveTextContent("resets in 3d 1h"); const reset = screen.getAllByTestId("pool-quota")[1]!; expect(reset).toHaveTextContent("100%"); expect(reset).toHaveTextContent("—"); expect(account).toHaveTextContent("250.0K tokens"); expect(account).toHaveTextContent("$12.00 · 3.4M tokens in 7 days"); expect(account).toHaveTextContent("2 live"); expect(screen.getByText("team1@example.com")).toBeInTheDocument(); // An analyst reads: no add, remove or new pool. expect(screen.queryByTestId("pool-add-account")).toBeNull(); expect(screen.queryByRole("button", { name: /remove/i })).toBeNull(); expect(screen.queryByRole("button", { name: "New pool" })).toBeNull(); // Pool tools have no tab any more; an installed tool without a pool does. expect(screen.queryByRole("tab", { name: /Codex/ })).toBeNull(); await user.click(screen.getByRole("tab", { name: /Antigravity/ })); expect(screen.getByText("Antigravity has no account pool")).toBeInTheDocument(); expect(screen.getByText(/system keyring/)).toBeInTheDocument(); expect(screen.queryByRole("tab", { name: /Goose/ })).toBeNull(); }); it("adds a Cursor API key from Add account", async () => { accountPool.add.mockResolvedValue({ account: { name: "cu-1" } }); accountPool.list.mockResolvedValue({ ...POOL, cursor: [] }); const user = userEvent.setup(); render(card(true)); await screen.findByText("team-1"); await user.selectOptions(screen.getByTestId("pool-add-account"), "cursor"); const dialog = await screen.findByRole("dialog"); expect(screen.queryByLabelText("From")).toBeNull(); await user.type(screen.getByLabelText("Name"), "cu-1"); await user.type(screen.getByLabelText("Cursor API key"), "key_abc"); await user.type(screen.getByLabelText("Your password"), "boss-pass"); await user.click( Array.from(dialog.querySelectorAll("button")).find((b) => b.textContent === "Add account")! ); await waitFor(() => expect(accountPool.add).toHaveBeenCalledWith( { tool: "cursor", name: "cu-1", token: "key_abc" }, "boss-pass" ) ); expect(accountPool.profiles).not.toHaveBeenCalledWith("cursor"); }); it("builds pools on the board: members, accounts and profiles moved in, one password", async () => { accountPool.list.mockResolvedValue({ ...POOL, groups: [ { id: 1, name: "Team A", users: [{ userId: 8, username: "leo" }], accounts: { claude: ["team-1"], codex: [], cursor: [] }, }, ], }); accountPool.createGroup.mockResolvedValue({ group: { id: 2, name: "Team B" } }); accountPool.updateGroup.mockResolvedValue({ group: {} }); accountPool.add.mockResolvedValue({ account: { name: "work" } }); const user = userEvent.setup(); render(card(true)); const teamA = await screen.findByTestId("pool-group"); expect(teamA).toHaveTextContent("leo"); expect(teamA).toHaveTextContent("team-1"); await screen.findByText("mia"); await user.click(screen.getByRole("button", { name: "New pool" })); const names = screen.getAllByLabelText("Pool name"); await user.type(names[names.length - 1]!, "Team B"); const teamB = screen.getAllByTestId("pool-group")[1]!; drag(screen.getByText("mia").closest("[data-testid=pool-member]")!, teamB); expect(teamB).toHaveTextContent("mia"); const add = within(teamB).getByLabelText("Move an account into Team B"); await user.selectOptions(add, "codex|cx-1"); await user.selectOptions(add, "claude|profile:work"); expect(held(screen.getByTestId("pool-shared"))).not.toContain("cx-1"); expect(screen.getByTestId("pool-board-save")).toHaveTextContent("2 unsaved changes"); expect(accountPool.updateGroup).not.toHaveBeenCalled(); await user.click( within(screen.getByTestId("pool-board-save")).getByRole("button", { name: "Save" }) ); const dialog = await screen.findByRole("dialog"); expect(dialog).toHaveTextContent("Move the saved profile Work into the pool"); await user.type(within(dialog).getByLabelText("Your password"), "boss-pass"); await user.click(within(dialog).getByRole("button", { name: "Save" })); await waitFor(() => expect(accountPool.updateGroup).toHaveBeenCalledWith( 2, { userIds: [7], accounts: { claude: ["work"], codex: ["cx-1"], cursor: [] } }, "boss-pass" ) ); expect(accountPool.add).toHaveBeenCalledWith( { tool: "claude", name: "work", profile: "work" }, "boss-pass" ); expect(accountPool.createGroup).toHaveBeenCalledWith("Team B", "boss-pass"); // Team A did not change: nothing is sent for it. expect(accountPool.updateGroup).toHaveBeenCalledTimes(1); }); it("drags accounts between pools — one pool each — and a member back to the shared pool", async () => { accountPool.list.mockResolvedValue({ ...POOL, groups: [ { id: 1, name: "Team A", users: [{ userId: 8, username: "leo" }], accounts: { claude: ["team-1"], codex: [], cursor: [] }, }, ], }); accountPool.updateGroup.mockResolvedValue({ group: {} }); const user = userEvent.setup(); render(card(true)); const teamA = await screen.findByTestId("pool-group"); const shared = screen.getByTestId("pool-shared"); const item = (col: HTMLElement, name: string) => within(col) .getAllByTestId("pool-group-account") .find((c) => c.textContent?.includes(name))!; drag(item(shared, "team-2"), teamA); expect(held(teamA)).toEqual(["team-1", "team-2"]); // And back again: an account is in one pool, so it leaves the other. drag(item(teamA, "team-1"), shared); expect(held(teamA)).toEqual(["team-2"]); expect(held(shared)).toContain("team-1"); expect(teamA).toHaveTextContent("team-2"); await user.click( within(teamA).getByRole("button", { name: "Send leo back to the shared pool" }) ); expect(shared).toHaveTextContent("leo"); await user.click( within(screen.getByTestId("pool-board-save")).getByRole("button", { name: "Save" }) ); const dialog = await screen.findByRole("dialog"); await user.type(within(dialog).getByLabelText("Your password"), "boss-pass"); await user.click(within(dialog).getByRole("button", { name: "Save" })); await waitFor(() => expect(accountPool.updateGroup).toHaveBeenCalledWith( 1, { name: "Team A", userIds: [], accounts: { claude: ["team-2"], codex: [], cursor: [] }, }, "boss-pass" ) ); }); it("shows each account's plan on its card", async () => { accountPool.list.mockResolvedValue({ ...POOL, accounts: [{ ...POOL.accounts[0]!, plan: "Max 5x" }, POOL.accounts[1]!], }); render(card(false)); const [first, second] = await screen.findAllByTestId("pool-account"); expect(within(first!).getByTestId("pool-account-plan")).toHaveTextContent("Max 5x"); expect(within(second!).queryByTestId("pool-account-plan")).toBeNull(); }); it("splits an account off onto New pool, and merges a pool by its only account", async () => { accountPool.list.mockResolvedValue({ ...POOL, groups: [ { id: 1, name: "Team A", users: [{ userId: 8, username: "leo" }], accounts: { claude: ["team-1"], codex: [], cursor: [] }, }, { id: 2, name: "Team B", users: [{ userId: 7, username: "mia" }], accounts: { claude: [], codex: ["cx-1"], cursor: [] }, }, ], }); accountPool.createGroup.mockResolvedValue({ group: { id: 3, name: "Solo" } }); accountPool.updateGroup.mockResolvedValue({ group: {} }); accountPool.deleteGroup.mockResolvedValue({ ok: true }); const user = userEvent.setup(); render(card(true)); const [teamA, teamB] = await screen.findAllByTestId("pool-group"); await screen.findByText("mia"); const item = (col: HTMLElement, name: string) => within(col) .getAllByTestId("pool-group-account") .find((c) => c.textContent?.includes(name))!; // Split: team-2 leaves the shared pool for a new pool of its own. drag(item(screen.getByTestId("pool-shared"), "team-2"), screen.getByTestId("pool-new")); const pools = screen.getAllByTestId("pool-group"); expect(pools).toHaveLength(3); expect(held(pools[2]!)).toEqual(["team-2"]); await user.type(within(pools[2]!).getByLabelText("Pool name"), "Solo"); // Merge: Team B's only account into Team A brings mia along; Team B goes. drag(item(teamB!, "cx-1"), teamA!); expect(screen.getAllByTestId("pool-group")).toHaveLength(2); expect(held(teamA!)).toEqual(["team-1", "cx-1"]); expect(teamA).toHaveTextContent("mia"); await user.click( within(screen.getByTestId("pool-board-save")).getByRole("button", { name: "Save" }) ); const dialog = await screen.findByRole("dialog"); expect(dialog).toHaveTextContent("Merge Team B into Team A: pool Team B is deleted"); await user.type(within(dialog).getByLabelText("Your password"), "boss-pass"); await user.click(within(dialog).getByRole("button", { name: "Save" })); await waitFor(() => expect(accountPool.deleteGroup).toHaveBeenCalledWith(2, "boss-pass")); expect(accountPool.updateGroup).toHaveBeenCalledWith( 1, { name: "Team A", userIds: [8, 7], accounts: { claude: ["team-1"], codex: ["cx-1"], cursor: [] }, }, "boss-pass" ); expect(accountPool.createGroup).toHaveBeenCalledWith("Solo", "boss-pass"); expect(accountPool.updateGroup).toHaveBeenCalledWith( 3, { userIds: [], accounts: { claude: ["team-2"], codex: [], cursor: [] } }, "boss-pass" ); // The members moved before the emptied pool was deleted. const order = (fn: { mock: { invocationCallOrder: number[] } }) => fn.mock.invocationCallOrder[0]!; expect(order(accountPool.updateGroup)).toBeLessThan(order(accountPool.deleteGroup)); }); it("turns Oh My Pi login sharing on from its tab", async () => { const user = userEvent.setup(); render(card(true)); await screen.findByText("team-1"); await user.click(screen.getByRole("tab", { name: /Oh My Pi/ })); expect(screen.getByTestId("omp-pool-status")).toHaveTextContent(/Sharing is off/); await user.click(screen.getByRole("switch")); await waitFor(() => expect(aiTools.setOmpSharing).toHaveBeenCalledWith(true)); expect(screen.getByRole("tab", { name: /Oh My Pi/ })).toHaveTextContent("shared"); }); it("moves a saved login in, the server's own included", async () => { accountPool.add.mockResolvedValue({ account: { name: "work" } }); const user = userEvent.setup(); render(card(true)); await screen.findByText("team-1"); await user.selectOptions(screen.getByTestId("pool-add-account"), "claude"); 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(card(true)); await screen.findByText("team-1"); await user.selectOptions(screen.getByTestId("pool-add-account"), "claude"); 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(card(true)); await screen.findByText("team-1"); 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") ); }); it("sets who runs on an account from its Edit, with the admin's own password", async () => { accountPool.list.mockResolvedValue({ ...POOL, pins: { claude: [ { userId: 8, username: "leo", account: "team-1" }, { userId: 7, username: "mia", account: "team-0" }, ], codex: [], }, }); accountPool.setUsers.mockResolvedValue({ added: [{ userId: 7, username: "mia", switched: "switched" }], removed: [{ userId: 8, username: "leo" }], }); const user = userEvent.setup(); render(card(true)); expect(await screen.findByText("Used by: leo")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /switch a user/i })).toBeNull(); await user.click(screen.getByRole("button", { name: "Edit team-1" })); const dialog = await screen.findByRole("dialog"); const leo = await screen.findByRole("checkbox", { name: /leo/ }); const mia = screen.getByRole("checkbox", { name: /mia/ }); expect(leo).toHaveAttribute("aria-checked", "true"); expect(mia).toHaveAttribute("aria-checked", "false"); expect(screen.getByText("now on team-0")).toBeInTheDocument(); await user.click(mia); await user.click(leo); const save = Array.from(dialog.querySelectorAll("button")).find( (b) => b.textContent === "Save" )!; expect(save).toBeDisabled(); await user.type(screen.getByLabelText("Your password"), "boss-pass"); await user.click(save); await waitFor(() => expect(accountPool.setUsers).toHaveBeenCalledWith("claude", "team-1", [7], "boss-pass") ); }); it("moves a saved Codex profile in, and opens on one from its Move to pool link", async () => { accountPool.add.mockResolvedValue({ account: { name: "cx" } }); accountPool.profiles.mockImplementation(async (tool: string) => tool === "codex" ? { profiles: [ { name: "cx", label: "Codex", email: "c@x", subscriptionType: "plus", active: false }, ], } : { profiles: [] } ); const user = userEvent.setup(); render(card(true, "/admin?poolAdd=codex&poolProfile=cx")); const dialog = await screen.findByRole("dialog"); await waitFor(() => expect(accountPool.profiles).toHaveBeenCalledWith("codex")); await waitFor(() => expect(screen.getByLabelText("Profile")).toHaveValue("cx")); await user.type(screen.getByLabelText("Your password"), "boss-pass"); const confirm = Array.from(dialog.querySelectorAll("button")).find( (b) => b.textContent === "Add account" )!; await user.click(confirm); await waitFor(() => expect(accountPool.add).toHaveBeenCalledWith( { tool: "codex", name: "cx", profile: "cx" }, "boss-pass" ) ); }); it("links back to the profiles in Settings", async () => { render(card(false)); const link = await screen.findByRole("link", { name: "Profiles in Settings" }); expect(link.getAttribute("href")).toBe("/settings/agents#tool-profiles"); }); });