Files
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

587 lines
21 KiB
JavaScript

/**
* @file Tests server/lib/quota.js: a rollout's `rate_limits`
* window mapped to the status bar's label/percent/reset, and a window that has
* reset since Codex last wrote reading as empty, and the Codex login's
* account + expiry read from `auth.json`; and a saved Claude profile asked
* with its own token only while it is valid, never refreshed, and never shown
* with the usage of a token that belongs to a different account; each
* tool's plan name; the Cursor CLI's login asked of cursor.com only while
* valid; Antigravity's `agy --print /usage` pools, never on an old agy; and
* the Grok, Kimi Code and OpenCode Go logins, each sent only to its own host
* while valid, never refreshed.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { test } = require("node:test");
const assert = require("node:assert");
const { codexWindow, windowLabel } = require("../lib/quota");
// No test may read (or send) the machine's real Cursor login.
process.env.XDG_CONFIG_HOME = require("node:fs").mkdtempSync(
require("node:path").join(require("node:os").tmpdir(), "quota-xdg-")
);
// Nor run the machine's real agy: only the Antigravity test's fake one runs.
process.env.AGENTHUB_AGY_USAGE = "0";
// Nor its real Grok, Kimi or OpenCode logins.
for (const k of ["GROK_HOME", "KIMI_CODE_HOME", "XDG_DATA_HOME"]) {
process.env[k] = require("node:fs").mkdtempSync(
require("node:path").join(require("node:os").tmpdir(), "quota-home-")
);
}
test("windowLabel names the plan windows", () => {
assert.strictEqual(windowLabel(300), "5h");
assert.strictEqual(windowLabel(10080), "wk");
assert.strictEqual(windowLabel(43200), "mo");
assert.strictEqual(windowLabel(2880), "2d");
});
test("codexWindow keeps a live window and empties a reset one", () => {
const future = Math.floor(Date.now() / 1000) + 3600;
assert.deepStrictEqual(
codexWindow({ used_percent: 27, window_minutes: 300, resets_at: future }, Date.now()),
{ label: "5h", usedPercent: 27, resetsAt: new Date(future * 1000).toISOString() }
);
const past = codexWindow(
{ used_percent: 92, window_minutes: 10080, resets_in_seconds: 60 },
Date.now() - 120_000
);
assert.strictEqual(past.usedPercent, 0);
assert.strictEqual(codexWindow(null, 0), null);
});
test("codexAccount reads the email and access-token expiry, never a token", () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { codexAccount } = require("../lib/quota");
const jwt = (claims) => `h.${Buffer.from(JSON.stringify(claims)).toString("base64url")}.s`;
const home = fs.mkdtempSync(path.join(os.tmpdir(), "quota-codex-"));
const prev = process.env.CODEX_HOME;
process.env.CODEX_HOME = home;
try {
assert.deepStrictEqual(codexAccount(), {});
fs.writeFileSync(
path.join(home, "auth.json"),
JSON.stringify({
tokens: {
id_token: jwt({
email: "a@b.c",
"https://api.openai.com/auth": { chatgpt_plan_type: "plus" },
}),
access_token: jwt({ exp: 1_800_000_000 }),
},
})
);
assert.deepStrictEqual(codexAccount(), {
account: "a@b.c",
plan: "plus",
expiresAt: new Date(1_800_000_000_000).toISOString(),
});
} finally {
if (prev === undefined) delete process.env.CODEX_HOME;
else process.env.CODEX_HOME = prev;
fs.rmSync(home, { recursive: true, force: true });
}
});
test("a saved profile is asked with its own token only while valid, never refreshed", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { quotas, __resetCache } = require("../lib/quota");
const home = fs.mkdtempSync(path.join(os.tmpdir(), "quota-claude-"));
const dir = path.join(home, "agenthub-profiles");
fs.mkdirSync(dir);
const profile = (name, token, expiresAt) =>
fs.writeFileSync(
path.join(dir, `${name}.json`),
JSON.stringify({
label: name,
account: { accountUuid: name, emailAddress: `${name}@x.y` },
credentials: { accessToken: token, refreshToken: "r", expiresAt },
})
);
profile("fresh", "tok-fresh", Date.now() + 3600_000);
profile("stale", "tok-stale", Date.now() - 1000);
const prev = { home: process.env.CLAUDE_HOME, fetch: global.fetch };
const asked = [];
process.env.CLAUDE_HOME = home;
global.fetch = async (url, opts) => {
if (url.endsWith("/profile")) {
// The fresh profile's token is its own account's.
return { ok: true, json: async () => ({ account: { uuid: "fresh" } }) };
}
asked.push(`${url} ${opts.headers.Authorization}`);
return {
status: 200,
ok: true,
json: async () => ({ five_hour: { utilization: 40, resets_at: null } }),
};
};
try {
__resetCache();
const claude = (await quotas()).tools.filter((t) => t.id === "claude");
assert.deepStrictEqual(asked, ["https://api.anthropic.com/api/oauth/usage Bearer tok-fresh"]);
const by = Object.fromEntries(claude.map((t) => [t.label, t]));
assert.strictEqual(by.fresh.active, false);
assert.strictEqual(by.fresh.windows[0].usedPercent, 40);
assert.strictEqual(by.stale.error, "EPROFILEEXPIRED");
assert.strictEqual(by.stale.account, "stale@x.y");
} finally {
if (prev.home === undefined) delete process.env.CLAUDE_HOME;
else process.env.CLAUDE_HOME = prev.home;
global.fetch = prev.fetch;
__resetCache();
fs.rmSync(home, { recursive: true, force: true });
}
});
test("a profile holding another account's token shows EWRONGACCOUNT, not that account's usage", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { quotas, __resetCache } = require("../lib/quota");
const home = fs.mkdtempSync(path.join(os.tmpdir(), "quota-owner-"));
const dir = path.join(home, "agenthub-profiles");
fs.mkdirSync(dir);
fs.writeFileSync(
path.join(dir, "victor.json"),
JSON.stringify({
label: "Victor",
account: { accountUuid: "victor", emailAddress: "victor@x.y" },
credentials: { accessToken: "tok", refreshToken: "r", expiresAt: Date.now() + 3600_000 },
})
);
const prev = { home: process.env.CLAUDE_HOME, fetch: global.fetch };
process.env.CLAUDE_HOME = home;
global.fetch = async (url) => ({
status: 200,
ok: true,
json: async () =>
url.endsWith("/profile")
? { account: { uuid: "thang" } }
: { five_hour: { utilization: 30, resets_at: null } },
});
try {
__resetCache();
const [victor] = (await quotas()).tools.filter((t) => t.id === "claude");
assert.strictEqual(victor.label, "Victor");
assert.strictEqual(victor.error, "EWRONGACCOUNT");
assert.deepStrictEqual(victor.windows, []);
} finally {
if (prev.home === undefined) delete process.env.CLAUDE_HOME;
else process.env.CLAUDE_HOME = prev.home;
global.fetch = prev.fetch;
__resetCache();
fs.rmSync(home, { recursive: true, force: true });
}
});
test("the live row is the account its token belongs to, not the one .claude.json names", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { quotas, __resetCache } = require("../lib/quota");
const root = fs.mkdtempSync(path.join(os.tmpdir(), "quota-live-"));
const home = path.join(root, ".claude");
const dir = path.join(home, "agenthub-profiles");
fs.mkdirSync(dir, { recursive: true });
const save = (name, label, uuid) =>
fs.writeFileSync(
path.join(dir, `${name}.json`),
JSON.stringify({
label,
account: { accountUuid: uuid, emailAddress: `${name}@x.y` },
credentials: { accessToken: `tok-${name}`, refreshToken: "r", expiresAt: 1 },
})
);
save("thang", "Thắng", "uuid-t");
save("victor", "Victor", "uuid-v");
// Thắng's identity, Victor's token.
fs.writeFileSync(
path.join(home, ".credentials.json"),
JSON.stringify({
claudeAiOauth: {
accessToken: "live-v",
expiresAt: Date.now() + 3600_000,
subscriptionType: "max",
},
})
);
fs.writeFileSync(
`${home}.json`,
JSON.stringify({ oauthAccount: { accountUuid: "uuid-t", emailAddress: "thang@x.y" } })
);
const prev = { home: process.env.CLAUDE_HOME, fetch: global.fetch };
process.env.CLAUDE_HOME = home;
global.fetch = async (url) => ({
status: 200,
ok: true,
json: async () =>
url.endsWith("/profile")
? { account: { uuid: "uuid-v", email: "victor@x.y" } }
: { five_hour: { utilization: 82, resets_at: null } },
});
try {
__resetCache();
const claude = (await quotas()).tools.filter((t) => t.id === "claude");
const live = claude.find((t) => t.active);
assert.strictEqual(live.label, "Victor");
assert.strictEqual(live.account, "victor@x.y");
assert.strictEqual(live.ownerUuid, undefined);
assert.strictEqual(live.plan, "max");
assert.deepStrictEqual(
claude.filter((t) => !t.active).map((t) => t.label),
["Thắng"]
);
} finally {
if (prev.home === undefined) delete process.env.CLAUDE_HOME;
else process.env.CLAUDE_HOME = prev.home;
global.fetch = prev.fetch;
__resetCache();
fs.rmSync(root, { recursive: true, force: true });
}
});
test("Cursor: the CLI's own login asks cursor.com for its plan + cycle usage, never past expiry", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { quotas, __resetCache } = require("../lib/quota");
const jwt = (claims) => `h.${Buffer.from(JSON.stringify(claims)).toString("base64url")}.s`;
const root = fs.mkdtempSync(path.join(os.tmpdir(), "quota-cursor-"));
const auth = path.join(root, "config", "cursor", "auth.json");
fs.mkdirSync(path.dirname(auth), { recursive: true });
fs.mkdirSync(path.join(root, "cursor"));
fs.writeFileSync(
path.join(root, "cursor", "cli-config.json"),
JSON.stringify({ authInfo: { email: "c@d.e", authId: "auth0|user_1" } })
);
const prev = {
xdg: process.env.XDG_CONFIG_HOME,
dir: process.env.AGENTHUB_CURSOR_DIR,
fetch: global.fetch,
};
process.env.XDG_CONFIG_HOME = path.join(root, "config");
process.env.AGENTHUB_CURSOR_DIR = path.join(root, "cursor");
const asked = [];
global.fetch = async (url, opts) => {
if (!url.includes("cursor.com")) return { ok: false, status: 401, json: async () => ({}) };
asked.push(`${url} ${opts.headers.Cookie}`);
return {
ok: true,
status: 200,
json: async () => ({
billingCycleEnd: "2099-01-01T00:00:00.000Z",
membershipType: "pro",
individualUsage: { plan: { totalPercentUsed: 9.5 } },
}),
};
};
const cursor = async () => (await quotas()).tools.find((t) => t.id === "cursor");
try {
__resetCache();
assert.strictEqual(await cursor(), undefined); // not logged in: no row
const token = jwt({ sub: "auth0|user_1", exp: Math.floor(Date.now() / 1000) + 3600 });
fs.writeFileSync(auth, JSON.stringify({ accessToken: token, refreshToken: "r" }));
__resetCache();
assert.deepStrictEqual(await cursor(), {
id: "cursor",
active: true,
account: "c@d.e",
plan: "pro",
expiresAt: new Date(
JSON.parse(Buffer.from(token.split(".")[1], "base64url")).exp * 1000
).toISOString(),
windows: [{ label: "mo", usedPercent: 9.5, resetsAt: "2099-01-01T00:00:00.000Z" }],
});
assert.deepStrictEqual(asked, [
`https://cursor.com/api/usage-summary WorkosCursorSessionToken=user_1::${token}`,
]);
fs.writeFileSync(auth, JSON.stringify({ accessToken: jwt({ sub: "auth0|user_1", exp: 1 }) }));
__resetCache();
assert.strictEqual((await cursor()).error, "ETOKENEXPIRED");
assert.strictEqual(asked.length, 1);
} finally {
for (const [k, v] of [
["XDG_CONFIG_HOME", prev.xdg],
["AGENTHUB_CURSOR_DIR", prev.dir],
]) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
global.fetch = prev.fetch;
__resetCache();
fs.rmSync(root, { recursive: true, force: true });
}
});
test("Antigravity: `agy --print /usage` in an empty dir, one row per pool window, never on an old agy", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const aiTools = require("../lib/ai-tools");
const { quotas, __resetCache } = require("../lib/quota");
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "quota-agy-"));
const log = path.join(bin, "calls");
const version = path.join(bin, "version");
fs.writeFileSync(version, "1.2.17");
const bucket = (id, window, rf) => ({
id,
window,
remaining_fraction: rf,
reset_time: "2099-01-01T00:00:00Z",
});
fs.writeFileSync(
path.join(bin, "out.json"),
JSON.stringify({
status: "SUCCESS",
num_turns: 0,
command: {
name: "usage",
data: {
groups: [
{
name: "Gemini Models",
buckets: [bucket("g-w", "weekly", 0.75), bucket("g-5", "5h", 1)],
},
{ name: "Claude and GPT models", buckets: [bucket("c-5", "5h", 0.5)] },
],
},
},
})
);
fs.writeFileSync(
path.join(bin, "agy"),
`#!/bin/sh\necho "$PWD $*" >> '${log}'\n[ "$1" = --version ] && { cat '${version}'; exit 0; }\ncat '${path.join(bin, "out.json")}'\n`,
{ mode: 0o755 }
);
const prevPath = process.env.PATH;
process.env.PATH = `${bin}:${prevPath}`;
delete process.env.AGENTHUB_AGY_USAGE;
const agy = async () => (await quotas()).tools.find((t) => t.id === "antigravity");
try {
aiTools.refresh();
__resetCache();
const row = await agy();
assert.strictEqual(row.active, true);
assert.deepStrictEqual(row.windows, [
{ group: "Gemini Models", label: "5h", usedPercent: 0, resetsAt: "2099-01-01T00:00:00.000Z" },
{
group: "Gemini Models",
label: "wk",
usedPercent: 25,
resetsAt: "2099-01-01T00:00:00.000Z",
},
{
group: "Claude and GPT models",
label: "5h",
usedPercent: 50,
resetsAt: "2099-01-01T00:00:00.000Z",
},
]);
const calls = fs.readFileSync(log, "utf8").trim().split("\n");
assert.strictEqual(calls[0].split(" ").slice(1).join(" "), "--version");
const [cwd, ...args] = calls[1].split(" ");
assert.deepStrictEqual(args, ["--print", "/usage", "--output-format", "json"]);
assert.notStrictEqual(cwd, process.cwd());
assert.strictEqual(fs.existsSync(cwd), false); // the private dir is gone again
// An agy older than 1.1.11 may bill /usage as a model request: never asked.
fs.writeFileSync(version, "1.1.10");
fs.writeFileSync(log, "");
__resetCache();
assert.strictEqual(await agy(), undefined);
assert.match(fs.readFileSync(log, "utf8"), /^\S+ --version\n$/);
} finally {
process.env.PATH = prevPath;
process.env.AGENTHUB_AGY_USAGE = "0";
aiTools.refresh();
__resetCache();
fs.rmSync(bin, { recursive: true, force: true });
}
});
/** A fetch stub answering per host, recording every `url auth` it was asked. */
function stubFetch(answers) {
const asked = [];
const prev = global.fetch;
global.fetch = async (url, opts = {}) => {
asked.push(`${url} ${opts.headers?.Authorization || ""}`.trim());
const hit = Object.entries(answers).find(([k]) => url.startsWith(k));
if (!hit) return { ok: false, status: 404, json: async () => ({}) };
const [status, body] = hit[1];
return { ok: status < 300, status, json: async () => body };
};
return { asked, restore: () => (global.fetch = prev) };
}
test("Grok: the CLI's own auth.x.ai session asks cli-chat-proxy for credits + plan, never past expiry", async () => {
const fs = require("node:fs");
const path = require("node:path");
const { quotas, __resetCache } = require("../lib/quota");
const auth = path.join(process.env.GROK_HOME, "auth.json");
const f = stubFetch({
"https://cli-chat-proxy.grok.com/v1/billing?format=credits": [
200,
{
config: {
creditUsagePercent: 12,
currentPeriod: { end: "2099-01-08T00:00:00Z" },
subscriptionTier: "SuperGrok",
monthlyLimit: { val: "200" },
used: { val: "50" },
billingPeriodEnd: "2099-02-01T00:00:00Z",
},
},
],
});
const grok = async () => (await quotas()).tools.find((t) => t.id === "grok");
try {
__resetCache();
assert.strictEqual(await grok(), undefined);
const later = new Date(Date.now() + 3600_000).toISOString();
fs.writeFileSync(
auth,
JSON.stringify({
"https://other.example": { key: "tok-other", expires_at: later },
"https://auth.x.ai": { key: "tok-x", user_id: "u1", email: "g@x.ai", expires_at: later },
})
);
__resetCache();
assert.deepStrictEqual(await grok(), {
id: "grok",
active: true,
account: "g@x.ai",
plan: "SuperGrok",
expiresAt: later,
windows: [
{ label: "wk", usedPercent: 12, resetsAt: "2099-01-08T00:00:00.000Z" },
{ label: "mo", usedPercent: 25, resetsAt: "2099-02-01T00:00:00.000Z" },
],
});
assert.deepStrictEqual(
f.asked.filter((a) => a.includes("grok.com")),
["https://cli-chat-proxy.grok.com/v1/billing?format=credits Bearer tok-x"]
);
fs.writeFileSync(
auth,
JSON.stringify({ "https://auth.x.ai": { key: "tok-x", expires_at: "2000-01-01T00:00:00Z" } })
);
__resetCache();
assert.strictEqual((await grok()).error, "ETOKENEXPIRED");
assert.strictEqual(f.asked.filter((a) => a.includes("grok.com")).length, 1);
} finally {
f.restore();
fs.rmSync(auth, { force: true });
__resetCache();
}
});
test("Kimi Code: the CLI's own token asks api.kimi.com/coding for 5h, week and month", async () => {
const fs = require("node:fs");
const path = require("node:path");
const { quotas, __resetCache } = require("../lib/quota");
const dir = path.join(process.env.KIMI_CODE_HOME, "credentials");
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, "kimi-code.json");
const f = stubFetch({
"https://api.kimi.com/coding/v1/usages": [
200,
{
usages: {
limit_5h: { used_ratio: 0.3, reset_time: "2099-01-01T05:00:00Z" },
limit_7d: { used_ratio: "0.2", reset_time: "2099-01-07T00:00:00Z" },
limit_month_total: { used_ratio: 0.4, reset_time: "2099-02-01T00:00:00Z" },
limit_month_code: { used_ratio: 0.25, reset_time: "2099-02-01T00:00:00Z" },
},
},
],
});
const kimi = async () => (await quotas()).tools.find((t) => t.id === "kimi");
try {
__resetCache();
assert.strictEqual(await kimi(), undefined);
const exp = Math.floor(Date.now() / 1000) + 600;
fs.writeFileSync(
file,
JSON.stringify({ access_token: "tok-k", refresh_token: "r", expires_at: exp })
);
__resetCache();
const row = await kimi();
assert.strictEqual(row.expiresAt, new Date(exp * 1000).toISOString());
assert.deepStrictEqual(
row.windows.map((w) => [w.label, Math.round(w.usedPercent)]),
[
["5h", 30],
["wk", 20],
["mo", 40],
]
);
assert.deepStrictEqual(
f.asked.filter((a) => a.includes("kimi")),
["https://api.kimi.com/coding/v1/usages Bearer tok-k"]
);
fs.writeFileSync(file, JSON.stringify({ access_token: "tok-k", expires_at: 1 }));
__resetCache();
assert.strictEqual((await kimi()).error, "ETOKENEXPIRED");
assert.strictEqual(f.asked.filter((a) => a.includes("kimi")).length, 1);
} finally {
f.restore();
fs.rmSync(file, { force: true });
__resetCache();
}
});
test("OpenCode Go: opencode's own `opencode-go` key asks opencode.ai for its meters", async () => {
const fs = require("node:fs");
const path = require("node:path");
const { quotas, __resetCache } = require("../lib/quota");
const dir = path.join(process.env.XDG_DATA_HOME, "opencode");
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, "auth.json");
const f = stubFetch({
"https://opencode.ai/zen/go/v1/usage": [
200,
{
usage: {
rolling: { percent: 10, resetsAt: "2099-01-01T05:00:00Z" },
weekly: { percent: 55.5, resetsAt: "2099-01-07T00:00:00Z" },
},
},
],
});
const go = async () => (await quotas()).tools.find((t) => t.id === "opencode");
try {
__resetCache();
// A login with no Go key (Copilot through opencode, say) is no row.
fs.writeFileSync(file, JSON.stringify({ "github-copilot": { type: "oauth", access: "a" } }));
assert.strictEqual(await go(), undefined);
fs.writeFileSync(
file,
JSON.stringify({
"opencode-go": { type: "api", key: "sk-go" },
other: { type: "api", key: "x" },
})
);
__resetCache();
assert.deepStrictEqual(await go(), {
id: "opencode",
active: true,
plan: "Go",
windows: [
{ label: "5h", usedPercent: 10, resetsAt: "2099-01-01T05:00:00.000Z" },
{ label: "wk", usedPercent: 55.5, resetsAt: "2099-01-07T00:00:00.000Z" },
],
});
assert.deepStrictEqual(
f.asked.filter((a) => a.includes("opencode")),
["https://opencode.ai/zen/go/v1/usage Bearer sk-go"]
);
} finally {
f.restore();
fs.rmSync(file, { force: true });
__resetCache();
}
});