Files
AgentHub/server/__tests__/workflow-tool-flow.test.js
T
nntrivi2001 198fbb7980 fix: event-loop freezes, plugin-install boot crash, tmux attach/scrollbar, Codex transcripts
- GET /api/workflows: one LEAD() window pass in a read worker (read-pool.js,
  workflow-stats.js); idx_events_session restored, idx_events_tool added
- compat-sqlite gains function/pluck/readonly so a plugin install without a
  better-sqlite3 build boots again; antigravity-usage uses db.js's Database
- install-hooks adds no checkout hooks while the agenthub plugin is enabled
  (events were doubled)
- terminal attach runs the live tmux server's own binary; the pane scrollbar
  gets a visible 2-column style, and claude panes start with
  CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 so tmux keeps history to scroll
- Codex sessions' conversation in GET /api/sessions/:id/transcript
- Codex/Antigravity sessions claimed by their workspace without hooks
- UI-audit follow-ups, docs and OpenAPI
2026-10-05 17:01:05 +07:00

82 lines
3.1 KiB
JavaScript

/**
* @file GET /api/workflows tool-to-tool transitions (`stats.topFlow`,
* `toolFlow.transitions`): each tool event pairs with the NEXT tool event of the
* same session, and the query stays linear in the session's event count — the
* old correlated `MIN(id)` subquery went quadratic once `idx_events_session`
* was dropped and froze the whole server on a large session.
* @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 fs = require("fs");
const os = require("os");
const path = require("path");
const TEST_DB = path.join(os.tmpdir(), `dashboard-toolflow-test-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
const { createApp, startServer } = require("../index");
const { db } = require("../db");
let server;
let BASE;
function addSession(id, tools) {
db.prepare("INSERT INTO sessions (id, status) VALUES (?, 'completed')").run(id);
const ins = db.prepare("INSERT INTO events (session_id, event_type, tool_name) VALUES (?, ?, ?)");
db.transaction(() => {
for (const t of tools) ins.run(id, t ? "PreToolUse" : "Stop", t);
})();
}
before(async () => {
// A: Read → (Stop, no tool) → Edit → Read ; B: Read → Edit
addSession("flow-a", ["Read", null, "Edit", "Read"]);
addSession("flow-b", ["Read", "Edit"]);
server = await startServer(createApp(), 0);
BASE = `http://127.0.0.1:${server.address().port}`;
});
after(() => {
server?.close();
try {
db.close();
} catch {}
for (const s of ["", "-wal", "-shm"]) fs.rmSync(TEST_DB + s, { force: true });
});
describe("GET /api/workflows tool transitions", () => {
it("pairs each tool event with the next tool event of its own session", async () => {
const body = await (await fetch(`${BASE}/api/workflows`)).json();
const flow = Object.fromEntries(
body.toolFlow.transitions.map((t) => [`${t.source}>${t.target}`, t.value])
);
// Read>Edit twice (A skips its Stop, B), Edit>Read once; nothing crosses sessions.
assert.deepEqual(flow, { "Read>Edit": 2, "Edit>Read": 1 });
assert.deepEqual(body.stats.topFlow, { source: "Read", target: "Edit", count: 2 });
});
it("?sections= computes only the named parts, and refuses names it does not know", async () => {
const body = await (await fetch(`${BASE}/api/workflows?sections=stats,toolFlow,bogus`)).json();
assert.deepEqual(Object.keys(body), ["stats", "toolFlow"]);
assert.equal(body.stats.topFlow.source, "Read");
const bad = await fetch(`${BASE}/api/workflows?sections=bogus`);
assert.equal(bad.status, 400);
assert.equal((await bad.json()).error.code, "INVALID_SECTIONS");
});
it("stays fast on one large session", async () => {
const tools = ["Read", "Edit", "Bash", "Grep"];
addSession(
"flow-big",
Array.from({ length: 20000 }, (_, i) => tools[i % 4])
);
const t0 = Date.now();
const res = await fetch(`${BASE}/api/workflows`);
assert.equal(res.status, 200);
await res.json();
assert.ok(Date.now() - t0 < 3000, `took ${Date.now() - t0} ms`);
});
});