/** * @file host-stats.test.js * @description Tests for server/lib/host-stats.js — the Server tab's hardware * snapshot and per-user CPU/memory attribution (`GET /api/host`), the * /proc and /sys parsers behind its per-core, network, disk I/O and * temperature figures, the PSI pressure parser, and the in-memory history (`GET /api/host/history`). * @author Nguyễn Ngọc Trí Vĩ */ const { test } = require("node:test"); const assert = require("node:assert/strict"); const { hostStats, hostHistory, mountOf, parseCpuStat, parsePressure, parseNetDev, parseDiskstats, physicalCores, temperatures, downsample, __setDeps, __setHistory, } = require("../lib/host-stats"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const linux = process.platform === "linux"; test("mountOf picks the longest mount point holding the path", () => { const list = [{ mount: "/" }, { mount: "/srv" }, { mount: "/srv/users" }]; assert.equal(mountOf(list, "/srv/users/alice"), "/srv/users"); assert.equal(mountOf(list, "/srv/usersx"), "/srv"); assert.equal(mountOf(list, "/home/x"), "/"); assert.equal(mountOf(list, null), null); }); test("hostStats reports hardware and attributes processes by uid", { skip: !linux }, async () => { const me = process.getuid(); __setDeps({ users: () => [ { id: 7, username: "zed" }, { id: 8, username: "idle" }, ], memberNameForUid: (uid) => (uid === me ? "zed" : null), laneOwner: () => null, homesRoot: () => "/", wait: () => new Promise((r) => setTimeout(r, 20)), }); const r = await hostStats(); assert.ok(r.cpu.cores > 0); assert.ok(r.memory.total > 0 && r.memory.used >= 0); assert.ok(Array.isArray(r.disks)); const zed = r.usage.groups.find((g) => g.key === "user:7"); assert.ok(zed.processes > 0, "this test's own process belongs to zed"); assert.ok(zed.memory > 0); assert.ok(zed.top.every((t) => typeof t.name === "string" && !("argv" in t))); const idle = r.usage.groups.find((g) => g.key === "user:8"); assert.deepEqual([idle.cpu, idle.memory, idle.processes], [0, 0, 0]); assert.equal( r.usage.groups.find((g) => g.key === "host"), undefined ); }); test( "a host process with a workspace's AGENTHUB_LANE_ID counts for its owner", { skip: !linux }, async () => { const { spawn } = require("node:child_process"); const child = spawn("sleep", ["5"], { env: { ...process.env, AGENTHUB_LANE_ID: "4242" } }); try { __setDeps({ users: () => [{ id: 3, username: "own" }], memberNameForUid: () => null, laneOwner: (id) => (id === 4242 ? 3 : null), wait: () => new Promise((r) => setTimeout(r, 20)), }); const r = await hostStats(); const own = r.usage.groups.find((g) => g.key === "user:3"); assert.ok(own.top.some((t) => t.pid === child.pid)); } finally { child.kill(); } } ); test("parsePressure reads some/full averages and refuses a missing file", () => { assert.deepEqual( parsePressure( "some avg10=12.50 avg60=3.00 avg300=1.00 total=1\nfull avg10=4.25 avg60=1.00 avg300=0.50 total=1\n" ), { some10: 12.5, some60: 3, full10: 4.25 } ); // The cpu file of an older kernel has no `full` line. assert.deepEqual(parsePressure("some avg10=1.00 avg60=2.00 avg300=0.00 total=1\n"), { some10: 1, some60: 2, full10: 0, }); assert.equal(parsePressure(null), null); assert.equal(parsePressure("garbage"), null); }); test("parseCpuStat reads the machine line and every core", () => { const r = parseCpuStat( "cpu 10 0 10 70 10 0 0 0 0 0\ncpu0 5 0 5 35 5 0 0 0\ncpu1 5 0 5 35 5 0 0 0\nintr 1 2\n" ); assert.deepEqual(r.all, { total: 100, idle: 80 }); assert.equal(r.cores.length, 2); assert.deepEqual(r.cores[1], { total: 50, idle: 40 }); assert.deepEqual(parseCpuStat(""), { all: null, cores: [] }); }); test("parseNetDev skips lo and reads rx/tx bytes", () => { const text = [ "Inter-| Receive | Transmit", " face |bytes packets|bytes packets", " lo: 100 1 0 0 0 0 0 0 100 1 0 0 0 0 0 0", "enp4s0: 5000 10 0 0 0 0 0 0 2000 6 0 0 0 0 0 0", ].join("\n"); const m = parseNetDev(text); assert.equal(m.has("lo"), false); assert.deepEqual(m.get("enp4s0"), { rx: 5000, tx: 2000 }); }); test("parseDiskstats keeps whole disks and converts sectors to bytes", () => { const text = [ " 7 0 loop0 14 0 34 12 0 0 0 0 0 11 12", " 8 0 sda 100 0 2048 50 20 0 1024 9 0 30 40", " 8 1 sda1 90 0 2000 40 20 0 1000 9 0 30 40", ].join("\n"); const m = parseDiskstats(text, (n) => n === "sda"); assert.deepEqual([...m.keys()], ["sda"]); assert.deepEqual(m.get("sda"), { read: 2048 * 512, write: 1024 * 512 }); }); test("physicalCores counts distinct physical/core id pairs", () => { const info = [0, 1].flatMap((core) => [0, 1].map((ht) => `processor\t: ${core * 2 + ht}\nphysical id\t: 0\ncore id\t\t: ${core}\n`) ); assert.equal(physicalCores(info.join("\n")), 2); assert.equal(physicalCores(""), null); }); test("temperatures reads hwmon inputs with their labels", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "hwmon-")); try { const chip = path.join(root, "hwmon0"); fs.mkdirSync(chip); fs.writeFileSync(path.join(chip, "name"), "coretemp\n"); fs.writeFileSync(path.join(chip, "temp1_input"), "45500\n"); fs.writeFileSync(path.join(chip, "temp1_label"), "Package id 0\n"); fs.writeFileSync(path.join(chip, "temp2_input"), "38000\n"); assert.deepEqual(await temperatures(root), [ { chip: "coretemp", label: "Package id 0", celsius: 45.5 }, { chip: "coretemp", label: "temp2", celsius: 38 }, ]); assert.deepEqual(await temperatures(path.join(root, "missing")), []); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test("hostHistory filters by range and downsample averages buckets", () => { const now = Date.now(); const pt = (minsAgo, cpu) => ({ at: new Date(now - minsAgo * 60e3).toISOString(), cpu, memory: 50, memoryUsed: 100, swapUsed: 0, load1: 1, users: { 3: { cpu, memory: 10 } }, }); __setHistory([pt(300, 90), pt(30, 10), pt(1, 20)]); const h1 = hostHistory("1h"); assert.equal(h1.range, "1h"); assert.deepEqual( h1.points.map((p) => p.cpu), [10, 20] ); assert.equal(hostHistory("6h").points.length, 3); assert.equal(hostHistory("bogus").range, "1h"); const d = downsample([pt(3, 10), pt(2, 20), pt(1, 30), pt(0, 40)], 2); assert.deepEqual( d.map((p) => p.cpu), [15, 35] ); assert.equal(d[0].users[3].cpu, 15); __setHistory([]); }); test( "hostStats adds per-core, network, disk I/O, top processes and process facts", { skip: !linux }, async () => { __setDeps({ users: () => [], memberNameForUid: () => null, laneOwner: () => null, homesRoot: () => "/", wait: () => new Promise((r) => setTimeout(r, 20)), }); const r = await hostStats(); assert.equal(r.cpu.perCore.length, r.cpu.cores); assert.ok(Array.isArray(r.network) && Array.isArray(r.diskIo)); assert.ok(r.network.every((n) => n.name !== "lo")); assert.ok(r.topProcesses.length > 0 && r.topProcesses.length <= 10); assert.ok(r.topProcesses.every((p) => typeof p.group === "string" && !("argv" in p))); assert.equal(r.process.pid, process.pid); assert.ok(r.process.rss > 0); assert.ok(typeof r.bootTime === "string"); } );