b6b40b0c1b
/users becomes /admin (redirected), a tab shell over the existing Users view and a new Server tab backed by GET /api/host (server/lib/host-stats.js): hostname/OS/uptime, CPU and load, memory and swap, every real disk with HDD/SSD, NVIDIA GPUs, and each user's live share from two /proc samples — attributed by the member's Linux uid (S3) or, for host processes, by the inherited AGENTHUB_LANE_ID's workspace owner. Read-only, staff-only (/host in STAFF_ONLY_PREFIXES), comm names only. Members' tmux card moves to the Server tab.
172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
/**
|
|
* @file Administration.test.tsx
|
|
* @description The Administration page (`/admin`): local mode and a developer
|
|
* are sent home; staff land on the Users tab; the Server tab shows the
|
|
* machine's hardware, every disk, and each user's CPU/memory/home share from
|
|
* `GET /api/host`, with a user's busiest processes on click.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|
import type { AuthState, HostStats } from "../../lib/api";
|
|
|
|
let authState: AuthState = { mode: "local" };
|
|
vi.mock("../../components/AuthGate", () => ({
|
|
useAuth: () => ({ state: authState, logout: async () => {} }),
|
|
}));
|
|
|
|
const GB = 1024 ** 3;
|
|
const stats: HostStats = {
|
|
at: "2026-10-15T00:00:00Z",
|
|
hostname: "box",
|
|
os: { platform: "linux", release: "7.0.0", distro: "Ubuntu 26.04", arch: "x64" },
|
|
uptime: 90000,
|
|
cpu: { model: "i5-7500", cores: 4, load: [1, 0.5, 0.25] },
|
|
memory: { total: 16 * GB, available: 8 * GB, used: 8 * GB, swapTotal: 4 * GB, swapUsed: 0 },
|
|
disks: [
|
|
{
|
|
device: "/dev/sdb2",
|
|
mount: "/",
|
|
type: "ext4",
|
|
kind: "SSD",
|
|
total: 200 * GB,
|
|
used: 50 * GB,
|
|
free: 150 * GB,
|
|
},
|
|
{
|
|
device: "/dev/sda1",
|
|
mount: "/data",
|
|
type: "ext4",
|
|
kind: "HDD",
|
|
total: 500 * GB,
|
|
used: 0,
|
|
free: 500 * GB,
|
|
},
|
|
],
|
|
homesMount: "/",
|
|
gpus: [],
|
|
usage: {
|
|
cpu: 40,
|
|
sampleMs: 1000,
|
|
groups: [
|
|
{
|
|
key: "user:2",
|
|
kind: "user",
|
|
userId: 2,
|
|
username: "mia",
|
|
cpu: 25,
|
|
memory: 2 * GB,
|
|
memoryPercent: 12.5,
|
|
processes: 3,
|
|
top: [{ pid: 77, name: "claude", cpu: 20, memory: GB }],
|
|
},
|
|
{ key: "host", kind: "host", cpu: 5, memory: GB, memoryPercent: 6.3, processes: 90, top: [] },
|
|
],
|
|
},
|
|
};
|
|
|
|
const api = vi.hoisted(() => ({
|
|
host: { stats: vi.fn() },
|
|
users: {
|
|
list: vi.fn(),
|
|
audit: vi.fn(),
|
|
storage: vi.fn(),
|
|
tmux: vi.fn(async () => ({ available: false, users: [] })),
|
|
avatarUrl: () => null,
|
|
},
|
|
}));
|
|
vi.mock("../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
return {
|
|
...actual,
|
|
api: {
|
|
...api,
|
|
lanes: { list: vi.fn(async () => ({ lanes: [], counts: {} })) },
|
|
accountPool: { list: vi.fn(async () => ({ accounts: [], codex: [], blockPercent: 90 })) },
|
|
},
|
|
};
|
|
});
|
|
|
|
import { Administration } from "../Administration";
|
|
|
|
const renderAt = (url: string) =>
|
|
render(
|
|
<MemoryRouter initialEntries={[url]}>
|
|
<Routes>
|
|
<Route path="/admin" element={<Administration />} />
|
|
<Route path="/" element={<div data-testid="home" />} />
|
|
</Routes>
|
|
</MemoryRouter>
|
|
);
|
|
|
|
beforeEach(() => {
|
|
api.host.stats.mockResolvedValue(stats);
|
|
api.users.list.mockResolvedValue({
|
|
users: [
|
|
{
|
|
id: 1,
|
|
username: "boss",
|
|
role: "admin",
|
|
disabled: false,
|
|
workspaces: 0,
|
|
sessions: 0,
|
|
last7d: { cost: 0, tokens: 0 },
|
|
last30d: { cost: 0, tokens: 0 },
|
|
},
|
|
{
|
|
id: 2,
|
|
username: "mia",
|
|
role: "developer",
|
|
displayName: "Mia",
|
|
disabled: false,
|
|
workspaces: 1,
|
|
sessions: 2,
|
|
last7d: { cost: 0, tokens: 0 },
|
|
last30d: { cost: 0, tokens: 0 },
|
|
},
|
|
],
|
|
});
|
|
api.users.audit.mockResolvedValue({ entries: [] });
|
|
api.users.storage.mockResolvedValue({
|
|
storage: { 2: { bytes: 20 * GB, files: 10, partial: false, measuredAt: "x" } },
|
|
measuring: false,
|
|
});
|
|
});
|
|
|
|
describe("Administration", () => {
|
|
it("sends local mode and a developer home", async () => {
|
|
authState = { mode: "local" };
|
|
renderAt("/admin");
|
|
expect(await screen.findByTestId("home")).toBeTruthy();
|
|
authState = { mode: "server", user: { id: 2, username: "mia", role: "developer" } };
|
|
renderAt("/admin");
|
|
expect((await screen.findAllByTestId("home")).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("opens on the Users tab", async () => {
|
|
authState = { mode: "server", user: { id: 1, username: "boss", role: "admin" } };
|
|
renderAt("/admin");
|
|
expect(await screen.findByTestId("users-table")).toBeTruthy();
|
|
expect(screen.getByRole("tab", { name: /users/i }).getAttribute("aria-selected")).toBe("true");
|
|
});
|
|
|
|
it("shows hardware and each user's share on the Server tab", async () => {
|
|
authState = { mode: "server", user: { id: 1, username: "boss", role: "admin" } };
|
|
renderAt("/admin?tab=server");
|
|
const table = await screen.findByTestId("server-usage-table");
|
|
expect(await screen.findByText("box")).toBeTruthy();
|
|
expect(screen.getByTestId("server-disks-table").textContent).toContain("HDD");
|
|
const mia = await within(table).findByText("Mia");
|
|
const row = mia.closest("tr")!;
|
|
expect(row.textContent).toContain("25%");
|
|
expect(row.textContent).toContain("(13%)");
|
|
// 20 GB home on the 200 GB homes disk.
|
|
expect(row.textContent).toContain("10%");
|
|
expect(within(table).queryByText("claude")).toBeNull();
|
|
fireEvent.click(mia.closest("button")!);
|
|
expect(within(table).getByText("claude")).toBeTruthy();
|
|
});
|
|
});
|