/** * @file server-mode-account-pool.test.js * @description The Claude/Codex account pool from the Users page: only an * admin may add or remove an account, each change needs the admin's OWN * password again (EREAUTH), the token only goes in — no answer (the add, the * list) ever carries it — the audit log names the account, not its secret, and * in local mode the routes refuse. An admin pins a user to one account * (`PUT /api/accounts/pins`): that user's new panes skip the balancer. * @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(), "server-mode-account-pool-")); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.CLAUDE_HOME = path.join(TMP, ".claude"); process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp, startServer } = require("../index"); const { db } = require("../db"); const auth = require("../lib/auth"); const claudePool = require("../lib/claude-pool"); const codexPool = require("../lib/codex-pool"); // The quota read would go to the network; the pool's list must not. require("../lib/cc-usage").fetchUsage = async () => ({}); let server; let BASE; const BOSS_PASS = "admin-pass-1"; const TOKEN = `sk-ant-oat01-${"a".repeat(40)}`; const KEY = `sk-proj-${"b".repeat(40)}`; 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"; server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; await auth.createUser("boss", BOSS_PASS, "admin"); await auth.createUser("ana", "analyst-pass-1", "analyst"); await auth.createUser("mia", "member-pass-1", "developer"); }); after(() => { delete process.env.AGENTHUB_MODE; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); describe("the account pool from the Users page", () => { it("lets an admin add a Claude account, never answering with its token", async () => { const boss = await login("boss", BOSS_PASS); const r = await call("POST", "/api/accounts", { cookie: boss, body: { tool: "claude", name: "team-1", token: TOKEN, currentPassword: BOSS_PASS }, }); assert.equal(r.status, 201); assert.equal(r.body.account.name, "team-1"); assert.ok(!JSON.stringify(r.body).includes(TOKEN)); assert.equal(claudePool.pickAccount().token, TOKEN, "new panes run on it"); const list = await call("GET", "/api/accounts", { cookie: boss }); assert.deepEqual( list.body.accounts.map((a) => a.name), ["team-1"] ); assert.ok(!JSON.stringify(list.body).includes(TOKEN)); const audit = db.prepare("SELECT path FROM audit_log ORDER BY id DESC LIMIT 1").get(); assert.match(audit.path, /add claude account team-1/); assert.ok(!audit.path.includes(TOKEN)); }); it("adds a Codex API key, and removes accounts again", async () => { const boss = await login("boss", BOSS_PASS); const add = await call("POST", "/api/accounts", { cookie: boss, body: { tool: "codex", name: "cx-1", token: KEY, currentPassword: BOSS_PASS }, }); assert.equal(add.status, 201); assert.equal(add.body.account.kind, "apikey"); assert.ok(!JSON.stringify(add.body).includes(KEY)); assert.equal(codexPool.__read("cx-1").key, KEY); const del = await call("DELETE", "/api/accounts/cx-1", { cookie: boss, body: { tool: "codex", currentPassword: BOSS_PASS }, }); assert.equal(del.status, 200); assert.equal(codexPool.__read("cx-1"), null); const gone = await call("DELETE", "/api/accounts/cx-1", { cookie: boss, body: { tool: "codex", currentPassword: BOSS_PASS }, }); assert.equal(gone.status, 404); }); it("refuses a wrong password, a bad token, a taken name and an unknown tool", async () => { const boss = await login("boss", BOSS_PASS); const add = (body) => call("POST", "/api/accounts", { cookie: boss, body: { tool: "claude", name: "team-2", token: TOKEN, currentPassword: BOSS_PASS, ...body }, }); assert.equal((await add({ currentPassword: "nope" })).body.error.code, "EREAUTH"); assert.equal((await add({ token: "not-a-token" })).body.error.code, "EBADTOKEN"); assert.equal((await add({ name: "team-1" })).body.error.code, "EEXISTS"); assert.equal((await add({ tool: "cursor" })).body.error.code, "EBADTOOL"); assert.equal((await add({ name: "../x" })).body.error.code, "EBADNAME"); }); it("is an admin's only: an analyst reads it, a developer neither reads nor writes", async () => { const ana = await login("ana", "analyst-pass-1"); assert.equal((await call("GET", "/api/accounts", { cookie: ana })).status, 200); const w = await call("POST", "/api/accounts", { cookie: ana, body: { tool: "claude", name: "x", token: TOKEN, currentPassword: "analyst-pass-1" }, }); assert.equal(w.status, 403); const mia = await login("mia", "member-pass-1"); assert.equal((await call("GET", "/api/accounts", { cookie: mia })).status, 403); const d = await call("DELETE", "/api/accounts/team-1", { cookie: mia, body: { tool: "claude", currentPassword: "member-pass-1" }, }); assert.equal(d.status, 403); assert.ok(claudePool.pickAccount(), "team-1 is still there"); }); }); describe("pinning a user to one account", () => { const pins = require("../lib/account-pins"); const pin = (cookie, body) => call("PUT", "/api/accounts/pins", { cookie, body: { tool: "claude", currentPassword: BOSS_PASS, ...body }, }); it("lets an admin pin and unpin, audited, and new panes follow the pin", async () => { const boss = await login("boss", BOSS_PASS); const miaId = db.prepare("SELECT id FROM users WHERE username = 'mia'").get().id; claudePool.addAccount("team-z", `sk-ant-oat01-${"z".repeat(40)}`); assert.equal(claudePool.pickAccount(null, miaId).name, "team-1", "the balancer's pick"); const r = await pin(boss, { userId: miaId, account: "team-z" }); assert.equal(r.status, 200); assert.deepEqual(r.body.pin, { userId: miaId, username: "mia", account: "team-z" }); assert.equal(claudePool.pickAccount(null, miaId).name, "team-z"); assert.equal(claudePool.pickAccount(null, null).name, "team-1", "nobody else moves"); const audit = db.prepare("SELECT owner_id, path FROM audit_log ORDER BY id DESC LIMIT 1").get(); assert.equal(audit.owner_id, miaId); assert.match(audit.path, /pin mia to claude account team-z/); const list = await call("GET", "/api/accounts", { cookie: boss }); assert.deepEqual( list.body.pins.claude.map((p) => [p.username, p.account]), [["mia", "team-z"]] ); assert.deepEqual(list.body.pins.codex, []); const off = await pin(boss, { userId: miaId, account: null }); assert.equal(off.body.pin.account, null); assert.equal(claudePool.pickAccount(null, miaId).name, "team-1"); claudePool.removeAccount("team-z"); }); it("refuses an unknown account or user, a wrong password, and a non-admin", async () => { const boss = await login("boss", BOSS_PASS); const miaId = db.prepare("SELECT id FROM users WHERE username = 'mia'").get().id; assert.equal( (await pin(boss, { userId: miaId, account: "nope" })).body.error.code, "ENOACCOUNT" ); assert.equal((await pin(boss, { userId: 9999, account: "team-1" })).body.error.code, "ENOUSER"); assert.equal( (await pin(boss, { userId: miaId, account: "team-1", currentPassword: "x" })).body.error.code, "EREAUTH" ); const ana = await login("ana", "analyst-pass-1"); const w = await call("PUT", "/api/accounts/pins", { cookie: ana, body: { tool: "claude", userId: miaId, account: "team-1", currentPassword: "analyst-pass-1" }, }); assert.equal(w.status, 403); assert.equal(pins.pinFor(miaId, "claude"), null); }); it("is ignored in local mode and dropped with its user", async () => { const miaId = db.prepare("SELECT id FROM users WHERE username = 'mia'").get().id; pins.setPin(miaId, "claude", "team-1"); delete process.env.AGENTHUB_MODE; try { assert.equal(pins.pinFor(miaId, "claude"), null); } finally { process.env.AGENTHUB_MODE = "server"; } assert.equal(pins.pinFor(miaId, "claude"), "team-1"); db.prepare("DELETE FROM users WHERE id = ?").run(miaId); assert.equal( db.prepare("SELECT COUNT(*) AS n FROM account_pins WHERE user_id = ?").get(miaId).n, 0 ); }); });