/** * @file Tests for each member's own tmux server on the Users page (S3, * server/lib/member-tmux.js + `/api/users/tmux*`): its version beside the * installed one, an admin's restart of one member (password again, audited, * ETMUXSTUCK when it will not stop) and of every idle outdated member — a * member whose agent is working is skipped. Member tmux calls are faked. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const path = require("node:path"); const fs = require("node:fs"); const os = require("node:os"); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "member-tmux-")); const USERS = path.join(TMP, "users"); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp, startServer } = require("../index"); const { db } = require("../db"); const auth = require("../lib/auth"); const linux = require("../lib/linux-accounts"); const tmux = require("../lib/tmux"); let server; let BASE; const ids = {}; const BOSS_PASS = "admin-pass-1"; // Fake member tmux servers, by Linux login. const servers = {}; function fakeMember(acct, args) { const s = servers[acct.login]; if (!s || !s.alive) throw Object.assign(new Error("no server running"), { status: 1 }); if (args[0] === "display-message") return `${s.version}\t${s.pid}\t1700000000\t${s.sessions}\n`; if (args[0] === "kill-server") { if (s.stuck) throw Object.assign(new Error("server exited unexpectedly"), { status: 1 }); s.alive = false; return ""; } if (args[0] === "list-sessions") return ""; return ""; } async function call(method, p, { cookie, body } = {}) { const res = await fetch(BASE + p, { method, headers: { "Content-Type": "application/json", ...(cookie ? { cookie } : {}) }, body: body ? JSON.stringify(body) : undefined, }); return { status: res.status, body: await res.json().catch(() => null) }; } async function login(username, password) { const res = await fetch(BASE + "/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); return res.status === 200 ? res.headers.get("set-cookie").split(";")[0] : null; } before(async () => { process.env.AGENTHUB_MODE = "server"; process.env.AGENTHUB_USERS_ROOT = USERS; fs.mkdirSync(USERS, { recursive: true }); server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; ids.boss = (await auth.createUser("boss", BOSS_PASS, "admin")).id; ids.mia = (await auth.createUser("mia", "member-pass-1", "developer")).id; ids.zed = (await auth.createUser("zed", "member-pass-1", "developer")).id; ids.ann = (await auth.createUser("ann", "member-pass-1", "developer")).id; ids.stu = (await auth.createUser("stu", "member-pass-1", "developer")).id; // zed's agent is working right now. db.prepare( "INSERT INTO lanes (cwd, tool, owner_id, status) VALUES (?, 'claude', ?, 'running')" ).run(path.join(USERS, "zed", "p"), ids.zed); // S3 on: a server.conf whose TMUX_BIN answers 3.8, and tmux-member installed. const bin = path.join(TMP, "tmux"); fs.writeFileSync(bin, "#!/bin/sh\necho 'tmux 3.8'\n", { mode: 0o755 }); const wrapper = path.join(TMP, "tmux-member"); fs.writeFileSync(wrapper, "#!/bin/sh\n", { mode: 0o755 }); process.env.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf"); fs.writeFileSync( process.env.AGENTHUB_SERVER_CONF, `HOST_USER=h\nHOST_UID=1\nUSERS_ROOT=${USERS}\nTMUX_BIN=${bin}\nGIT_BIN=/usr/bin/git\n` ); linux.__setTmuxMember(wrapper); Object.assign(servers, { "ah-mia": { version: "3.6", pid: 11, sessions: 2, alive: true }, "ah-zed": { version: "3.6", pid: 12, sessions: 1, alive: true }, "ah-ann": { version: "3.8", pid: 13, sessions: 3, alive: true }, "ah-stu": { version: "3.6", pid: 14, sessions: 1, alive: true, stuck: true }, }); tmux.__setAccountImpl(() => null, fakeMember); }); after(() => { delete process.env.AGENTHUB_MODE; delete process.env.AGENTHUB_USERS_ROOT; delete process.env.AGENTHUB_SERVER_CONF; linux.__setTmuxMember("/opt/agenthub/libexec/tmux-member"); linux.__resetConfCache(); tmux.__reset(); server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); describe("member tmux servers on the Users page", () => { it("lists each member's version beside the installed one, and who is working", async () => { const boss = await login("boss", BOSS_PASS); const r = await call("GET", "/api/users/tmux", { cookie: boss }); assert.equal(r.status, 200); assert.equal(r.body.available, true); assert.equal(r.body.installed, "3.8"); assert.equal(r.body.matched, true); const by = Object.fromEntries(r.body.users.map((u) => [u.username, u])); assert.equal(by.mia.version, "3.6"); assert.equal(by.mia.outdated, true); assert.equal(by.mia.sessions, 2); assert.equal(by.zed.busy, 1); assert.equal(by.ann.outdated, false); // boss (no server of theirs) is listed as not running. assert.equal(by.boss.running, false); }); it("is staff-only to read and an admin's to restart", async () => { const mia = await login("mia", "member-pass-1"); assert.equal((await call("GET", "/api/users/tmux", { cookie: mia })).status, 403); const r = await call("POST", `/api/users/${ids.ann}/tmux-restart`, { cookie: mia, body: { currentPassword: "member-pass-1" }, }); assert.equal(r.status, 403); assert.equal(servers["ah-ann"].alive, true); }); it("restarts one member's server with the admin's password, audited", async () => { const boss = await login("boss", BOSS_PASS); const wrong = await call("POST", `/api/users/${ids.ann}/tmux-restart`, { cookie: boss, body: { currentPassword: "nope" }, }); assert.equal(wrong.body.error.code, "EREAUTH"); assert.equal(servers["ah-ann"].alive, true); const ok = await call("POST", `/api/users/${ids.ann}/tmux-restart`, { cookie: boss, body: { currentPassword: BOSS_PASS }, }); assert.equal(ok.status, 200); assert.equal(servers["ah-ann"].alive, false); const audit = db .prepare("SELECT path FROM audit_log WHERE owner_id = ? ORDER BY id DESC LIMIT 1") .get(ids.ann); assert.match(audit.path, /ann: tmux restarted \(was 3\.8, 3 session/); }); it("names the pid to kill as root when a server will not stop", async () => { const boss = await login("boss", BOSS_PASS); const r = await call("POST", `/api/users/${ids.stu}/tmux-restart`, { cookie: boss, body: { currentPassword: BOSS_PASS }, }); assert.equal(r.status, 409); assert.equal(r.body.error.code, "ETMUXSTUCK"); assert.match(r.body.error.message, /sudo kill -9 14/); }); it("restarts every idle outdated member, skipping one whose agent works", async () => { servers["ah-ann"] = { version: "3.8", pid: 13, sessions: 1, alive: true }; const boss = await login("boss", BOSS_PASS); const r = await call("POST", "/api/users/tmux-restart-idle", { cookie: boss, body: { currentPassword: BOSS_PASS }, }); assert.equal(r.status, 200); assert.deepEqual(r.body.restarted, ["mia"]); assert.deepEqual(r.body.skipped, ["zed"]); assert.equal(r.body.failed[0].username, "stu"); assert.equal(servers["ah-mia"].alive, false); assert.equal(servers["ah-zed"].alive, true); assert.equal(servers["ah-ann"].alive, true); // already on 3.8 }); });