/** * @file Tests for the Claude Code statusline (`statusline/statusline.js`): the * line formatter, today's cost from a fake dashboard (cache hit, cache of a * failure, the 1-second timeout), the script end to end on stdin, and * `agenthub statusline install|uninstall` against a temp HOME — never the * real ~/.claude/settings.json. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const http = require("node:http"); const os = require("node:os"); const path = require("node:path"); const { spawn } = require("node:child_process"); const ROOT = path.resolve(__dirname, "..", ".."); const { format, contextPercent, todayCost, localDate } = require( path.join(ROOT, "statusline", "statusline.js") ); const { isOurs, statuslineCommand } = require(path.join(ROOT, "statusline", "install.js")); const plain = (s) => s.replace(/\x1b\[[0-9;]*m/g, ""); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-statusline-test-")); after(() => fs.rmSync(TMP, { recursive: true, force: true })); /** Spawn a node script asynchronously (a fake server may live in this process). */ function run(args, { env = {}, input = "" } = {}) { return new Promise((resolve) => { const child = spawn(process.execPath, args, { env: { ...process.env, ...env } }); let out = ""; let err = ""; child.stdout.on("data", (d) => (out += d)); child.stderr.on("data", (d) => (err += d)); child.on("close", (code) => resolve({ code, out, err })); child.stdin.end(input); }); } describe("format", () => { it("renders model · branch · context bar · session $ · today $", () => { const line = format( { model: { id: "claude-opus-5-5", display_name: "Opus 5.5" }, context_window: { used_percentage: 34 }, cost: { total_cost_usd: 1.234 }, }, { branch: "main", today: 45.678 } ); assert.equal(plain(line), "Opus 5.5 · main · ███░░░░░░░ 34% · $1.23 · today $45.68"); }); it("omits whatever is missing", () => { assert.equal(plain(format({ model: { display_name: "Sonnet" } }, { today: null })), "Sonnet"); assert.equal(format({}), ""); assert.equal(format(null), ""); assert.equal(plain(format({ cost: { total_cost_usd: 0 } })), "$0.00"); }); it("computes context % from usage when used_percentage is absent, clamped", () => { const ctx = { context_window_size: 200_000, current_usage: { input_tokens: 10_000, cache_read_input_tokens: 40_000, output_tokens: 999 }, }; assert.equal(contextPercent(ctx), 25); assert.equal(contextPercent({}), null); assert.match(plain(format({ context_window: { used_percentage: 140 } })), /██████████ 100%/); }); }); describe("todayCost", () => { let api; let url; let hits = 0; let slow = false; let lastUrl = ""; before(async () => { api = http.createServer((req, res) => { hits++; lastUrl = req.url; if (slow) return; // never answers — the timeout must fire res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ daily_costs: [ { date: localDate(), cost: 3.5 }, { date: "2000-01-01", cost: 9 }, ], }) ); }); await new Promise((r) => api.listen(0, "127.0.0.1", r)); url = `http://127.0.0.1:${api.address().port}`; }); after(() => { api.closeAllConnections(); api.close(); }); it("reads today's row and caches it", async () => { const file = path.join(TMP, "cache-a.json"); assert.equal(await todayCost({ url, file }), 3.5); assert.equal(lastUrl, `/api/pricing/cost?tz_offset=${new Date().getTimezoneOffset()}`); assert.equal(await todayCost({ url, file }), 3.5); assert.equal(hits, 1); // A different dashboard URL is a cache miss. await todayCost({ url: url.replace("127.0.0.1", "localhost"), file }); assert.equal(hits, 2); }); it("gives up after 1 s and caches the miss", async () => { slow = true; hits = 0; const file = path.join(TMP, "cache-b.json"); const t0 = Date.now(); assert.equal(await todayCost({ url, file }), null); assert.ok(Date.now() - t0 < 2_000); assert.equal(await todayCost({ url, file }), null); assert.equal(hits, 1); slow = false; }); }); describe("statusline.js on stdin", () => { it("prints one line, and nothing (exit 0) on garbage", async () => { const env = { AGENTHUB_URL: "http://127.0.0.1:9", TMPDIR: TMP }; const ok = await run([path.join(ROOT, "statusline", "statusline.js")], { env, input: JSON.stringify({ model: { display_name: "Opus" }, cwd: ROOT, cost: { total_cost_usd: 2 }, }), }); assert.equal(ok.code, 0); assert.match(plain(ok.out), /^Opus · \S+ · \$2\.00\n$/); const bad = await run([path.join(ROOT, "statusline", "statusline.js")], { env, input: "{nope", }); assert.equal(bad.code, 0); assert.equal(bad.out, "\n"); }); }); describe("agenthub statusline install|uninstall", () => { const HOME = path.join(TMP, "home"); const settings = path.join(HOME, ".claude", "settings.json"); const env = { HOME, CLAUDE_HOME: "", NO_COLOR: "1" }; const cli = (...a) => run([path.join(ROOT, "bin", "agenthub.js"), "statusline", ...a], { env }); const read = () => JSON.parse(fs.readFileSync(settings, "utf8")); const backups = () => fs.readdirSync(path.dirname(settings)).filter((f) => f.includes("agenthub-backup")); it("installs into a fresh HOME, then is idempotent", async () => { const r = await cli("install"); assert.equal(r.code, 0, r.err); assert.equal(read().statusLine.command, statuslineCommand()); assert.ok(isOurs(read().statusLine)); assert.equal(backups().length, 0); // nothing to back up const again = await cli("install"); assert.match(again.out, /already set/); assert.equal(backups().length, 0); }); it("refuses a foreign statusLine without --force, keeps other keys, backs up", async () => { fs.writeFileSync( settings, JSON.stringify({ model: "opus", statusLine: { type: "command", command: "~/my-line.sh" } }) ); const refused = await cli("install"); assert.equal(refused.code, 1); assert.match(refused.err, /not AgentHub's/); assert.equal(read().statusLine.command, "~/my-line.sh"); assert.equal((await cli("uninstall")).code, 1); assert.equal(read().statusLine.command, "~/my-line.sh"); const forced = await cli("install", "--force"); assert.equal(forced.code, 0, forced.err); assert.ok(isOurs(read().statusLine)); assert.equal(read().model, "opus"); const [bak] = backups(); assert.equal( JSON.parse(fs.readFileSync(path.join(path.dirname(settings), bak))).statusLine.command, "~/my-line.sh" ); }); it("uninstalls its own entry only", async () => { const r = await cli("uninstall"); assert.equal(r.code, 0, r.err); assert.equal(read().statusLine, undefined); assert.equal(read().model, "opus"); assert.match((await cli("uninstall")).out, /No statusLine/); }); it("uses the stable launcher for a plugin-cache install", () => { const home = path.join(TMP, "launcher-home"); fs.mkdirSync(path.join(home, ".local", "bin"), { recursive: true }); fs.writeFileSync(path.join(home, ".local", "bin", "agenthub"), "#!/bin/sh\n"); const root = path.join(home, ".claude", "plugins", "cache", "agenthub", "agenthub", "1.0.0"); const cmd = statuslineCommand(root, home); assert.equal(cmd, `"${path.join(home, ".local", "bin", "agenthub")}" statusline`); assert.ok(isOurs({ command: cmd })); assert.equal(isOurs({ command: "bash ~/statusline.sh" }), false); }); });