372 lines
13 KiB
TypeScript
372 lines
13 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const usage = await import("../../open-sse/services/usage.ts");
|
|
const { USAGE_SUPPORTED_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
|
|
|
test("USAGE_SUPPORTED_PROVIDERS includes opencode-go", () => {
|
|
assert.ok(
|
|
(USAGE_SUPPORTED_PROVIDERS as string[]).includes("opencode-go"),
|
|
"opencode-go must be in the usage-supported providers allowlist"
|
|
);
|
|
});
|
|
|
|
test("getUsageForProvider returns helpful message when opencode-go has no apiKey", async () => {
|
|
let called = false;
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => {
|
|
called = true;
|
|
return new Response("unexpected", { status: 500 });
|
|
};
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-no-key",
|
|
provider: "opencode-go",
|
|
apiKey: "",
|
|
})) as { message?: string };
|
|
|
|
assert.equal(called, false, "quota fetch must not run without an API key");
|
|
assert.match(result.message ?? "", /OpenCode Go/);
|
|
assert.match(result.message ?? "", /OPENCODE_GO_WORKSPACE_ID/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("getUsageForProvider exposes OpenCode Go 5h, weekly, and monthly quotas", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const reset5h = Date.now() + 2 * 60 * 60 * 1000;
|
|
const resetWeekly = Date.now() + 4 * 24 * 60 * 60 * 1000;
|
|
const resetMonthly = Date.now() + 20 * 24 * 60 * 60 * 1000;
|
|
let requestUrl = "";
|
|
let requestHeaders: Headers | null = null;
|
|
|
|
globalThis.fetch = async (input, init) => {
|
|
requestUrl = String(input);
|
|
requestHeaders = new Headers(init?.headers as HeadersInit | undefined);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
code: 200,
|
|
success: true,
|
|
data: {
|
|
level: "pro",
|
|
limits: [
|
|
{
|
|
type: "TOKENS_LIMIT",
|
|
unit: 3,
|
|
number: 5,
|
|
percentage: 25,
|
|
nextResetTime: reset5h,
|
|
},
|
|
{
|
|
type: "TOKENS_LIMIT",
|
|
unit: 6,
|
|
number: 1,
|
|
percentage: 50,
|
|
nextResetTime: resetWeekly,
|
|
},
|
|
{
|
|
type: "TIME_LIMIT",
|
|
percentage: 10,
|
|
currentValue: 6,
|
|
usage: 60,
|
|
nextResetTime: resetMonthly,
|
|
usageDetails: [{ modelCode: "search-prime", usage: 3 }],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-usage",
|
|
provider: "opencode-go",
|
|
apiKey: "Bearer opencode-go-key",
|
|
})) as {
|
|
plan?: string | null;
|
|
quotas?: Record<
|
|
string,
|
|
{
|
|
used: number;
|
|
total: number;
|
|
remaining: number;
|
|
remainingPercentage: number;
|
|
resetAt: string | null;
|
|
displayName?: string;
|
|
currency?: string;
|
|
details?: Array<{ name: string; used: number }>;
|
|
}
|
|
>;
|
|
};
|
|
|
|
assert.equal(requestUrl, "https://api.z.ai/api/monitor/usage/quota/limit");
|
|
assert.equal(requestHeaders?.get("Authorization"), "Bearer opencode-go-key");
|
|
assert.equal(requestHeaders?.get("Content-Type"), "application/json");
|
|
assert.equal(result.plan, "OpenCode Go Pro");
|
|
assert.deepEqual(Object.keys(result.quotas ?? {}), ["session", "weekly", "mcp_monthly"]);
|
|
|
|
assert.equal(result.quotas!.session.displayName, "5-hour rolling");
|
|
assert.equal(result.quotas!.session.currency, "USD");
|
|
assert.equal(result.quotas!.session.used, 3);
|
|
assert.equal(result.quotas!.session.total, 12);
|
|
assert.equal(result.quotas!.session.remaining, 9);
|
|
assert.equal(result.quotas!.session.remainingPercentage, 75);
|
|
assert.equal(result.quotas!.session.resetAt, new Date(reset5h).toISOString());
|
|
|
|
assert.equal(result.quotas!.weekly.displayName, "Weekly");
|
|
assert.equal(result.quotas!.weekly.used, 15);
|
|
assert.equal(result.quotas!.weekly.total, 30);
|
|
assert.equal(result.quotas!.weekly.remaining, 15);
|
|
assert.equal(result.quotas!.weekly.remainingPercentage, 50);
|
|
assert.equal(result.quotas!.weekly.resetAt, new Date(resetWeekly).toISOString());
|
|
|
|
assert.equal(result.quotas!.mcp_monthly.displayName, "Monthly");
|
|
assert.equal(result.quotas!.mcp_monthly.used, 6);
|
|
assert.equal(result.quotas!.mcp_monthly.total, 60);
|
|
assert.equal(result.quotas!.mcp_monthly.remaining, 54);
|
|
assert.equal(result.quotas!.mcp_monthly.remainingPercentage, 90);
|
|
assert.equal(result.quotas!.mcp_monthly.resetAt, new Date(resetMonthly).toISOString());
|
|
assert.deepEqual(result.quotas!.mcp_monthly.details, [{ name: "search-prime", used: 3 }]);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("getUsageForProvider ignores out-of-range OpenCode Go reset timestamps", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
code: 200,
|
|
success: true,
|
|
data: {
|
|
level: "pro",
|
|
limits: [
|
|
{
|
|
type: "TOKENS_LIMIT",
|
|
unit: 3,
|
|
number: 5,
|
|
percentage: 25,
|
|
nextResetTime: Number.MAX_VALUE,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-huge-reset",
|
|
provider: "opencode-go",
|
|
apiKey: "opencode-go-key",
|
|
})) as { quotas?: Record<string, { resetAt: string | null }> };
|
|
|
|
assert.equal(result.quotas!.session.resetAt, null);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("getUsageForProvider scrapes OpenCode Go dashboard quota when workspace cookie is configured", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalWorkspace = process.env.OPENCODE_GO_WORKSPACE_ID;
|
|
const originalCookie = process.env.OPENCODE_GO_AUTH_COOKIE;
|
|
let requestUrl = "";
|
|
let requestHeaders: Headers | null = null;
|
|
|
|
process.env.OPENCODE_GO_WORKSPACE_ID = "workspace-123";
|
|
process.env.OPENCODE_GO_AUTH_COOKIE = "auth-cookie-value";
|
|
|
|
globalThis.fetch = async (input, init) => {
|
|
requestUrl = String(input);
|
|
requestHeaders = new Headers(init?.headers as HeadersInit | undefined);
|
|
return new Response(
|
|
[
|
|
'<div data-slot="usage-item">',
|
|
'<span data-slot="usage-label">Rolling Usage</span>',
|
|
'<span data-slot="usage-value">25%</span>',
|
|
'<span data-slot="reset-time">Resets in 1 hour 30 minutes</span>',
|
|
"</div>",
|
|
'<div data-slot="usage-item">',
|
|
'<span data-slot="usage-label">Weekly Usage</span>',
|
|
'<span data-slot="usage-value">50%</span>',
|
|
'<span data-slot="reset-time">Resets in 2 days</span>',
|
|
"</div>",
|
|
'<div data-slot="usage-item">',
|
|
'<span data-slot="usage-label">Monthly Usage</span>',
|
|
'<span data-slot="usage-value">10%</span>',
|
|
'<span data-slot="reset-time">Resets in 10 days</span>',
|
|
"</div>",
|
|
].join(""),
|
|
{ status: 200, headers: { "content-type": "text/html" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-dashboard",
|
|
provider: "opencode-go",
|
|
apiKey: "opencode-go-key",
|
|
})) as {
|
|
plan?: string | null;
|
|
quotas?: Record<string, { used: number; total: number; remainingPercentage: number }>;
|
|
};
|
|
|
|
assert.equal(requestUrl, "https://opencode.ai/workspace/workspace-123/go");
|
|
assert.equal(requestHeaders?.get("Cookie"), "auth=auth-cookie-value");
|
|
assert.equal(result.plan, "OpenCode Go");
|
|
assert.deepEqual(Object.keys(result.quotas ?? {}), ["session", "weekly", "mcp_monthly"]);
|
|
assert.equal(result.quotas!.session.used, 3);
|
|
assert.equal(result.quotas!.session.total, 12);
|
|
assert.equal(result.quotas!.session.remainingPercentage, 75);
|
|
assert.equal(result.quotas!.weekly.used, 15);
|
|
assert.equal(result.quotas!.weekly.remainingPercentage, 50);
|
|
assert.equal(result.quotas!.mcp_monthly.used, 6);
|
|
assert.equal(result.quotas!.mcp_monthly.remainingPercentage, 90);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
if (originalWorkspace === undefined) delete process.env.OPENCODE_GO_WORKSPACE_ID;
|
|
else process.env.OPENCODE_GO_WORKSPACE_ID = originalWorkspace;
|
|
if (originalCookie === undefined) delete process.env.OPENCODE_GO_AUTH_COOKIE;
|
|
else process.env.OPENCODE_GO_AUTH_COOKIE = originalCookie;
|
|
}
|
|
});
|
|
|
|
// Regression: React SSR wraps the reset-time text in hydration comment markers
|
|
// (<!--$--> … <!--/-->). The reset string must be fully sanitized (complete <!--...-->
|
|
// removal, not just the two literal markers) so the reset time still parses — and so no
|
|
// partial "<!--" survives (CodeQL js/incomplete-multi-character-sanitization).
|
|
test("getUsageForProvider parses OpenCode Go reset-time wrapped in React hydration comments", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalWorkspace = process.env.OPENCODE_GO_WORKSPACE_ID;
|
|
const originalCookie = process.env.OPENCODE_GO_AUTH_COOKIE;
|
|
|
|
process.env.OPENCODE_GO_WORKSPACE_ID = "workspace-123";
|
|
process.env.OPENCODE_GO_AUTH_COOKIE = "auth-cookie-value";
|
|
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
[
|
|
'<div data-slot="usage-item">',
|
|
'<span data-slot="usage-label">Rolling Usage</span>',
|
|
'<span data-slot="usage-value">25%</span>',
|
|
'<span data-slot="reset-time"><!--$-->Resets in 1 hour 30 minutes<!--/--></span>',
|
|
"</div>",
|
|
].join(""),
|
|
{ status: 200, headers: { "content-type": "text/html" } }
|
|
);
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-dashboard",
|
|
provider: "opencode-go",
|
|
apiKey: "opencode-go-key",
|
|
})) as {
|
|
quotas?: Record<string, { used: number; total: number; remainingPercentage: number }>;
|
|
};
|
|
|
|
// session quota resolved → the comment-wrapped reset time was sanitized and parsed
|
|
assert.ok(
|
|
result.quotas?.session,
|
|
"session quota should resolve from comment-wrapped reset-time"
|
|
);
|
|
assert.equal(result.quotas!.session.remainingPercentage, 75);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
if (originalWorkspace === undefined) delete process.env.OPENCODE_GO_WORKSPACE_ID;
|
|
else process.env.OPENCODE_GO_WORKSPACE_ID = originalWorkspace;
|
|
if (originalCookie === undefined) delete process.env.OPENCODE_GO_AUTH_COOKIE;
|
|
else process.env.OPENCODE_GO_AUTH_COOKIE = originalCookie;
|
|
}
|
|
});
|
|
|
|
test("getUsageForProvider returns message for invalid OpenCode Go API keys", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => new Response("nope", { status: 401 });
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-401",
|
|
provider: "opencode-go",
|
|
apiKey: "bad-key",
|
|
})) as { message: string };
|
|
assert.equal(
|
|
result.message,
|
|
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
|
|
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping."
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("getUsageForProvider returns message when OpenCode Go quota fetch fails", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => {
|
|
throw new Error("network offline");
|
|
};
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-network-error",
|
|
provider: "opencode-go",
|
|
apiKey: "opencode-go-key",
|
|
})) as { message: string };
|
|
|
|
assert.match(result.message, /OpenCode Go quota API error:/);
|
|
assert.match(result.message, /network offline/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("getUsageForProvider returns message when OpenCode Go quota API returns 200 with auth error in body", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () =>
|
|
new Response(JSON.stringify({ code: 401, msg: "token expired or incorrect", success: false }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-body-401",
|
|
provider: "opencode-go",
|
|
apiKey: "sk-test-key",
|
|
})) as { message: string };
|
|
assert.equal(
|
|
result.message,
|
|
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
|
|
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping."
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("getUsageForProvider returns message when OpenCode Go quota response is invalid JSON", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () =>
|
|
new Response("<html>not json</html>", {
|
|
status: 200,
|
|
headers: { "content-type": "text/html" },
|
|
});
|
|
|
|
try {
|
|
const result = (await usage.getUsageForProvider({
|
|
id: "opencode-go-bad-json",
|
|
provider: "opencode-go",
|
|
apiKey: "sk-test-key",
|
|
})) as { message: string };
|
|
assert.equal(result.message, "OpenCode Go quota response parsing failed.");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|