Files
AgentHub/scripts/test-server.js
T
Nguyễn Ngọc Trí Vĩ cd427d218b fix(terminal): psmux wheel scrolling, per-platform tmux upgrade advice, Windows-safe server tests
- Server tests on Windows no longer reach the user's real psmux: psmux
  ignores TMUX_TMPDIR (and 3.3.x PSMUX_DATA_DIR), so quit.test.js's Quit and
  the runner's kill-server ended every live lane pane — the pre-commit hook
  included. scripts/test-server.js now takes tmux off the suites' PATH there.
- Wheel scrolling on Windows: psmux never turns mouse tracking on for its
  client, so xterm.js never reported the wheel and a pane could not scroll.
  A Windows attach is told {type:"wheel"} and the browser sends the SGR wheel
  report itself; psmux scrolls or forwards it to the pane's app.
- tmux.upgradeAdvice(): the oldest tmux AgentHub asks for and how each
  platform upgrades (psmux 3.4.0 -> winget upgrade marlocarlo.psmux; tmux 3.7
  -> brew upgrade tmux / build the release on Linux). Used by the doctor,
  GET /api/run/tmux ({available, version, upgrade}), `agenthub doctor`, and a
  sticky toast on the Workspace page. Advice only: nothing installs tmux.

Committed with --no-verify (user's call): the pre-commit hook runs the full
backend suite, which has ~378 pre-existing failures on Windows (S3/Linux-only
tests: sudo, /proc, systemd, members). Every test touching this change passes:
pty-attach, tmux, run, plugin-doctor-tmux, TerminalView, Workspace, and the
client tests related to the changed files; tsc and the header audit are clean.
2026-10-15 11:46:09 +07:00

113 lines
4.9 KiB
JavaScript

#!/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-<id>` 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.
* It also points `DASHBOARD_PIPELINES_DIR` at `server/__tests__/fixtures/
* pipelines/`: AgentHub ships no pipeline template any more, and the two old
* ones kept there are what the dormant stage/detection code is tested against.
* `AGENTHUB_SETTINGS_FILE` goes to the same temp dir, so a suite that saves a
* setting never writes the developer's real `~/.config/agenthub/settings.env`,
* and `AGENTHUB_PRICING_SYNC=0` keeps every suite off the network. The
* `GIT_DIR`-family variables a git hook exports are dropped too, so a test's
* own `git init`/`commit` can never land in the real repository. Every
* inherited `AGENTHUB_*` variable is dropped as well: a dashboard pane carries
* `AGENTHUB_MODE=server` and friends, which turned the suites login-gated.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
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-"));
// A dashboard pane's own AGENTHUB_* (AGENTHUB_MODE=server, AGENTHUB_USERS_ROOT,
// its pane token…) would turn the suites into a login-gated server: hundreds
// of 401s and EFOLDERs that have nothing to do with the code under test.
const inherited = Object.fromEntries(
Object.entries(process.env).filter(([k]) => !k.startsWith("AGENTHUB_"))
);
const env = {
...inherited,
TMUX_TMPDIR: tmuxDir,
// Never the developer's real ~/.config/agenthub/settings.env: it can pin
// DASHBOARD_DATA_DIR at their live database.
AGENTHUB_SETTINGS_FILE: path.join(tmuxDir, "settings.env"),
// No suite may fetch LiteLLM's price list (reset-pricing syncs at once).
AGENTHUB_PRICING_SYNC: "0",
// Nor run the developer's real agy (it may update itself) or send their
// real Cursor, Grok, Kimi or OpenCode login for the quota.
AGENTHUB_AGY_USAGE: "0",
// Nor refresh (spend) a real Claude pool login: its refresh token is single use.
DASHBOARD_POOL_REFRESH_MS: "0",
XDG_CONFIG_HOME: tmuxDir,
XDG_DATA_HOME: tmuxDir,
GROK_HOME: tmuxDir,
KIMI_CODE_HOME: tmuxDir,
DASHBOARD_PIPELINES_DIR: path.join(dir, "fixtures", "pipelines"),
// Never this machine's server-mode S3 install: its system unit would become
// "the" service manager and its server.conf would send user changes to sudo.
AGENTHUB_SYSTEM_UNIT_DIR: path.join(tmuxDir, "system-units"),
AGENTHUB_SERVER_CONF: path.join(tmuxDir, "server.conf"),
AGENTHUB_PASSWD: path.join(tmuxDir, "passwd"),
};
delete env.TMUX;
delete env.TMUX_PANE;
// psmux (the WinGet `tmux` on Windows) ignores TMUX_TMPDIR — and 3.3.x its
// PSMUX_DATA_DIR too — so every tmux call reached the user's REAL server:
// quit.test.js's Quit killed every live lane pane, and the kill-server below
// the rest. Only `-L` isolates there, which no env var can add to each call,
// so on Windows the suites get no tmux at all; theirs skip (or fail) instead.
if (process.platform === "win32") {
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH");
if (key) {
env[key] = env[key]
.split(path.delimiter)
.filter(
(d) => !["tmux.exe", "tmux.cmd", "tmux.bat"].some((f) => fs.existsSync(path.join(d, f)))
)
.join(path.delimiter);
}
delete env.PSMUX_SESSION;
}
// Git exports these to a hook it runs from a linked worktree (the pre-commit
// hook runs this suite): inherited, every `git init`/`commit` a test makes in
// its temp folder went to the REAL repository instead — commits on the
// worktree's HEAD and `core.bare = true` in the shared config.
for (const k of [
"GIT_DIR",
"GIT_INDEX_FILE",
"GIT_WORK_TREE",
"GIT_COMMON_DIR",
"GIT_PREFIX",
"GIT_OBJECT_DIRECTORY",
]) {
delete env[k];
}
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);