Files
AgentHub/server/__tests__/cursor-pool.test.js
T
nntrivi2001 685eb748e8 feat: account pool per tool with groups and a Cursor pool; update runs in the background
- The update dialog can be closed while an update applies: it keeps running,
  the page still reloads itself when the new server is up, and a failure
  re-opens the dialog with its error.
- Account pool card: one tab per AI tool (Claude Code, Codex, Cursor CLI,
  Oh My Pi, and every other installed tool, which says it has no pool),
  accounts as cards with 5-hour and weekly quota shown as what is LEFT plus
  their reset times, and the saved profiles of Settings → AI tools →
  Profiles as cards with "Share to pool".
- Cursor account pool (server/lib/cursor-pool.js): Cursor API keys handed to
  a cursor-agent pane only through tmux -e CURSOR_API_KEY;
  `agenthub accounts add <name> --tool cursor`.
- Pool groups (server/lib/pool-groups.js, /api/accounts/groups): a group's
  members run only on its accounts of each tool, a user in no group on the
  accounts no group lists, a pin still wins. account_pins loses its tool
  CHECK (rebuilt, pins kept); three new tables.
- Oh My Pi login sharing can be switched from its pool tab too.
2026-10-22 17:02:08 +07:00

139 lines
5.4 KiB
JavaScript

/**
* @file cursor-pool.test.js
* @description The Cursor account pool (server/lib/cursor-pool.js), fake keys
* only: key validation and file modes, the balancer (fewest live Cursor panes,
* a pin wins), the key reaching a `cursor-agent` pane only through tmux `-e
* CURSOR_API_KEY` and never its argv, the account recorded on the run, pins
* for `cursor` and the account_pins rebuild that drops the old tool CHECK, and
* an empty pool leaving every spawn as it was.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
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.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "cursor-pool-test-")));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
process.env.CLAUDE_HOME = path.join(TMP, "claude");
const { db } = require("../db");
const pool = require("../lib/cursor-pool");
const pins = require("../lib/account-pins");
const tmux = require("../lib/tmux");
const pty = require("../lib/pty-run");
const KEY = (c) => `key_${c.repeat(40)}`;
let tmuxArgv;
const envOf = (argv, name) =>
argv
.slice(0, argv.indexOf("--"))
.find((x) => String(x).startsWith(`${name}=`))
?.slice(name.length + 1);
function run(id, account, status = "running") {
db.prepare(
`INSERT INTO dashboard_runs (id, mode, cwd, status, started_at, account, tool)
VALUES (?, 'interactive', '/tmp', ?, ?, ?, 'cursor')`
).run(id, status, new Date().toISOString(), account);
}
before(() => {
tmux.__setExecImpl((args) => {
if (args[0] === "has-session") throw Object.assign(new Error("gone"), { status: 1 });
if (args[0] === "new-session") tmuxArgv = args;
return "";
});
});
after(() => {
tmux.__setExecImpl(null);
fs.rmSync(TMP, { recursive: true, force: true });
});
beforeEach(() => {
fs.rmSync(pool.poolDir(), { recursive: true, force: true });
db.prepare("DELETE FROM dashboard_runs").run();
tmuxArgv = null;
});
describe("cursor pool: accounts", () => {
it("stores a key 0600 in a 0700 dir and never lists it", () => {
const added = pool.addAccount("team", { apiKey: KEY("a") });
assert.deepEqual(Object.keys(added).sort(), ["addedAt", "kind", "name"]);
assert.equal(fs.statSync(pool.poolDir()).mode & 0o777, 0o700);
assert.equal(fs.statSync(path.join(pool.poolDir(), "team.json")).mode & 0o777, 0o600);
const listed = pool.poolStatus();
assert.equal(listed.length, 1);
assert.ok(!JSON.stringify(listed).includes(KEY("a")));
assert.equal(listed[0].quotaPercent, null);
});
it("refuses a bad name, a bad key and a taken name", () => {
assert.throws(() => pool.addAccount("../x", { apiKey: KEY("a") }), { code: "EBADNAME" });
assert.throws(() => pool.addAccount("x", { apiKey: "short" }), { code: "EBADTOKEN" });
assert.throws(() => pool.addAccount("x", { apiKey: `${KEY("a")}\nX=1` }), {
code: "EBADTOKEN",
});
pool.addAccount("x", { apiKey: KEY("a") });
assert.throws(() => pool.addAccount("x", { apiKey: KEY("b") }), { code: "EEXISTS" });
pool.removeAccount("x");
assert.throws(() => pool.removeAccount("x"), { code: "ENOACCOUNT" });
});
it("picks the account with the fewest live panes", () => {
pool.addAccount("a", { apiKey: KEY("a") });
pool.addAccount("b", { apiKey: KEY("b") });
run("r1", "a");
run("r2", "b", "completed");
assert.equal(pool.pickAccount().name, "b");
});
});
describe("cursor pool: spawn", () => {
it("an empty pool leaves a Cursor spawn exactly as before", () => {
pty.spawnRun({ laneId: 7, cwd: "/tmp", tool: "cursor" });
assert.equal(envOf(tmuxArgv, "CURSOR_API_KEY"), undefined);
const row = db.prepare("SELECT account FROM dashboard_runs WHERE id = 'agenthub-lane-7'").get();
assert.equal(row.account, null);
});
it("the key reaches the pane through tmux -e only, and the run names its account", () => {
pool.addAccount("a", { apiKey: KEY("a") });
pty.spawnRun({ laneId: 7, cwd: "/tmp", tool: "cursor", initialPrompt: "hi" });
assert.equal(envOf(tmuxArgv, "CURSOR_API_KEY"), KEY("a"));
const after = tmuxArgv.slice(tmuxArgv.indexOf("--"));
assert.ok(!after.some((x) => String(x).includes(KEY("a"))), "never in the pane's argv");
const row = db.prepare("SELECT account FROM dashboard_runs WHERE id = 'agenthub-lane-7'").get();
assert.equal(row.account, "a");
// Other tools never get it.
tmuxArgv = null;
pty.spawnRun({ laneId: 8, cwd: "/tmp", tool: "claude" });
assert.ok(!tmuxArgv.some((x) => String(x).includes("CURSOR_API_KEY")));
});
});
describe("cursor pool: pins", () => {
it("account_pins takes a cursor pin, and removing the account drops it", () => {
const sql = db
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='account_pins'")
.get().sql;
assert.ok(!sql.includes("CHECK(tool IN"), "no tool CHECK left");
assert.ok(pins.TOOLS.includes("cursor"));
const uid = db
.prepare("INSERT INTO users (username, password_hash) VALUES ('pinned', 'x')")
.run().lastInsertRowid;
pool.addAccount("a", { apiKey: KEY("a") });
db.prepare("INSERT INTO account_pins (user_id, tool, account) VALUES (?, 'cursor', 'a')").run(
uid
);
assert.equal(pins.listPins("cursor").length, 1);
pool.removeAccount("a");
assert.equal(pins.listPins("cursor").length, 0);
});
});