Files
2026-07-13 13:39:12 +08:00

158 lines
5.9 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.ts";
import { isSupportedUsageConnection } from "../../src/lib/usage/providerLimits.ts";
import {
__clearQoderJobTokenCache,
parseQoderJobTokenResponse,
} from "../../open-sse/services/qoderCli.ts";
const usageModule = await import("../../open-sse/services/usage.ts");
const { parseQoderUserStatusUsage } = usageModule.__testing;
// A Teams seat draws from a pooled org quota — `quota: 0` here means "pooled",
// not "exhausted", so it must render as an unlimited plan entry, not a 0-left one.
test("parseQoderUserStatusUsage maps a Teams/pooled account to an unlimited plan entry", () => {
const { plan, quotas } = parseQoderUserStatusUsage({
userType: "teams",
userTag: "Teams",
plan: "PLAN_TIER_TEAM",
quota: 0,
isQuotaExceeded: false,
nextResetAt: 1784736000000,
});
assert.equal(plan, "Teams");
assert.deepEqual(Object.keys(quotas), ["Plan"]);
assert.equal(quotas.Plan.unlimited, true);
assert.equal(quotas.Plan.remaining, 0);
assert.match(quotas.Plan.displayName!, /Teams plan · pooled/);
// nextResetAt (ms) must be surfaced as an ISO reset timestamp.
assert.equal(quotas.Plan.resetAt, new Date(1784736000000).toISOString());
// Regression: a pooled/unlimited window MUST report 100% remaining, else the
// quota→routing conversion (which ignores `unlimited`) treats total:0 as 0%
// and 429-blocks every request. See src/domain/quotaCache.ts.
assert.equal(quotas.Plan.remainingPercentage, 100);
});
test("parseQoderUserStatusUsage maps an individual plan with remaining quota to a Requests entry", () => {
const { plan, quotas } = parseQoderUserStatusUsage({
userType: "individual",
plan: "PLAN_TIER_PRO",
quota: 42,
isQuotaExceeded: false,
nextResetAt: 1784736000000,
});
// No userTag → prettified from the PLAN_TIER_* enum.
assert.equal(plan, "Pro");
assert.deepEqual(Object.keys(quotas), ["Requests"]);
assert.equal(quotas.Requests.remaining, 42);
assert.equal(quotas.Requests.total, 42);
assert.equal(quotas.Requests.unlimited, false);
});
test("parseQoderUserStatusUsage flags an exceeded quota", () => {
const { quotas } = parseQoderUserStatusUsage({
userType: "individual",
plan: "PLAN_TIER_FREE",
quota: 100,
isQuotaExceeded: true,
nextResetAt: 1784736000000,
});
assert.deepEqual(Object.keys(quotas), ["Quota"]);
assert.equal(quotas.Quota.remaining, 0);
assert.equal(quotas.Quota.unlimited, false);
assert.match(quotas.Quota.displayName!, /exceeded/i);
});
test("getUsageForProvider (qoder) exchanges the PAT then reads /user/status", async () => {
__clearQoderJobTokenCache();
const originalFetch = globalThis.fetch;
const calls: string[] = [];
// @ts-ignore — test stub
globalThis.fetch = async (url: string, init?: Record<string, unknown>) => {
const u = String(url);
calls.push(u);
if (u.includes("/jobToken/exchange")) {
assert.deepEqual(JSON.parse(String(init?.body ?? "{}")), {
personal_token: "pt-usage-token",
});
return new Response(JSON.stringify({ token: "jt-usage", expires_in: 86400 }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
// /api/v3/user/status — assert the jt-* is carried, return a Teams payload.
assert.match(u, /openapi\.qoder\.sh\/api\/v3\/user\/status/);
assert.equal(
String((init?.headers as Record<string, string>)?.Authorization),
"Bearer jt-usage"
);
return new Response(
JSON.stringify({
userType: "teams",
userTag: "Teams",
plan: "PLAN_TIER_TEAM",
quota: 0,
isQuotaExceeded: false,
nextResetAt: 1784736000000,
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
};
try {
const result = (await usageModule.getUsageForProvider({
provider: "qoder",
apiKey: "pt-usage-token",
})) as { plan?: string; quotas?: Record<string, unknown> };
assert.equal(result.plan, "Teams");
assert.ok(result.quotas && Object.keys(result.quotas).length > 0);
// The exchange must precede the usage call.
const exchangeIdx = calls.findIndex((c) => c.includes("/jobToken/exchange"));
const statusIdx = calls.findIndex((c) => c.includes("/user/status"));
assert.ok(exchangeIdx >= 0 && statusIdx > exchangeIdx);
} finally {
globalThis.fetch = originalFetch;
__clearQoderJobTokenCache();
}
});
test("getUsageForProvider (qoder) without a token returns a friendly message", async () => {
const result = (await usageModule.getUsageForProvider({ provider: "qoder" })) as {
message?: string;
};
assert.match(result.message ?? "", /Personal Access Token/i);
});
test("qoder is registered for both the usage fetcher and the quota widget whitelist", () => {
assert.ok(USAGE_SUPPORTED_PROVIDERS.includes("qoder"));
assert.ok(usageModule.USAGE_FETCHER_PROVIDERS.includes("qoder"));
});
// A PAT connection is authType "apikey"; the provider-limits sync only picks up
// apikey connections whose provider is explicitly allow-listed — guard that qoder
// is, so the quota widget actually refreshes it (regression for the sync gate).
test("a qoder PAT (apikey) connection is picked up by the provider-limits sync", () => {
assert.equal(
isSupportedUsageConnection({ id: "c1", provider: "qoder", authType: "apikey" }),
true
);
// Sanity: an unrelated apikey provider not on the list is excluded.
assert.equal(
isSupportedUsageConnection({ id: "c2", provider: "some-random-provider", authType: "apikey" }),
false
);
});
// Guards the shared exchange contract the usage path relies on.
test("parseQoderJobTokenResponse reads the exchange token field", () => {
const parsed = parseQoderJobTokenResponse({ token: "jt-abc", expires_in: 86400 });
assert.equal(parsed?.jobToken, "jt-abc");
});