/** * @file pool-groups.test.js * @description Account pool groups (server/lib/pool-groups.js and the * `/api/accounts/groups` routes), fake accounts only: with no group every * account is shared as before; a group's member gets only the group's * accounts of each tool (none → no pool account), a user in no group only * the accounts no group lists, a pin wins over the group; one group per user; * a removed account or deleted group leaves no rows behind; and the routes * need an admin's own password again, are audited and refuse in local mode. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after, beforeEach } = 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(), "pool-groups-test-")); 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 groups = require("../lib/pool-groups"); const cursorPool = require("../lib/cursor-pool"); const claudePool = require("../lib/claude-pool"); const pins = require("../lib/account-pins"); require("../lib/cc-usage").fetchUsage = async () => ({}); const BOSS_PASS = "admin-pass-1"; const KEY = (c) => `key_${c.repeat(40)}`; const TOKEN = (c) => `sk-ant-oat01-${c.repeat(40)}`; let server; let BASE; let MIA; let LEO; let ZOE; 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.headers.get("set-cookie").split(";")[0]; } 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"); MIA = (await auth.createUser("mia", "member-pass-1", "developer")).id; LEO = (await auth.createUser("leo", "member-pass-2", "developer")).id; ZOE = (await auth.createUser("zoe", "member-pass-3", "developer")).id; }); after(() => { delete process.env.AGENTHUB_MODE; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); beforeEach(() => { db.prepare("DELETE FROM pool_groups").run(); db.prepare("DELETE FROM account_pins").run(); for (const dir of [cursorPool.poolDir(), claudePool.poolDir()]) { fs.rmSync(dir, { recursive: true, force: true }); } for (const c of ["a", "b", "c"]) cursorPool.addAccount(c, { apiKey: KEY(c) }); }); describe("pool groups: who may take which account", () => { it("shares every account while no group exists", () => { assert.equal(cursorPool.pickAccount(MIA).name, "a"); assert.ok(["a", "b", "c"].every(groups.allowed(MIA, "cursor"))); }); it("gives a member only the group's accounts, the rest only the shared ones", () => { const g = groups.createGroup("Team A"); groups.setGroupUsers(g.id, [MIA]); groups.setGroupAccounts(g.id, "cursor", ["b"]); assert.equal(cursorPool.pickAccount(MIA).name, "b"); assert.equal(cursorPool.pickAccount(LEO).name, "a", "b is no longer shared"); // An account may serve several groups; a group with no account of a tool // leaves its members on their own sign-in. const h = groups.createGroup("Team B"); groups.setGroupUsers(h.id, [ZOE]); assert.equal(cursorPool.pickAccount(ZOE), null); groups.setGroupAccounts(h.id, "cursor", ["b", "c"]); assert.ok(["b", "c"].includes(cursorPool.pickAccount(ZOE).name)); assert.equal(cursorPool.pickAccount(LEO).name, "a"); // Claude follows the same groups. claudePool.addAccount("shared", TOKEN("s")); claudePool.addAccount("team", TOKEN("t")); groups.setGroupAccounts(g.id, "claude", ["team"]); assert.equal(claudePool.pickAccount(null, MIA).name, "team"); assert.equal(claudePool.pickAccount(null, LEO).name, "shared"); }); it("puts a user in one group only, and honours a pin outside it", () => { const g = groups.createGroup("A"); const h = groups.createGroup("B"); groups.setGroupUsers(g.id, [MIA, LEO]); groups.setGroupUsers(h.id, [MIA]); const list = groups.listGroups(); assert.deepEqual( list.find((x) => x.name === "A").users.map((u) => u.username), ["leo"] ); assert.deepEqual( list.find((x) => x.name === "B").users.map((u) => u.username), ["mia"] ); groups.setGroupAccounts(h.id, "cursor", ["b"]); pins.setPin(MIA, "cursor", "c"); assert.equal(cursorPool.pickAccount(MIA).name, "c"); }); it("drops a removed account from its groups, and a group's rows with it", () => { const g = groups.createGroup("A"); groups.setGroupUsers(g.id, [MIA]); groups.setGroupAccounts(g.id, "cursor", ["b"]); cursorPool.removeAccount("b"); assert.deepEqual(groups.listGroups()[0].accounts.cursor, []); groups.deleteGroup(g.id); assert.equal(db.prepare("SELECT COUNT(*) AS n FROM pool_group_users").get().n, 0); assert.throws(() => groups.setGroupAccounts(g.id, "cursor", ["a"]), { code: "ENOGROUP" }); assert.throws(() => groups.createGroup(""), { code: "EBADNAME" }); }); }); describe("pool groups: routes", () => { it("creates, edits and deletes a group with the admin's own password, audited", async () => { const boss = await login("boss", BOSS_PASS); const bad = await call("POST", "/api/accounts/groups", { cookie: boss, body: { name: "Team A", currentPassword: "nope" }, }); assert.equal(bad.body.error.code, "EREAUTH"); const made = await call("POST", "/api/accounts/groups", { cookie: boss, body: { name: "Team A", currentPassword: BOSS_PASS }, }); assert.equal(made.status, 201); const id = made.body.group.id; const dup = await call("POST", "/api/accounts/groups", { cookie: boss, body: { name: "team a", currentPassword: BOSS_PASS }, }); assert.equal(dup.body.error.code, "EEXISTS"); const edit = await call("PATCH", `/api/accounts/groups/${id}`, { cookie: boss, body: { name: "Team Alpha", userIds: [MIA], accounts: { cursor: ["a"] }, currentPassword: BOSS_PASS, }, }); assert.equal(edit.status, 200); assert.equal(edit.body.group.name, "Team Alpha"); assert.deepEqual(edit.body.group.accounts.cursor, ["a"]); const missing = await call("PATCH", `/api/accounts/groups/${id}`, { cookie: boss, body: { accounts: { cursor: ["nope"] }, currentPassword: BOSS_PASS }, }); assert.equal(missing.body.error.code, "ENOACCOUNT"); const list = await call("GET", "/api/accounts", { cookie: boss }); assert.equal(list.body.groups[0].users[0].username, "mia"); const audit = db.prepare("SELECT path FROM audit_log ORDER BY id DESC LIMIT 1").get(); assert.match(audit.path, /group Team Alpha cursor accounts/); const dev = await login("leo", "member-pass-2"); const refused = await call("DELETE", `/api/accounts/groups/${id}`, { cookie: dev, body: { currentPassword: "member-pass-2" }, }); assert.equal(refused.status, 403); const del = await call("DELETE", `/api/accounts/groups/${id}`, { cookie: boss, body: { currentPassword: BOSS_PASS }, }); assert.equal(del.status, 200); const gone = await call("DELETE", `/api/accounts/groups/${id}`, { cookie: boss, body: { currentPassword: BOSS_PASS }, }); assert.equal(gone.status, 404); }); });