03f1673fce
- Workspace, SettingsHub, SessionDetail and the Workflow sub-tab are lazy (main bundle 2,166 kB -> ~920 kB), wrapped in RouteErrorBoundary: a stale chunk after a self-update reloads once (vite:preloadError too), any other error shows a Reload panel instead of a blank page. - Live refresh: lib/throttle.ts + hooks/useLiveRefresh.ts replace trailing debounces that froze under a steady event stream; refresh skips hidden tabs and refetches after a WebSocket reconnect. Monitor loads only while shown, with one batched GET /api/agents?session_ids= instead of N+1; a failing endpoint is an inline error, not a blank Dashboard. - NeedsYouStrip on Monitor and above Analytics. Tabs, filters, page, sort and search live in the URL (Dashboard, Sessions, Activity, SessionDetail). - Sessions: tool / workspace / date-range filters, CSV/JSON export (formula-neutralised), focusable row links, Open in workspace (the dead /run links are gone), stale responses dropped, error + Retry. - Fixes: "NaNs" durations, 30/7-day charts skipping idle days, weekday labels west of UTC (now averages), cache hit rate denominator, billable tokens vs cache reads, exact tool-call counts, "To" date = end of day, invalid ?from= no longer throws, recent list no longer shrinks to 15, stable message keys, transcript switch races. - Light-theme-unreadable bg-black code panels and dark-only DAG fills use tokens; status configs and chart colours use theme tokens; Health tab and other literals translated. - Server (additive): GET /api/agents?session_ids=, GET /api/sessions awaiting=1&from=&to=, stats tool_call_count/distinct_tool_count, event rows carry session/agent labels.
129 lines
4.5 KiB
JavaScript
129 lines
4.5 KiB
JavaScript
/**
|
|
* @file ui-audit-routes.test.js
|
|
* @description Contract tests for the read-route additions the dashboard UI audit
|
|
* needed: batched agents by `session_ids`, exact tool-call totals on session
|
|
* stats, session/agent labels joined onto the events list, and the
|
|
* `awaiting`/`from`/`to` filters on the sessions list.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { describe, it, before, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
|
|
const TEST_DB = path.join(os.tmpdir(), `ui-audit-test-${Date.now()}-${process.pid}.db`);
|
|
process.env.DASHBOARD_DB_PATH = TEST_DB;
|
|
process.env.DASHBOARD_LIVENESS_PROBE = "0";
|
|
|
|
const { createApp, startServer } = require("../index");
|
|
const { db } = require("../db");
|
|
|
|
let server;
|
|
let BASE;
|
|
|
|
async function get(p) {
|
|
const res = await fetch(BASE + p);
|
|
return { status: res.status, body: await res.json() };
|
|
}
|
|
|
|
before(async () => {
|
|
server = await startServer(createApp(), 0);
|
|
BASE = `http://127.0.0.1:${server.address().port}`;
|
|
const s = db.prepare(
|
|
"INSERT INTO sessions (id, name, status, cwd, started_at, awaiting_input_since) VALUES (?, ?, ?, ?, ?, ?)"
|
|
);
|
|
s.run("ua-1", "First", "active", "/tmp/ua", "2026-01-10T10:00:00.000Z", "2026-01-10T10:05:00Z");
|
|
s.run("ua-2", "Second", "completed", "/tmp/ua", "2026-02-10T10:00:00.000Z", null);
|
|
s.run("ua-3", "Third", "active", "/tmp/ua", "2026-03-10T10:00:00.000Z", null);
|
|
const a = db.prepare(
|
|
"INSERT INTO agents (id, session_id, name, type, subagent_type) VALUES (?, ?, ?, ?, ?)"
|
|
);
|
|
a.run("ua-1-main", "ua-1", "Main", "main", null);
|
|
a.run("ua-2-main", "ua-2", "Main", "main", null);
|
|
a.run("ua-2-sub", "ua-2", "Explorer", "subagent", "Explore");
|
|
a.run("ua-3-main", "ua-3", "Main", "main", null);
|
|
const e = db.prepare(
|
|
"INSERT INTO events (session_id, agent_id, event_type, tool_name) VALUES (?, ?, ?, ?)"
|
|
);
|
|
// ua-1: 20 distinct tools, one PreToolUse + one PostToolUse each.
|
|
for (let i = 0; i < 20; i++) {
|
|
e.run("ua-1", "ua-1-main", "PreToolUse", `Tool${i}`);
|
|
e.run("ua-1", "ua-1-main", "PostToolUse", `Tool${i}`);
|
|
}
|
|
// ua-2: imported shape, PostToolUse only.
|
|
e.run("ua-2", "ua-2-sub", "PostToolUse", "Read");
|
|
e.run("ua-2", "ua-2-sub", "PostToolUse", "Read");
|
|
});
|
|
|
|
after(() => {
|
|
if (server) server.close();
|
|
if (db) db.close();
|
|
for (const f of [TEST_DB, TEST_DB + "-wal", TEST_DB + "-shm"]) {
|
|
try {
|
|
fs.unlinkSync(f);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
});
|
|
|
|
describe("GET /api/agents?session_ids=", () => {
|
|
it("returns every agent of the named sessions in one call", async () => {
|
|
const { status, body } = await get("/api/agents?session_ids=ua-1,ua-2");
|
|
assert.equal(status, 200);
|
|
assert.deepEqual(body.agents.map((a) => a.id).sort(), ["ua-1-main", "ua-2-main", "ua-2-sub"]);
|
|
});
|
|
it("an empty list returns no agents", async () => {
|
|
const { body } = await get("/api/agents?session_ids=");
|
|
assert.deepEqual(body.agents, []);
|
|
});
|
|
});
|
|
|
|
describe("GET /api/sessions/:id/stats tool totals", () => {
|
|
it("counts every PreToolUse, not the top-15 list", async () => {
|
|
const { body } = await get("/api/sessions/ua-1/stats");
|
|
assert.equal(body.tools_used.length, 15);
|
|
assert.equal(body.tool_call_count, 20);
|
|
assert.equal(body.distinct_tool_count, 20);
|
|
});
|
|
it("falls back to PostToolUse when a session has no PreToolUse", async () => {
|
|
const { body } = await get("/api/sessions/ua-2/stats");
|
|
assert.equal(body.tool_call_count, 2);
|
|
assert.equal(body.distinct_tool_count, 1);
|
|
});
|
|
});
|
|
|
|
describe("GET /api/events labels", () => {
|
|
it("joins session name/cwd and agent name/type onto each row", async () => {
|
|
const { body } = await get("/api/events?session_id=ua-2");
|
|
assert.equal(body.total, 2);
|
|
const ev = body.events[0];
|
|
assert.equal(ev.session_name, "Second");
|
|
assert.equal(ev.session_cwd, "/tmp/ua");
|
|
assert.equal(ev.agent_name, "Explorer");
|
|
assert.equal(ev.agent_subagent_type, "Explore");
|
|
});
|
|
});
|
|
|
|
describe("GET /api/sessions filters", () => {
|
|
it("awaiting=1 keeps only active sessions waiting on the user", async () => {
|
|
const { body } = await get("/api/sessions?awaiting=1&q=ua-");
|
|
assert.deepEqual(
|
|
body.sessions.map((s) => s.id),
|
|
["ua-1"]
|
|
);
|
|
assert.equal(body.total, 1);
|
|
});
|
|
it("from/to bound started_at", async () => {
|
|
const { body } = await get(
|
|
"/api/sessions?q=ua-&from=2026-02-01T00:00:00Z&to=2026-02-28T23:59:59Z"
|
|
);
|
|
assert.deepEqual(
|
|
body.sessions.map((s) => s.id),
|
|
["ua-2"]
|
|
);
|
|
});
|
|
});
|