Files
AgentHub/client/src/components/health/__tests__/HostHealth.test.tsx
T
nntrivi2001 1c6dfea5b5 feat(health): System Health in three groups, a score that sees lag; admin user detail
Dashboard → System Health is grouped into AgentHub, Machine and Agent
activity. The machine (GET /api/host, every signed-in user) is drawn by
components/health/HostHealth.tsx: host card beside the CPU/memory history,
then CPU (threads as compact cells + temperatures/GPUs), disks & network,
busiest processes, and per-user tiles. Concurrency bars each show their own
agent type on hover.

The Health Score (client/src/lib/healthScore.ts) is now 100 minus what each
check costs, between an amber and a red threshold: event-loop p99
(server/lib/loop-delay.js, shared with /api/metrics), Linux PSI pressure for
CPU, memory and disk I/O (/api/host `pressure`; load per core without PSI),
RAM, the database's disk, V8 heap, hooks silent while agents work, and agent
errors in the last 24 h. All-time success rate, API errors and the transcript
cache hit rate no longer count. /api/settings/info adds server.event_loop and
activity {last_event_at, working_agents, agents_24h}.

Administration: a user row opens its detail in place (GET
/api/users/:id/details — spend, account pool share, live panes, workspaces,
newest sessions); the server panel is gone, member tmux folded into the table;
the Sessions list shows each row's owner in server mode.
2026-10-27 16:39:38 +07:00

183 lines
6.0 KiB
TypeScript

/**
* @file HostHealth.test.tsx
* @description The machine on the Dashboard's System Health tab
* (`HostHealth.tsx`, `GET /api/host`), its whole machine group: the first card names the host, its OS,
* load and the AgentHub process with bars for CPU, memory, swap and heap; the
* rows under it show every CPU thread, temperatures, each disk with its I/O
* rate and each network interface, the CPU / memory history with its range
* switch (`GET /api/host/history`), and the busiest processes with who each
* counts for, the processes of no user in a short list of their own, and
* each user's live CPU / RAM as a tile (their 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 type { HostStats } from "../../../lib/api";
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(), history: vi.fn() },
me: { avatarUrl: () => null },
}));
vi.mock("../../../lib/api", async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
return { ...actual, api };
});
import { HostSections, useHostStats } from "../HostHealth";
function Tab() {
const host = useHostStats();
return (
<HostSections
host={host}
heap={{ heapUsed: GB / 4, heapTotal: GB / 2, heapLimit: GB, external: 0 }}
/>
);
}
const full: HostStats = {
...stats,
usage: {
...stats.usage!,
groups: stats.usage!.groups.map((g) => (g.kind === "user" ? { ...g, displayName: "Mia" } : g)),
},
bootTime: "2026-10-14T00:00:00Z",
cpu: { ...stats.cpu, physicalCores: 4, perCore: [10, 20, 30, 40] },
network: [{ name: "enp4s0", rx: 2048, tx: 1024 }],
diskIo: [{ name: "sda", read: 4096, write: 0 }],
temperatures: [{ chip: "coretemp", label: "Package id 0", celsius: 55 }],
topProcesses: [{ pid: 77, name: "claude", cpu: 20, memory: GB, group: "user:2" }],
process: { pid: 9, node: "v24.0.0", uptime: 3600, rss: GB, heapUsed: GB / 2 },
};
beforeEach(() => {
vi.clearAllMocks();
api.host.stats.mockResolvedValue(full);
api.host.history.mockResolvedValue({
range: "1h",
intervalMs: 30000,
running: true,
startedAt: "2026-10-15T00:00:00Z",
points: [
{
at: "2026-10-15T00:00:00Z",
cpu: 30,
memory: 50,
memoryUsed: 0,
swapUsed: 0,
load1: 1,
users: {},
},
{
at: "2026-10-15T00:00:30Z",
cpu: 40,
memory: 52,
memoryUsed: 0,
swapUsed: 0,
load1: 1,
users: {},
},
],
});
});
describe("HostHealth", () => {
it("names the machine and the AgentHub process, with bars", async () => {
render(<Tab />);
const card = await screen.findByTestId("server-facts");
expect(await within(card).findByText("box")).toBeTruthy();
expect(card.textContent).toContain("Ubuntu 26.04");
expect(card.textContent).toContain("1.0 GB");
expect(within(card).getByRole("meter", { name: "CPU" }).getAttribute("aria-valuenow")).toBe(
"40"
);
expect(within(card).getByRole("meter", { name: "Memory" }).getAttribute("aria-valuenow")).toBe(
"50"
);
expect(within(card).getByRole("meter", { name: "V8 Heap" })).toBeTruthy();
});
it("shows every thread, sensors, disks with I/O, network, history and processes", async () => {
render(<Tab />);
expect(await screen.findByTestId("server-history")).toBeTruthy();
expect(await screen.findByRole("img", { name: /CPU/ })).toBeTruthy();
expect(screen.getByTestId("server-cores").textContent).toContain("40%");
expect(screen.getByTestId("server-temperatures").textContent).toContain("55 °C");
const disks = screen.getByTestId("server-disk-io").textContent;
expect(disks).toContain("/data · HDD");
expect(disks).toContain("4.0 KB/s");
expect(screen.getByTestId("server-network").textContent).toContain("enp4s0");
const top = screen.getByTestId("server-top-processes");
expect(await within(top).findByText("Mia")).toBeTruthy();
const other = screen.getByTestId("server-usage-table");
expect(within(other).queryByText("Mia")).toBeNull();
expect(within(other).getByText(/^Host account/)).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "24 h" }));
expect(api.host.history).toHaveBeenLastCalledWith("24h");
});
it("shows each user's live CPU and RAM, their processes on click", async () => {
render(<Tab />);
const tile = await screen.findByTestId("server-user");
expect(tile.textContent).toContain("Mia");
expect(within(tile).getByRole("meter", { name: "CPU" }).getAttribute("aria-valuenow")).toBe(
"25"
);
expect(tile.textContent).toContain("2.0 GB");
expect(within(tile).queryByText("claude")).toBeNull();
fireEvent.click(within(tile).getByRole("button", { expanded: false }));
expect(within(tile).getByText("claude")).toBeTruthy();
});
});