Files
AgentHub/server/__tests__/tray.test.js
T
nntrivi2001 a9c71cd905 feat: v2.5.12 — plan and quota for every AI tool, Grok Build and Kimi Code, Quit AgentHub (#65)
Usage panel (status bar):
- every account shows its plan (Claude subscriptionType, Codex
  chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier)
- new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity
  (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs
  agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's
  own file, sent only to its own host while valid, never refreshed
- in-flight quota reads are shared between tabs

AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added;
Gemini CLI, Hermes and OpenClaw removed.

Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat
on an editor selection, Settings in three groups, JetBrains-style Changes
panel (ticked files/hunks, ignore, blame, file history), terminal font.

--no-verify: :3000 is taken by another dev server (lane-detect tests);
both suites run by hand — server 2636/2639 (the 3 are :3000), client
1131/1131.
2026-10-08 12:22:14 +07:00

101 lines
3.7 KiB
JavaScript

/**
* @file Tests the status-area icon's keeper (server/lib/tray.js): on by
* default, never started once the user hid it, one icon at a time, a stale
* heartbeat never trusted (its pid may be another program's by now), an icon
* from another install root replaced only at server start, and no retry loop
* for an icon that keeps dying. No real icon is spawned.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, beforeEach, after } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { EventEmitter } = require("node:events");
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-tray-"));
process.env.DASHBOARD_DATA_DIR = TMP;
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
process.env.DISPLAY = process.env.DISPLAY || ":99"; // guiEnv() on Linux
const tray = require("../lib/tray");
after(() => fs.rmSync(TMP, { recursive: true, force: true }));
function fakeSpawn() {
const calls = [];
const spawn = (cmd, args, opts) => {
calls.push({ cmd, args, opts });
const child = new EventEmitter();
child.unref = () => {};
return child;
};
return { spawn, calls };
}
describe("tray keeper", () => {
beforeEach(() => {
fs.rmSync(path.join(TMP, "runtime"), { recursive: true, force: true });
});
it("is on by default and starts scripts/tray.js detached", () => {
assert.equal(tray.isEnabled(), true);
const { spawn, calls } = fakeSpawn();
assert.equal(tray.ensureTray({ spawn }), "started");
assert.equal(calls.length, 1);
assert.match(calls[0].args[0], /scripts[\\/]tray\.js$/);
assert.equal(calls[0].opts.detached, true);
});
it("starts nothing once the user hid it", () => {
fs.mkdirSync(path.dirname(tray.offPath()), { recursive: true });
fs.writeFileSync(tray.offPath(), "x");
const { spawn, calls } = fakeSpawn();
assert.equal(tray.ensureTray({ spawn }), "skipped");
assert.equal(calls.length, 0);
});
it("leaves a live icon alone, and replaces one from another root only when asked", () => {
tray.writePid(process.pid, "/some/other/root");
const { spawn, calls } = fakeSpawn();
assert.equal(tray.ensureTray({ spawn }), "running");
assert.equal(calls.length, 0);
tray.writePid(process.pid); // this root: nothing to replace
assert.equal(tray.ensureTray({ spawn, replace: true }), "running");
assert.equal(calls.length, 0);
});
it("does not trust a heartbeat that went stale", () => {
tray.writePid(process.pid);
const old = new Date(Date.now() - 10 * tray.HEARTBEAT_MS);
fs.utimesSync(tray.pidPath(), old, old);
assert.equal(tray.liveTray(), null);
});
it("writes and clears the off flag", () => {
assert.equal(tray.setEnabled(false).enabled, false);
assert.equal(fs.existsSync(tray.offPath()), true);
tray.writePid(999_999_999); // no such process: nothing to signal, nothing to trust
const { spawn, calls } = fakeSpawn();
assert.equal(tray.setEnabled(true, { spawn }).enabled, true);
assert.equal(fs.existsSync(tray.offPath()), false);
assert.equal(calls.length, 1, "turning it on shows the icon at once");
});
it("gives up on an icon that keeps dying at once", () => {
const spawned = [];
const spawn = () => {
const child = new EventEmitter();
child.unref = () => {};
spawned.push(child);
return child;
};
for (let i = 0; i < 5; i++) {
if (tray.ensureTray({ spawn }) === "started") spawned.at(-1).emit("exit", 1);
}
assert.ok(spawned.length <= 4, `retried ${spawned.length} times`);
assert.equal(tray.ensureTray({ spawn }), "skipped");
});
});