Files
nntrivi2001 7a88b74016 feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars
- Artifacts hub: upload a file from the computer; make a skill by uploading a
  Markdown file, files, a folder or a .zip/.skill, or by writing it in the
  dialog; add plugins by picking from the list a marketplace link answers
  (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub.
- Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid).
- Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB,
  <data dir>/avatars/), shown with name and role at the sidebar's foot and on
  the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at).
- Users page: per-user storage (GET /api/users/storage, measured in a worker)
  and account-pool spend/tokens per account (server/lib/pool-usage.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:21:33 +07:00

188 lines
8.0 KiB
JavaScript

/**
* @file claude-pool.test.js
* @description The Claude account pool (server/lib/claude-pool.js): token
* validation and file modes, the balancer's order (full quota last, then fewest
* live panes, then least recent spend, expired accounts never), and spawnRun
* handing the pick to a Claude pane through tmux `-e CLAUDE_CODE_OAUTH_TOKEN`
* and recording it — but not for a Codex pane, and not with an empty pool.
* @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.mkdtempSync(path.join(os.tmpdir(), "claude-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 ccUsage = require("../lib/cc-usage");
const pool = require("../lib/claude-pool");
const tmux = require("../lib/tmux");
const pty = require("../lib/pty-run");
const TOKEN = (c) => `sk-ant-oat01-${c.repeat(40)}`;
const realFetchUsage = ccUsage.fetchUsage;
function run(id, account, { status = "completed", session = null, started, ended = null } = {}) {
db.prepare(
`INSERT INTO dashboard_runs (id, session_id, mode, cwd, status, started_at, ended_at, account)
VALUES (?, ?, 'interactive', '/tmp', ?, ?, ?, ?)`
).run(id, session, status, started || new Date().toISOString(), ended, account);
}
const MODEL = "claude-sonnet-4-5-20250929";
const hoursAgo = (h) => new Date(Date.now() - h * 3600e3).toISOString();
/**
* `tokens` output tokens of `session` spent `atHoursAgo` hours ago — the
* hourly row plus the session total, as the hook writer keeps them both.
* `{hourly: false}` = a total with no hourly row (a sub-agent's bucket).
*/
function spend(session, tokens, { atHoursAgo = 0, hourly = true, model = MODEL } = {}) {
db.prepare(
"INSERT OR IGNORE INTO sessions (id, name, status, cwd, started_at) VALUES (?, ?, 'completed', '/tmp', ?)"
).run(session, session, hoursAgo(atHoursAgo));
db.prepare(
`INSERT INTO token_usage (session_id, model, output_tokens) VALUES (?, ?, ?)
ON CONFLICT DO UPDATE SET output_tokens = output_tokens + excluded.output_tokens`
).run(session, model, tokens);
if (!hourly) return;
db.prepare(
`INSERT INTO token_usage_hourly (session_id, hour, model, output_tokens) VALUES (?, ?, ?, ?)`
).run(session, hoursAgo(atHoursAgo).slice(0, 13), model, tokens);
}
before(() => {
// No network: every quota read answers "no reading" unless a test says otherwise.
ccUsage.fetchUsage = async () => {
throw Object.assign(new Error("offline"), { code: "EUNAUTHORIZED" });
};
});
after(() => {
ccUsage.fetchUsage = realFetchUsage;
tmux.__setExecImpl(null);
fs.rmSync(TMP, { recursive: true, force: true });
});
describe("claude pool", () => {
beforeEach(() => {
fs.rmSync(pool.poolDir(), { recursive: true, force: true });
db.prepare("DELETE FROM dashboard_runs").run();
db.prepare("DELETE FROM token_usage_hourly").run();
db.prepare("DELETE FROM token_usage").run();
db.prepare("DELETE FROM sessions").run();
pool.__quotaCache.clear();
});
it("is empty until an admin adds an account, and stores it 0600", () => {
assert.equal(pool.pickAccount(), null);
assert.throws(() => pool.addAccount("a", "not-a-token"), { code: "EBADTOKEN" });
assert.throws(() => pool.addAccount("../x", TOKEN("a")), { code: "EBADNAME" });
const added = pool.addAccount("a", TOKEN("a"));
assert.ok(Date.parse(added.expiresAt) > Date.now() + 360 * 864e5);
assert.throws(() => pool.addAccount("a", TOKEN("b")), { code: "EEXISTS" });
const mode = fs.statSync(path.join(pool.poolDir(), "a.json")).mode & 0o777;
assert.equal(mode, 0o600);
assert.equal(fs.statSync(pool.poolDir()).mode & 0o777, 0o700);
assert.ok(
!JSON.stringify(pool.poolStatus()).includes(TOKEN("a")),
"status never shows a token"
);
pool.removeAccount("a");
assert.equal(pool.pickAccount(), null);
});
it("prefers fewer live panes, then less recent spend, and skips expired tokens", () => {
for (const n of ["a", "b", "c"]) pool.addAccount(n, TOKEN(n));
assert.equal(pool.pickAccount().name, "a");
run("r1", "a", { status: "running" });
assert.equal(pool.pickAccount().name, "b");
run("r2", "b", { session: "s-b" });
spend("s-b", 100000);
const picked = pool.pickAccount();
assert.equal(picked.name, "c");
assert.equal(picked.token, TOKEN("c"));
const status = Object.fromEntries(pool.poolStatus().map((a) => [a.name, a]));
assert.equal(status.a.livePanes, 1);
assert.ok(status.b.spend5h > 0 && status.b.spend7d >= status.b.spend5h);
assert.equal(status.c.spend5h, 0);
// c's token is a year old: never handed out.
const file = path.join(pool.poolDir(), "c.json");
const rec = JSON.parse(fs.readFileSync(file, "utf8"));
rec.addedAt = new Date(Date.now() - 366 * 864e5).toISOString();
fs.writeFileSync(file, JSON.stringify(rec));
assert.equal(pool.pickAccount().name, "b");
});
it("books a session to an account only for the hours its runs held it, sub-agents included", () => {
for (const n of ["a", "b"]) pool.addAccount(n, TOKEN(n));
// s1 ran on a 30 h ago, then was resumed on b 2 h ago.
run("r-a", "a", { session: "s1", started: hoursAgo(30), ended: hoursAgo(29) });
run("r-b", "b", { session: "s1", started: hoursAgo(2), status: "running" });
spend("s1", 1000, { atHoursAgo: 30 });
spend("s1", 3000, { atHoursAgo: 2 });
// s2 (on a, now): its main agent and a Haiku sub-agent with no hourly row.
run("r-a2", "a", { session: "s2" });
spend("s2", 500);
spend("s2", 200, { hourly: false, model: "claude-haiku-4-5-20251001" });
const status = Object.fromEntries(pool.poolStatus().map((x) => [x.name, x]));
assert.equal(status.a.tokens5h, 700, "s2, sub-agent included; s1 is b's now");
assert.equal(status.a.tokens7d, 1700);
assert.equal(status.b.tokens5h, 3000, "not the hours a held s1");
assert.equal(status.b.tokens7d, 3000);
assert.ok(status.a.spend7d > status.a.spend5h && status.a.spend5h > 0);
assert.ok(status.b.spend5h > 0);
});
it("puts an account whose quota is nearly used up last", async () => {
for (const n of ["a", "b"]) pool.addAccount(n, TOKEN(n));
ccUsage.fetchUsage = async (token) => ({
fiveHour: { utilization: token === TOKEN("a") ? 95 : 10, resetsAt: null },
sevenDay: { utilization: 20, resetsAt: null },
});
try {
await pool.refreshQuotas({ force: true });
run("r1", "b", { status: "running" }); // b busier, but a is full
assert.equal(pool.pickAccount().name, "b");
assert.equal(pool.poolStatus().find((x) => x.name === "a").full, true);
} finally {
ccUsage.fetchUsage = async () => {
throw Object.assign(new Error("offline"), { code: "EUNAUTHORIZED" });
};
}
});
it("gives a Claude pane the picked account's token and records it", () => {
pool.addAccount("a", TOKEN("a"));
let argv = null;
tmux.__setExecImpl((args) => {
if (args[0] === "has-session") throw Object.assign(new Error("gone"), { status: 1 });
if (args[0] === "new-session") argv = args;
return "";
});
pty.spawnRun({ laneId: 7, cwd: "/tmp", tool: "claude" });
assert.ok(argv.includes(`CLAUDE_CODE_OAUTH_TOKEN=${TOKEN("a")}`));
assert.ok(argv.indexOf(`CLAUDE_CODE_OAUTH_TOKEN=${TOKEN("a")}`) < argv.indexOf("--"));
const row = db.prepare("SELECT account FROM dashboard_runs WHERE id = 'agenthub-lane-7'").get();
assert.equal(row.account, "a");
argv = null;
pty.spawnRun({ laneId: 8, cwd: "/tmp", tool: "codex" });
assert.ok(!argv.some((x) => String(x).startsWith("CLAUDE_CODE_OAUTH_TOKEN=")));
pool.removeAccount("a");
argv = null;
pty.spawnRun({ laneId: 9, cwd: "/tmp", tool: "claude" });
assert.ok(!argv.some((x) => String(x).startsWith("CLAUDE_CODE_OAUTH_TOKEN=")));
});
});