feat(pool): pin a user to one Claude/Codex pool account
An admin pins a server-mode user to one pool account per tool (account_pins; PUT /api/accounts/pins with the password step-up and audit, the Users page's Pin a user, agenthub accounts pin|unpin). The workspace owner's pin wins over the balancer whenever the pane may use the account; a pinned login still waits while another pool login holds the home.
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
* 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.
|
||||
* sees no add or remove control. An admin pins a user to one account and
|
||||
* unpins them, each with their own password; the pins show on the accounts.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -19,10 +20,12 @@ const accountPool = vi.hoisted(() => ({
|
||||
add: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
profiles: vi.fn(),
|
||||
pin: vi.fn(),
|
||||
}));
|
||||
const users = vi.hoisted(() => ({ list: vi.fn() }));
|
||||
vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return { ...actual, api: { accountPool } };
|
||||
return { ...actual, api: { accountPool, users } };
|
||||
});
|
||||
|
||||
import { AccountPoolCard } from "../AccountPoolCard";
|
||||
@@ -66,6 +69,12 @@ const POOL: AccountPool = {
|
||||
beforeEach(() => {
|
||||
for (const fn of Object.values(accountPool)) fn.mockReset();
|
||||
accountPool.list.mockResolvedValue(POOL);
|
||||
users.list.mockResolvedValue({
|
||||
users: [
|
||||
{ id: 7, username: "mia", disabled: false },
|
||||
{ id: 8, username: "leo", disabled: false },
|
||||
],
|
||||
});
|
||||
accountPool.profiles.mockResolvedValue({
|
||||
profiles: [
|
||||
{
|
||||
@@ -158,4 +167,48 @@ describe("AccountPoolCard", () => {
|
||||
expect(accountPool.remove).toHaveBeenCalledWith("codex", "cx-1", "boss-pass")
|
||||
);
|
||||
});
|
||||
|
||||
it("pins a user to an account and unpins them, with the admin's own password", async () => {
|
||||
accountPool.list.mockResolvedValue({
|
||||
...POOL,
|
||||
pins: { claude: [{ userId: 8, username: "leo", account: "team-1" }], codex: [] },
|
||||
});
|
||||
accountPool.pin.mockResolvedValue({ pin: { userId: 7, username: "mia", account: "team-1" } });
|
||||
const user = userEvent.setup();
|
||||
render(<AccountPoolCard isAdmin />);
|
||||
expect(await screen.findByText("Pinned: leo")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: /pin a user/i }));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
await waitFor(() => expect(screen.getByRole("option", { name: "mia" })).toBeInTheDocument());
|
||||
await user.selectOptions(screen.getByLabelText("User"), "7");
|
||||
await user.selectOptions(screen.getByLabelText("Account"), "team-1");
|
||||
const confirm = Array.from(dialog.querySelectorAll("button")).find(
|
||||
(b) => b.textContent === "Save pin"
|
||||
)!;
|
||||
expect(confirm).toBeDisabled();
|
||||
await user.type(screen.getByLabelText("Your password"), "boss-pass");
|
||||
await user.click(confirm);
|
||||
await waitFor(() =>
|
||||
expect(accountPool.pin).toHaveBeenCalledWith("claude", 7, "team-1", "boss-pass")
|
||||
);
|
||||
});
|
||||
|
||||
it("unpins from the pinned list", async () => {
|
||||
accountPool.list.mockResolvedValue({
|
||||
...POOL,
|
||||
pins: { claude: [{ userId: 8, username: "leo", account: "team-1" }], codex: [] },
|
||||
});
|
||||
accountPool.pin.mockResolvedValue({ pin: { userId: 8, username: "leo", account: null } });
|
||||
const user = userEvent.setup();
|
||||
render(<AccountPoolCard isAdmin />);
|
||||
await user.click(await screen.findByRole("button", { name: "Unpin leo" }));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
await user.type(screen.getByLabelText("Your password"), "boss-pass");
|
||||
await user.click(
|
||||
Array.from(dialog.querySelectorAll("button")).find((b) => b.textContent === "Save pin")!
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(accountPool.pin).toHaveBeenCalledWith("claude", 8, null, "boss-pass")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user