Files
AgentHub/server/__tests__/pty-attach.test.js
T
nntrivi2001 ca821008e2 perf: v2.5.10 — ready for 10+ concurrent users, leaner locally
Measured on a copy of a real 1.2 GB / 253K-event database, 10 simulated
browsers + 10 hooks/s for 90 s: event-loop delay p99 1239 ms → 32 ms (max
2.0 s → 94 ms), 266 failed requests → 0, 74 → 111 requests/s served, server
RSS 633 → 346 MB. Entry bundle 975 KB raw → 394 KB (106 KB brotli).

Server
- /api/analytics: the events-table scans run in the read worker
  (scoped-stats eventAggregates, read-pool job analyticsEvents); runRead takes
  maxAgeMs — analytics 10 s, /api/workflows 30 s — keyed by the args
- /api/workflows/skill-usage: +event_type and a tool_name range instead of
  LIKE, so it uses idx_events_tool (784 ms → a few ms)
- idx_events_session_type_tool (session_id, event_type, tool_name,
  created_at) replaces idx_events_session_type: covering reads for session
  stats, the hook dedupes and the import resume point (1.2 s → 20 ms); 64 MB
  cache_size and 256 MB mmap_size on both connections
- WAL: the server sets wal_autocheckpoint = 0 and checkpoints PASSIVE every
  DASHBOARD_WAL_CHECKPOINT_MS (5 s) from the read worker's own writable
  connection; a failed checkpoint restores the automatic one (hook COMMIT p95
  109 ms → 15 ms at 100 hooks/s)
- tmux: one list-sessions answers a whole GET /api/lanes and the heal beat
  (liveSessions), shared for 1 s and cleared by this process's new/kill —
  GET /api/run's sync spawns had blocked 60 s of a 90 s run; the heal beat
  skips tmux when nothing runs; attach options chained into 2 spawns
- hooks: TurnDuration/APIError deduped against one read per hook instead of a
  query per transcript entry (23 ms/hook on a 763-turn session); one lane
  scan per hook; hourly upserts skip rows that did not grow; frames are sent
  after the transaction commits (and after the pane binding); new_event
  carries its row id; a split pane's (slot 1–9) hooks no longer rebind the
  lane's session_id
- watchdog liveness probe runs ps asynchronously; transcript newline scan via
  indexOf; startup subagent backfill fingerprints the files it already checked
- pty attach: backpressure (pause past 1 MB unsent, resume under 256 KB)
- static: hashed assets served from their pre-compressed .br/.gz sibling

Client
- hidden tabs neither refetch on WS events nor poll (useLiveRefresh, panes,
  lane cards, git header, changes panel, settings); no per-tab hourly
  /updates/check (the server already checks)
- refetch storms throttled (workspace tree/session list 3 s, /api/run only on
  a run-key change, notification pane lookup only when shown); WS reconnect
  jitter; terminal retry backoff 1.5 → 30 s, stops on 1008
- heavier throttles for aggregates (15 s) and detail views; Monitor keepIfSame;
  GET in-flight dedupe in request()
- lazy Dashboard/History, xterm (with the first terminal), lane modals, Sagi
  (only while enabled); only the active locale loads; Workspace chunk
  prefetched on a /workspace deep link
- Sagi: no work while disabled, pupil via CSS variables, memo avatar
- build: .br/.gz siblings, sourcemap hidden; service worker no longer caches
  /assets/ (cache v3 drops the old one)

Tests: scripts/test-server.js drops the GIT_DIR-family variables a hook
exports, so a suite run from a linked worktree's pre-commit can no longer
commit into the real repository or set core.bare.

Docs: ARCHITECTURE, DATABASE, API, DEPLOYMENT, client README,
docs/plans/perf/README.md (audit, checklist, results).
2026-10-07 13:24:15 +07:00

192 lines
6.5 KiB
JavaScript

