c83ddf62a0
A member's tmux server keeps the version it started with until their last pane closes, but the dashboard asked it with TMUX_BIN, the newest tmux: a 3.8 client on a 3.6 server spun it at 100% CPU (three members needed a manual kill on 2026-10-15). - /opt/agenthub/libexec/tmux-member (install-system, in sudoers): runs a member's tmux call with the binary of THEIR running server (/proc/<pid>/exe, found from the socket in /proc/net/unix), else TMUX_BIN. It runs as the member and execs only a process they own. sudoArgv uses it once installed (linux-accounts readConf), TMUX_BIN until then. - Users → Members' tmux (GET /api/users/tmux): each member server's version beside the installed one, sessions, agents working. An admin restarts one (POST /api/users/:id/tmux-restart) or every idle outdated one (POST /api/users/tmux-restart-idle), with their password again, audited per member; a server that will not stop answers 409 ETMUXSTUCK naming its pid for root. - server-s3-system.test.js: `delete` is a provision action since Delete user, so the "unknown action" case now uses `purge`. Operators: run `sudo agenthub server install-system` once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
4.1 KiB
TypeScript
101 lines
4.1 KiB
TypeScript
/**
|
|
* @file MemberTmuxCard.test.tsx
|
|
* @description The Users page's members' tmux card: hidden without one Linux
|
|
* user per account, lists each running member server with its version and a
|
|
* badge when it is older than the installed tmux, warns until tmux-member is
|
|
* installed, and an admin restarts one member — or every idle outdated one,
|
|
* the working member left out — only with their own password typed; an
|
|
* analyst sees no restart 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 { MemberTmux } from "../../../lib/api";
|
|
|
|
const users = vi.hoisted(() => ({
|
|
tmux: vi.fn(),
|
|
tmuxRestart: vi.fn(),
|
|
tmuxRestartIdle: vi.fn(),
|
|
}));
|
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
return { ...actual, api: { users } };
|
|
});
|
|
vi.mock("../../../lib/toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
|
|
import { MemberTmuxCard } from "../MemberTmuxCard";
|
|
|
|
const DATA: MemberTmux = {
|
|
available: true,
|
|
installed: "3.8",
|
|
matched: true,
|
|
users: [
|
|
{ id: 2, username: "mia", running: true, version: "3.6", sessions: 2, busy: 0, outdated: true },
|
|
{ id: 3, username: "zed", running: true, version: "3.6", sessions: 1, busy: 1, outdated: true },
|
|
{
|
|
id: 4,
|
|
username: "ann",
|
|
running: true,
|
|
version: "3.8",
|
|
sessions: 3,
|
|
busy: 0,
|
|
outdated: false,
|
|
},
|
|
{ id: 5, username: "bob", running: false, busy: 0, outdated: false },
|
|
],
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
users.tmux.mockResolvedValue(DATA);
|
|
});
|
|
|
|
describe("MemberTmuxCard", () => {
|
|
it("renders nothing without one Linux user per account", async () => {
|
|
users.tmux.mockResolvedValue({ available: false, users: [] });
|
|
const { container } = render(<MemberTmuxCard isAdmin />);
|
|
await waitFor(() => expect(users.tmux).toHaveBeenCalled());
|
|
expect(container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it("lists running servers and marks the ones older than the installed tmux", async () => {
|
|
render(<MemberTmuxCard isAdmin={false} />);
|
|
expect(await screen.findByText("mia")).toBeInTheDocument();
|
|
expect(screen.queryByText("bob")).toBeNull();
|
|
expect(screen.getAllByText("older than installed")).toHaveLength(2);
|
|
expect(screen.getByText(/Installed: tmux 3\.8/)).toBeInTheDocument();
|
|
// An analyst reads only.
|
|
expect(screen.queryByRole("button", { name: /Restart/ })).toBeNull();
|
|
});
|
|
|
|
it("warns until tmux-member is installed", async () => {
|
|
users.tmux.mockResolvedValue({ ...DATA, matched: false });
|
|
render(<MemberTmuxCard isAdmin />);
|
|
expect(await screen.findByText(/install-system/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("restarts one member only with the admin's password", async () => {
|
|
users.tmuxRestart.mockResolvedValue({});
|
|
render(<MemberTmuxCard isAdmin />);
|
|
await userEvent.click(await screen.findByRole("button", { name: "Restart mia's tmux" }));
|
|
expect(screen.getByText(/Every pane of mia closes \(2 sessions\)/)).toBeInTheDocument();
|
|
const go = screen.getByRole("button", { name: "Restart" });
|
|
expect(go).toBeDisabled();
|
|
await userEvent.type(screen.getByLabelText("Your password"), "pw");
|
|
await userEvent.click(go);
|
|
await waitFor(() => expect(users.tmuxRestart).toHaveBeenCalledWith(2, "pw"));
|
|
});
|
|
|
|
it("restarts every idle outdated member at once, the working one left out", async () => {
|
|
users.tmuxRestartIdle.mockResolvedValue({ restarted: ["mia"], skipped: ["zed"], failed: [] });
|
|
render(<MemberTmuxCard isAdmin />);
|
|
await userEvent.click(await screen.findByRole("button", { name: "Restart 1 idle member" }));
|
|
expect(screen.getByText(/no agent working: mia\./)).toBeInTheDocument();
|
|
await userEvent.type(screen.getByLabelText("Your password"), "pw");
|
|
await userEvent.click(screen.getByRole("button", { name: "Restart" }));
|
|
await waitFor(() => expect(users.tmuxRestartIdle).toHaveBeenCalledWith("pw"));
|
|
});
|
|
});
|