From 67210bcd6184e2017ccc80107f947064402775e4 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 24 Sep 2026 15:48:19 +0700 Subject: [PATCH] fix(test): run the server suite on a private tmux server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several suites start real `agenthub-lane-` tmux sessions and then kill or reset them, with lane ids from their own throwaway database. On the user's default tmux server those names collide with the dashboard's real lane panes, so a test run — including the one the pre-commit hook runs on every commit — killed whichever live session shared an id, the pane the commit was made from included. `npm run test:server` now goes through scripts/test-server.js, which drops TMUX, points TMUX_TMPDIR at a fresh directory and kills that server after. Also fixes the ERUNTIMEOUT test's tmux mock, which had the wrong (async, cmd+args) signature and only passed by accident. --- package.json | 4 +-- scripts/test-server.js | 42 +++++++++++++++++++++++++ server/__tests__/lane-lifecycle.test.js | 17 ++++------ 3 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 scripts/test-server.js diff --git a/package.json b/package.json index 485e2640..a8c03220 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,8 @@ "import-history": "node scripts/import-history.js", "reconcile-tokens": "node scripts/import-history.js --reconcile-tokens", "clear-data": "node scripts/clear-data.js", - "test": "node --test server/__tests__/*.test.js && cd client && npm test", - "test:server": "node --test server/__tests__/*.test.js", + "test": "node scripts/test-server.js && cd client && npm test", + "test:server": "node scripts/test-server.js", "test:client": "cd client && npm test", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/scripts/test-server.js b/scripts/test-server.js new file mode 100644 index 00000000..6496c97d --- /dev/null +++ b/scripts/test-server.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +/** + * @file test-server.js + * @description Runs the server test suite against a PRIVATE tmux server. + * Several suites start real `agenthub-lane-` tmux sessions and then kill + * or reset them, with lane ids from their own throwaway database — on the + * user's default tmux server those names collide with the dashboard's real + * lane panes, and a test run kills whichever live session shares an id + * (including the pane the test was launched from). Dropping `TMUX` and + * pointing `TMUX_TMPDIR` at a fresh directory gives every tmux call — the + * app's own and the tests' direct `execFileSync("tmux", …)` — its own socket. + * @author Nguyễn Ngọc Trí Vĩ + */ + +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const root = path.join(__dirname, ".."); +const dir = path.join(root, "server", "__tests__"); +const files = fs + .readdirSync(dir) + .filter((f) => f.endsWith(".test.js")) + .map((f) => path.join(dir, f)); +const args = process.argv.slice(2); + +const tmuxDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-test-tmux-")); +const env = { ...process.env, TMUX_TMPDIR: tmuxDir }; +delete env.TMUX; +delete env.TMUX_PANE; + +const result = spawnSync(process.execPath, ["--test", ...args, ...(args.length ? [] : files)], { + stdio: "inherit", + env, + cwd: root, +}); + +// Whatever a test leaked dies with the private server, never a real pane. +spawnSync("tmux", ["kill-server"], { env, stdio: "ignore" }); +fs.rmSync(tmuxDir, { recursive: true, force: true }); +process.exit(result.status ?? 1); diff --git a/server/__tests__/lane-lifecycle.test.js b/server/__tests__/lane-lifecycle.test.js index dc6996e9..d36c65ba 100644 --- a/server/__tests__/lane-lifecycle.test.js +++ b/server/__tests__/lane-lifecycle.test.js @@ -848,17 +848,12 @@ describe("destructive lane lifecycle actions", () => { const runId = started.body.lane.run_id; // Mock tmux so has-session always returns 0 (session exists), simulating a stuck session - tmux.__setExecImpl(async (cmd, args) => { - if (cmd === "tmux" && args[0] === "has-session" && args[1] === "-t" && args[2] === runId) { - return { code: 0, stdout: "", stderr: "" }; - } - if (cmd === "tmux" && args[0] === "list-sessions") { - return { code: 0, stdout: "", stderr: "" }; - } - if (cmd === "tmux" && args[0] === "kill-session" && args[1] === "-t" && args[2] === runId) { - return { code: 0, stdout: "", stderr: "" }; - } - return { code: 1, stdout: "", stderr: "" }; + // Same shape as tmux.js's own exec: sync, argv only, throws on failure. + tmux.__setExecImpl((args) => { + if (args[0] === "has-session" && args[2] === runId) return ""; + if (args[0] === "list-sessions") return `${runId}\n`; + if (args[0] === "kill-session" && args[2] === runId) return ""; + throw Object.assign(new Error("mocked tmux failure"), { status: 1 }); }); try {