/**
* @file pty-attach.test.js
* @description Unit tests for the PTY-attach helper's runId validation and
* data/control framing, using a fake node-pty implementation (no real tmux
* or PTY spawned).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, beforeEach, afterEach } = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const ptyAttach = require("../lib/pty-attach");
const tmux = require("../lib/tmux");
function makeFakePty() {
const emitter = new EventEmitter();
const writes = [];
const pty = {
onData: (fn) => emitter.on("data", fn),
onExit: (fn) => emitter.on("exit", fn),
write: (d) => writes.push(d),
resize: (cols, rows) => writes.push({ resize: [cols, rows] }),
kill: () => emitter.emit("exit", { exitCode: 0 }),
__emitter: emitter,
__writes: writes,
};
return pty;
}
function makeFakeWs() {
const emitter = new EventEmitter();
const sent = [];
return {
on: (evt, fn) => emitter.on(evt, fn),
send: (data, opts) => sent.push({ data, binary: !!(opts && opts.binary) }),
__emitter: emitter,
__sent: sent,
};
}
describe("pty-attach", () => {
let fakePty;
beforeEach(() => {
fakePty = makeFakePty();
ptyAttach.__setSpawnImpl(() => fakePty);
tmux.__setExecImpl(() => "");
});
afterEach(() => {
tmux.__reset();
});
it("rejects a runId that doesn't match agenthub-lane-<digits>", () => {
assert.throws(() => ptyAttach.validateRunId("../../etc/passwd"), /EBADRUNID/);
assert.throws(() => ptyAttach.validateRunId("some-other-session"), /EBADRUNID/);
});
it("accepts a well-formed runId", () => {
assert.doesNotThrow(() => ptyAttach.validateRunId("agenthub-lane-42"));
assert.doesNotThrow(() => ptyAttach.validateRunId("agenthub-lane-42-3"));
assert.doesNotThrow(() => ptyAttach.validateRunId("agenthub-lane-42-9"));
assert.throws(() => ptyAttach.validateRunId("agenthub-lane-42-10"), /EBADRUNID/);
assert.throws(() => ptyAttach.validateRunId("agenthub-lane-42-0"), /EBADRUNID/);
});
it("turns tmux mouse mode on for the session it attaches to", () => {
const calls = [];
tmux.__setExecImpl((args) => {
calls.push(args);
return "";
});
ptyAttach.attach(makeFakeWs(), "agenthub-lane-1", { cols: 80, rows: 24 });
assert.deepEqual(calls[0].slice(0, 5), [
"set-option",
"-t",
"=agenthub-lane-1:",
"mouse",
"on",
]);
});
it("attaches with the RUNNING tmux server's own binary, not the one on PATH", () => {
// A tmux upgraded while its server runs leaves a newer client on PATH that
// the old server answers with "open terminal failed: not a terminal".
tmux.__setExecImpl((args) => (args[0] === "display-message" ? `${process.pid}\n` : ""));
let file;
ptyAttach.__setSpawnImpl((f) => {
file = f;
return fakePty;
});
ptyAttach.attach(makeFakeWs(), "agenthub-lane-1", { cols: 80, rows: 24 });
assert.equal(file, process.platform === "linux" ? `/proc/${process.pid}/exe` : "tmux");
});
it("falls back to tmux on PATH when the server's pid cannot be read", () => {
tmux.__setExecImpl((args) => {
if (args[0] === "display-message") throw new Error("no server running");
return "";
});
let file;
ptyAttach.__setSpawnImpl((f) => {
file = f;
return fakePty;
});
ptyAttach.attach(makeFakeWs(), "agenthub-lane-1", { cols: 80, rows: 24 });
assert.equal(file, "tmux");
});
it("still attaches even if the mouse-mode set-option call fails", () => {
tmux.__setExecImpl(() => {
throw new Error("no such session: agenthub-lane-1");
});
assert.doesNotThrow(() =>
ptyAttach.attach(makeFakeWs(), "agenthub-lane-1", { cols: 80, rows: 24 })
);
});
it("wires PTY data to binary WS frames and WS binary frames to PTY writes", () => {
const ws = makeFakeWs();
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
fakePty.__emitter.emit("data", "hello from claude");
assert.equal(ws.__sent.length, 1);
assert.equal(ws.__sent[0].data, "hello from claude");
assert.equal(ws.__sent[0].binary, true);
ws.__emitter.emit("message", Buffer.from("typed text"), { binary: true });
assert.deepEqual(fakePty.__writes[0], "typed text");
});
it("routes a JSON text frame with type resize to pty.resize", () => {
const ws = makeFakeWs();
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
ws.__emitter.emit(
"message",
Buffer.from(JSON.stringify({ type: "resize", cols: 100, rows: 40 })),
{
binary: false,
}
);
assert.deepEqual(fakePty.__writes[0], { resize: [100, 40] });
});
it("forwards a plain text WS frame (keystrokes) to pty.write", () => {
// xterm.js's onData hands the browser a plain string, and
// WebSocket.send(string) always emits a TEXT frame — so every keystroke
// arrives here as non-binary. This must reach the pty, not be dropped as
// an unparseable control message.
const ws = makeFakeWs();
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
ws.__emitter.emit("message", Buffer.from("ls -la\r"), { binary: false });
assert.deepEqual(fakePty.__writes[0], "ls -la\r");
});
it("sends an exit control message and closes on PTY exit", () => {
const ws = makeFakeWs();
let closed = false;
ws.close = () => {
closed = true;
};
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
fakePty.__emitter.emit("exit", { exitCode: 0 });
const last = ws.__sent[ws.__sent.length - 1];
assert.equal(last.binary, false);
assert.deepEqual(JSON.parse(last.data), { type: "exit", code: 0 });
assert.equal(closed, true);
});
it("kills the PTY attach process when the WS connection closes", () => {
const ws = makeFakeWs();
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
let killed = false;
fakePty.kill = () => {
killed = true;
};
ws.__emitter.emit("close");
assert.equal(killed, true);
});
});
describe("attachBinary on Windows", () => {
it("names tmux with its extension — node-pty there never adds `.exe`", () => {
// A bare `tmux` made node-pty answer "File not found" on every attach, so
// the browser's terminal opened, closed and reconnected every 1.5 s.
const saved = process.env.PATH;
process.env.PATH = "";
try {
assert.equal(ptyAttach.attachBinary("win32"), "tmux.exe");
} finally {
process.env.PATH = saved;
}
});
});