from __future__ import annotations import json import subprocess import time import unittest from hub_case import HubCase from hub_support import HUB, NAV MODULE = HUB / "templates" / "resources.js" PAGE = HUB / "templates" / "page-resources.js" STYLE = HUB / "templates" / "style-resources.css" T = 1_760_000_000 FIXTURE = """ const asleep = {name: "0000:01:00.0", asleep: true}; const awake = (util, used) => ({name: "0000:01:00.0", util, vram_used: used, vram_total: 1000}); const unread = {name: "0000:01:00.0", util: null, vram_used: null, vram_total: null}; const row = (t, cpu, ram, waiting, gpu, disks) => ({t, cpu, ram, swap: 0, load: 1, disks, gpus: [gpu], queue: {waiting, active: 1}}); const root = [{mount: "/", used: 250, total: 1000}]; const samples = [ row(T, 20, 30, 0, asleep, root), row(T + 60, 40, 45, 2, asleep, root), row(T + 120, 45, 40, 3, awake(80, 500), [...root, {mount: "/mnt", used: 1, total: 4}]), row(T + 180, 70, 40, 3, awake(90, 600), root), row(T + 240, 30, 30, 1, awake(10, 100), root), row(T + 300, 35, 30, 0, unread, root), row(T + 5000, 10, 10, 1, asleep, root), row(T + 5060, 10, 10, 0, asleep, root), ]; const tokens = [ {t: T, agent: "Skills", model: "opus", input: 10, output: 20, cache_read: 300, cache_write: 40}, {t: T, agent: "Stage", model: "opus", input: 1, output: 2, cache_read: 3, cache_write: 4}, {t: T, agent: "Skills", model: "haiku", input: 5, output: 0, cache_read: 0, cache_write: 0}, {t: T + 3600, agent: "Stage", model: "opus", input: 500, output: 0, cache_read: 0, cache_write: 0}, {t: "x", agent: "Bad", model: "opus", input: 1, output: 0, cache_read: 0, cache_write: 0}, ]; """ PROBE = """ const R = require(process.argv[1]); const T = Number(process.argv[2]); @FIXTURE@ const lines = R.lines(samples); const pick = (list) => list.map((entry) => [entry.name, entry.kind, entry.points]); console.log(JSON.stringify({ zones: R.ZONES, disks: pick(lines.disks), host: pick(lines.host), gpu: pick(lines.gpu), segments: R.segments([[T, 1], [T + 60, null], [T + 120, 3], [T + 180, 4], [T + 4000, 5]]), waits: R.waits(samples), asleep: R.asleep(samples), unknown: R.unknown(samples), stack: R.stack(tokens), })); """.replace("@FIXTURE@", FIXTURE) PAGE_PROBE = """ const fs = require("fs"); const vm = require("vm"); const Resources = require(process.argv[1]); const code = fs.readFileSync(process.argv[2], "utf8"); const T = Number(process.argv[3]); @FIXTURE@ const make = (tag, attrs, children) => { const node = {tag, attrs: {}, children: [], textContent: "", isConnected: true, append(...items) { this.children.push(...items); }, replaceChildren(...items) { this.children = items; }, getBoundingClientRect() { return {width: 800, height: 120}; }}; for (const [key, value] of Object.entries(attrs)) { if (key !== "on" && value !== null && value !== undefined && value !== false) { node.attrs[key] = String(value); } } node.children = children.flat(Infinity).filter((child) => child !== null && child !== undefined && child !== false); return node; }; const h = (tag, attrs, ...children) => make(tag, attrs, children); const icon = (name) => make("svg", {class: "bi", "data-icon": name}, []); const fetched = []; const context = { Resources, h, s: h, icon, DAY: 86400, ui: {preset: 86400, from: null, to: null, asOf: null, offline: false}, iconBtn: (name, label, cls, run, hint) => make("button", {title: hint, "aria-label": label}, [icon(name)]), fixed: (value) => value.toFixed(1), dash: (value) => (value === null ? "–" : value), absolute: {format: () => "d"}, clock: {format: () => "c"}, stampOf: (t) => new Date(t * 1000), window: {setTimeout: (run) => run(), setInterval: () => 1, addEventListener: () => null}, document: {hidden: false}, windowQuery: () => "from=1", nowSeconds: () => 0, fetch: async (url) => { fetched.push(url); return {ok: true, json: async () => ({since: T - 600, until: T + 6000, samples, tokens})}; }, }; vm.runInNewContext(`${code}\\nscreen = resourcesScreen();`, context); const walk = (node, found) => { if (node && typeof node === "object") { found.push(node); node.children.forEach((child) => walk(child, found)); } return found; }; const has = (node, name) => (node.attrs.class || "").split(" ").includes(name); (async () => { for (let round = 0; round < 5; round += 1) { await new Promise((resolve) => setImmediate(resolve)); } const all = walk(context.screen, []); const charts = all.filter((node) => has(node, "res-chart")); const marks = (kind) => all.filter((node) => has(node, "res-mark") && has(node, kind)).map((node) => [Number(node.attrs["data-from"]), Number(node.attrs["data-to"])]); console.log(JSON.stringify({ fetched, count: all.find((node) => node.attrs["aria-label"] === "Samples").textContent, charts: charts.map((chart) => [chart.attrs["data-panel"], chart.attrs["data-from"], chart.attrs["data-to"]]), bands: charts.map((chart) => walk(chart, []).filter((node) => has(node, "band")).map((node) => node.attrs["data-zone"])), lines: [...new Set(all.filter((node) => has(node, "res-line")).map((node) => `${node.attrs["data-series"]}|${node.attrs["data-kind"]}`))], waits: marks("wait"), asleep: marks("asleep"), unknown: marks("unknown"), words: all.filter((node) => has(node, "res-mark") && has(node, "unknown")).map((node) => node.children[0].children.join("")), bars: all.filter((node) => has(node, "res-bar")).map((node) => [node.attrs["data-agent"], Number(node.attrs["data-t"])]), legends: all.filter((node) => has(node, "res-key")).map((node) => node.children.join("")), titles: all.filter((node) => has(node, "res-title")).map((node) => [node.attrs.title, node.attrs["aria-label"], node.children[0].attrs["data-icon"]]), })); })(); """.replace("@FIXTURE@", FIXTURE) class TestResourcesModule(unittest.TestCase): @classmethod def setUpClass(cls): done = subprocess.run(["node", "-e", PROBE, str(MODULE), str(T)], capture_output=True, text=True, timeout=60, check=True) cls.data = json.loads(done.stdout) def test_bands_mark_idle_below_thirty_and_saturated_above_ninety_percent(self): self.assertEqual(self.data["zones"], [{"kind": "idle", "from": 0, "to": 30}, {"kind": "saturated", "from": 90, "to": 100}]) def test_disks_cpu_ram_swap_gpu_and_vram_become_percent_lines_on_one_time_axis(self): times = [T, T + 60, T + 120, T + 180, T + 240, T + 300, T + 5000, T + 5060] self.assertEqual(self.data["disks"], [["/", "disk", [[t, 25] for t in times]], ["/mnt", "disk", [[T + 120, 25]]]]) self.assertEqual([name for name, _, _ in self.data["host"]], ["CPU", "RAM", "Swap"]) self.assertEqual(self.data["host"][0][2], [[t, cpu] for t, cpu in zip(times, (20, 40, 45, 70, 30, 35, 10, 10), strict=True)]) gpu, vram = self.data["gpu"] self.assertEqual((gpu[0], gpu[1], vram[0], vram[1]), ("0000:01:00.0", "util", "0000:01:00.0", "vram")) self.assertEqual(gpu[2], [[T, None], [T + 60, None], [T + 120, 80], [T + 180, 90], [T + 240, 10], [T + 300, None], [T + 5000, None], [T + 5060, None]]) self.assertEqual(vram[2][2:5], [[T + 120, 50], [T + 180, 60], [T + 240, 10]]) def test_a_line_breaks_at_a_missing_value_and_at_a_gap_of_more_than_half_an_hour(self): self.assertEqual(self.data["segments"], [[[T, 1]], [[T + 120, 3], [T + 180, 4]], [[T + 4000, 5]]]) def test_a_marker_spans_rows_where_jobs_waited_while_cpu_and_ram_stayed_under_half(self): self.assertEqual(self.data["waits"], [{"from": T + 60, "to": T + 180, "peak": 3}, {"from": T + 240, "to": T + 300, "peak": 1}, {"from": T + 5000, "to": T + 5060, "peak": 1}]) def test_an_asleep_gpu_gets_its_own_spans(self): self.assertEqual(self.data["asleep"], [{"name": "0000:01:00.0", "spans": [{"from": T, "to": T + 120, "peak": 1}, {"from": T + 5000, "to": T + 5120, "peak": 1}]}]) def test_an_awake_gpu_without_a_reading_gets_unknown_spans_apart_from_asleep_ones(self): self.assertEqual(self.data["unknown"], [{"name": "0000:01:00.0", "spans": [{"from": T + 300, "to": T + 360, "peak": 1}]}]) def test_tokens_stack_per_hour_with_the_biggest_agent_at_the_bottom(self): stack = self.data["stack"] self.assertEqual((stack["agents"], stack["top"]), (["Stage", "Skills"], 500)) first, second = stack["bars"] self.assertEqual((first["t"], first["total"]), (T, 385)) self.assertEqual([(part["agent"], part["from"], part["to"], part["models"]) for part in first["parts"]], [("Stage", 0, 10, ["opus"]), ("Skills", 10, 385, ["opus", "haiku"])]) self.assertEqual({key: first["parts"][1][key] for key in ("input", "output", "cache_read", "cache_write")}, {"input": 15, "output": 20, "cache_read": 300, "cache_write": 40}) self.assertEqual((second["t"], [(part["agent"], part["to"]) for part in second["parts"]]), (T + 3600, [("Stage", 500)])) class TestResourcesPage(unittest.TestCase): def test_the_view_draws_four_panels_on_one_time_axis_from_the_api_rows_alone(self): done = subprocess.run(["node", "-e", PAGE_PROBE, str(MODULE), str(PAGE), str(T)], capture_output=True, text=True, timeout=60, check=True) found = json.loads(done.stdout) self.assertEqual((found["fetched"], found["count"]), (["/api/resources?from=1"], "8")) axis = [str(T - 600), str(T + 6000)] self.assertEqual(found["charts"], [[panel, *axis] for panel in ("disks", "host", "gpu", "tokens")]) self.assertEqual(found["bands"], [["idle", "saturated"]] * 3 + [[]]) self.assertEqual(found["lines"], ["/|disk", "/mnt|disk", "CPU|host", "RAM|host", "Swap|host", "0000:01:00.0|util", "0000:01:00.0|vram"]) self.assertEqual(found["waits"], [[T + 60, T + 180], [T + 240, T + 300], [T + 5000, T + 5060]]) self.assertEqual(found["asleep"], [[T, T + 120], [T + 5000, T + 5120]]) self.assertEqual((found["unknown"], found["words"]), ([[T + 300, T + 360]], ["0000:01:00.0 unknown: no reading"])) self.assertEqual(found["bars"], [["Stage", T], ["Skills", T], ["Stage", T + 3600]]) self.assertEqual(found["legends"], ["/ 25 %", "/mnt 25 %", "CPU 10 %", "RAM 10 %", "Swap 0 %", "0000:01:00.0 10 %", "0000:01:00.0 VRAM 10 %", "Stage 510", "Skills 375"]) names = [("Disks", "bi-hdd-stack"), ("CPU, RAM and swap", "bi-cpu"), ("GPU and VRAM", "bi-gpu-card"), ("Tokens per agent", "bi-coin")] self.assertEqual(found["titles"], [[name, name, icon] for name, icon in names]) class TestHubResources(HubCase): def write(self, name: str, rows: list[dict], tail: str = "") -> None: folder = self.kept / "resources" folder.mkdir(exist_ok=True) (folder / name).write_text(self.jsonl(rows) + tail, encoding="utf-8") def fetch(self, query: str) -> tuple[int, bytes]: status, _, body = self.get(f"/api/resources?{query}") return status, body def test_the_view_reads_only_the_sampler_files_within_the_window(self): samples = [{"t": T + step, "cpu": step / 60, "ram": 1, "swap": 0, "load": 0, "disks": [], "gpus": [], "queue": {"waiting": 0, "active": 0}} for step in (0, 60, 120, 180)] tokens = [{"t": T - 3600, "agent": "Skills", "model": "opus", "input": 1, "output": 0, "cache_read": 0, "cache_write": 0}, {"t": T + 3600, "agent": "Skills", "model": "opus", "input": 2, "output": 0, "cache_read": 0, "cache_write": 0}] self.write("samples.jsonl", samples, "{broken\n[1]\n") self.write("tokens.jsonl", tokens) status, body = self.fetch(f"from={T + 60}&to={T + 120}") self.assertEqual(status, 200, body) self.assertEqual(json.loads(body), {"since": T + 60, "until": T + 120, "samples": samples[1:3], "tokens": []}) status, body = self.fetch(f"from={T - 1800}&to={T + 3600}") self.assertEqual(json.loads(body)["tokens"], tokens) self.assertEqual(json.loads(body)["samples"], samples) def test_missing_files_answer_empty_rows_and_a_reversed_window_is_refused(self): status, body = self.fetch("") self.assertEqual(status, 200, body) found = json.loads(body) self.assertEqual((found["samples"], found["tokens"]), ([], [])) self.assertAlmostEqual(found["until"] - found["since"], 86400) self.assertLess(abs(found["until"] - time.time()), 30) self.assertEqual(self.fetch(f"from={T + 10}&to={T}")[0], 400) self.assertEqual(self.fetch("from=x")[0], 400) def test_the_page_carries_the_resources_tab_module_view_and_icons(self): html = self.index() self.assertIn(MODULE.read_text(encoding="utf-8").strip(), html) self.assertIn(PAGE.read_text(encoding="utf-8").strip(), html) self.assertIn(STYLE.read_text(encoding="utf-8").strip(), html) for name in ("speedometer2", "hdd-stack", "cpu", "gpu-card", "coin", "hourglass-split", "moon", "question-circle"): with self.subTest(icon=name): self.assertIn(f'