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.
587 lines
21 KiB
JavaScript
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();
|
|
}
|
|
});
|