198fbb7980
- 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
82 lines
3.1 KiB
JavaScript
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`);
|
|
});
|
|
});
|