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

115 lines
4.3 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 crof", () => {
assert.ok(
(USAGE_SUPPORTED_PROVIDERS as string[]).includes("crof"),
"crof must be in the usage-supported providers allowlist"
);
});
test("getUsageForProvider returns helpful message when crof has no apiKey", async () => {
const result = (await usage.getUsageForProvider({
id: "crof-no-key",
provider: "crof",
apiKey: "",
})) as { message?: string };
assert.ok(typeof result.message === "string");
assert.match(result.message!, /CrofAI/);
});
test("getUsageForProvider exposes Requests Today + Credits for subscription accounts", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(JSON.stringify({ usable_requests: 450, credits: 12.3456 }), {
status: 200,
headers: { "content-type": "application/json" },
});
try {
const result = (await usage.getUsageForProvider({
id: "crof-sub",
provider: "crof",
apiKey: "test-key",
})) as {
quotas?: Record<
string,
{
used: number;
total: number;
remaining: number;
displayName?: string;
unlimited: boolean;
resetAt?: string | null;
}
>;
};
assert.ok(result.quotas, "quotas must be returned");
assert.ok(result.quotas!["Requests Today"], "Requests Today quota must be present");
assert.equal(result.quotas!["Requests Today"].remaining, 450);
assert.match(result.quotas!["Requests Today"].displayName!, /450 left/);
// Total must default to the Pro-plan baseline (1000) so the dashboard's
// percentage formula reads "remaining/total" rather than 0% (red, depleted).
assert.equal(result.quotas!["Requests Today"].total, 1000);
assert.equal(result.quotas!["Requests Today"].used, 550);
// CrofAI does not return a reset timestamp; we synthesize next UTC midnight
// so the dashboard's countdown renders. Assert it parses to a future Date
// within the next 24h without pinning the exact boundary.
const resetAt = result.quotas!["Requests Today"].resetAt;
assert.equal(typeof resetAt, "string", "Requests Today.resetAt must be an ISO string");
const resetTs = Date.parse(resetAt!);
assert.ok(Number.isFinite(resetTs), "Requests Today.resetAt must parse");
const deltaMs = resetTs - Date.now();
assert.ok(deltaMs > 0, "Requests Today.resetAt must be in the future");
assert.ok(deltaMs <= 24 * 60 * 60 * 1000, "Requests Today.resetAt must be within 24h");
assert.ok(result.quotas!["Credits"], "Credits quota must be present");
assert.equal(result.quotas!["Credits"].unlimited, true);
assert.match(result.quotas!["Credits"].displayName!, /\$12\.3456/);
assert.equal(result.quotas!["Credits"].resetAt ?? null, null, "Credits has no reset bucket");
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider omits Requests Today on pay-as-you-go (usable_requests=null)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(JSON.stringify({ usable_requests: null, credits: 5.5 }), {
status: 200,
headers: { "content-type": "application/json" },
});
try {
const result = (await usage.getUsageForProvider({
id: "crof-payg",
provider: "crof",
apiKey: "test-key",
})) as { quotas?: Record<string, unknown> };
assert.ok(result.quotas);
assert.equal(
result.quotas!["Requests Today"],
undefined,
"no Requests Today quota when not on a subscription plan"
);
assert.ok(result.quotas!["Credits"]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider returns 401 message when crof rejects the api key", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response("nope", { status: 401 });
try {
const result = (await usage.getUsageForProvider({
id: "crof-401",
provider: "crof",
apiKey: "bad-key",
})) as { message?: string };
assert.match(result.message ?? "", /rejected/);
} finally {
globalThis.fetch = originalFetch;
}
});