a9c71cd905
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.
101 lines
3.7 KiB
JavaScript
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");
|
|
});
|
|
});
|