feat(pool): an admin's switch moves a user's Claude login at once
PUT /api/accounts/pins (and agenthub accounts pin) now writes a pinned Claude login into the user's home right away (claude-pool.switchHome): every home's newer pair is copied back first, the member's own login is never written over, and no pane is restarted — running sessions take the new pair at their next credentials read. A live home stays on the login it holds, so unpinning does not flip it back. The Users page calls it "Switch a user's account" and says what happened.
This commit is contained in:
@@ -5,8 +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. An admin pins a user to one account and
|
||||
* unpins them, each with their own password; the pins show on the accounts.
|
||||
* sees no add or remove control. An admin switches a user to one account and
|
||||
* back to automatic, each with their own password; it shows on the accounts.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -173,17 +173,20 @@ describe("AccountPoolCard", () => {
|
||||
...POOL,
|
||||
pins: { claude: [{ userId: 8, username: "leo", account: "team-1" }], codex: [] },
|
||||
});
|
||||
accountPool.pin.mockResolvedValue({ pin: { userId: 7, username: "mia", account: "team-1" } });
|
||||
accountPool.pin.mockResolvedValue({
|
||||
pin: { userId: 7, username: "mia", account: "team-1" },
|
||||
switched: "switched",
|
||||
});
|
||||
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 }));
|
||||
expect(await screen.findByText("Used by: leo")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: /switch a user's account/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"
|
||||
(b) => b.textContent === "Switch"
|
||||
)!;
|
||||
expect(confirm).toBeDisabled();
|
||||
await user.type(screen.getByLabelText("Your password"), "boss-pass");
|
||||
@@ -198,14 +201,17 @@ describe("AccountPoolCard", () => {
|
||||
...POOL,
|
||||
pins: { claude: [{ userId: 8, username: "leo", account: "team-1" }], codex: [] },
|
||||
});
|
||||
accountPool.pin.mockResolvedValue({ pin: { userId: 8, username: "leo", account: null } });
|
||||
accountPool.pin.mockResolvedValue({
|
||||
pin: { userId: 8, username: "leo", account: null },
|
||||
switched: null,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
render(<AccountPoolCard isAdmin />);
|
||||
await user.click(await screen.findByRole("button", { name: "Unpin leo" }));
|
||||
await user.click(await screen.findByRole("button", { name: "Back to automatic for 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")!
|
||||
Array.from(dialog.querySelectorAll("button")).find((b) => b.textContent === "Switch")!
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(accountPool.pin).toHaveBeenCalledWith("claude", 8, null, "boss-pass")
|
||||
|
||||
Reference in New Issue
Block a user