Files
Claude-Code-Monitor/server/__tests__/subagent-cost.test.js
T
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

132 lines
4.6 KiB
JavaScript

/**
* @file Tests that a subagent's OWN cost is derived and surfaced per-agent.
*
* Subagent cards used to show the whole session's cost, which reads as if that
* one subagent cost the entire session's spend. The importer now stamps each
* subagent's own token buckets into its metadata, and the agent-list endpoints
* compute a per-agent `cost` from them (priced at current rates, like session
* cost). This suite verifies:
*
* 1. importSubagentFromJsonl stores the subagent's own token buckets in
* agent.metadata.tokens.
* 2. attachAgentCosts computes that subagent's cost from those buckets and the
* current pricing rules — independent of the session total.
* 3. A main agent (no per-agent tokens) gets cost 0 (its cost is the session
* total, shown separately).
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const fs = require("fs");
const os = require("os");
const TEST_DB = path.join(os.tmpdir(), `dashboard-subagent-cost-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
const dbModule = require("../db");
const { db, stmts } = dbModule;
const importHistory = require("../../scripts/import-history");
const { attachAgentCosts, calculateCost } = require("../routes/pricing");
const SESSION = "cccccccc-3333-4333-8333-cccccccccccc";
const MAIN = `${SESSION}-main`;
const SUB_AGENT_ID = "11112222-3333-4444-5555-666677778888";
const SUB_MODEL = "claude-haiku-4-5-20251001";
after(() => {
if (db) db.close();
for (const suffix of ["", "-wal", "-shm"]) {
try {
fs.unlinkSync(TEST_DB + suffix);
} catch {
/* ignore */
}
}
});
function writeJsonl(filePath, lines) {
fs.writeFileSync(filePath, lines.map((o) => JSON.stringify(o)).join("\n"));
}
/** A subagent transcript with one assistant turn carrying known Haiku usage. */
function subagentLines() {
const base = "2026-04-18T12:00:00.000Z";
return [
{ type: "user", sessionId: SESSION, timestamp: base, message: { content: "do the thing" } },
{
type: "assistant",
sessionId: SESSION,
timestamp: "2026-04-18T12:00:05.000Z",
message: {
model: SUB_MODEL,
content: [{ type: "text", text: "done" }],
usage: {
input_tokens: 1_000_000,
output_tokens: 500_000,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
},
];
}
let tmpDir;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subcost-"));
// Session + main agent so the subagent FK holds.
stmts.insertSession.run(
SESSION,
"Cost test session",
"completed",
"/tmp/x",
"claude-opus-4-8",
null
);
stmts.insertAgent.run(MAIN, SESSION, "Main Agent", "main", null, "completed", null, null, null);
// Deterministic Haiku pricing: $1 in / $5 out per MTok.
stmts.upsertPricing.run("claude-haiku%", "Claude Haiku 4.5", 1, 5, 0.1, 1.25, 2, 0, 0);
});
describe("per-subagent cost", () => {
it("stamps the subagent's own token buckets into its metadata on import", async () => {
const file = path.join(tmpDir, `agent-${SUB_AGENT_ID}.jsonl`);
writeJsonl(file, subagentLines());
const subData = await importHistory.parseSubagentFile(file);
assert.ok(subData, "subData parsed");
importHistory.importSubagentFromJsonl(dbModule, SESSION, MAIN, subData);
const row = stmts.getAgent.get(`${SESSION}-jsonl-${SUB_AGENT_ID}`);
assert.ok(row, "jsonl subagent row created");
const meta = JSON.parse(row.metadata);
assert.ok(Array.isArray(meta.tokens) && meta.tokens.length === 1, "one token bucket stored");
assert.equal(meta.tokens[0].model, SUB_MODEL);
assert.equal(meta.tokens[0].input_tokens, 1_000_000);
assert.equal(meta.tokens[0].output_tokens, 500_000);
});
it("computes the subagent's own cost from its buckets, not the session total", () => {
const agents = stmts.listAgentsBySession.all(SESSION);
const withCosts = attachAgentCosts(agents);
const sub = withCosts.find((a) => a.id === `${SESSION}-jsonl-${SUB_AGENT_ID}`);
const main = withCosts.find((a) => a.id === MAIN);
// 1M input @ $1 + 0.5M output @ $5 = $1 + $2.50 = $3.50.
assert.equal(sub.cost, 3.5);
// Cross-check against calculateCost directly.
const rules = stmts.listPricing.all();
assert.equal(
calculateCost(JSON.parse(sub.metadata).tokens, rules, "2026-04-18").total_cost,
3.5
);
// Main agent carries no per-agent tokens → 0 (its cost is the session total).
assert.equal(main.cost, 0);
});
});