mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-21 16:41:56 +00:00
test: dedupe provider usage tests and expand coverage
This commit is contained in:
@@ -64,6 +64,16 @@ describe("resolveProviderAuths key normalization", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function writeAuthProfiles(home: string, profiles: Record<string, unknown>) {
|
||||
const agentDir = path.join(home, ".openclaw", "agents", "main", "agent");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify({ version: 1, profiles }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
it("strips embedded CR/LF from env keys", async () => {
|
||||
await withSuiteHome(
|
||||
async () => {
|
||||
@@ -87,23 +97,10 @@ describe("resolveProviderAuths key normalization", () => {
|
||||
it("strips embedded CR/LF from stored auth profiles (token + api_key)", async () => {
|
||||
await withSuiteHome(
|
||||
async (home) => {
|
||||
const agentDir = path.join(home, ".openclaw", "agents", "main", "agent");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"minimax:default": { type: "token", provider: "minimax", token: "mini-\r\nmax" },
|
||||
"xiaomi:default": { type: "api_key", provider: "xiaomi", key: "xiao-\r\nmi" },
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await writeAuthProfiles(home, {
|
||||
"minimax:default": { type: "token", provider: "minimax", token: "mini-\r\nmax" },
|
||||
"xiaomi:default": { type: "api_key", provider: "xiaomi", key: "xiao-\r\nmi" },
|
||||
});
|
||||
|
||||
const auths = await resolveProviderAuths({
|
||||
providers: ["minimax", "xiaomi"],
|
||||
@@ -120,4 +117,67 @@ describe("resolveProviderAuths key normalization", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("returns injected auth values unchanged", async () => {
|
||||
const auths = await resolveProviderAuths({
|
||||
providers: ["anthropic"],
|
||||
auth: [{ provider: "anthropic", token: "token-1", accountId: "acc-1" }],
|
||||
});
|
||||
expect(auths).toEqual([{ provider: "anthropic", token: "token-1", accountId: "acc-1" }]);
|
||||
});
|
||||
|
||||
it("accepts z-ai env alias and normalizes embedded CR/LF", async () => {
|
||||
await withSuiteHome(
|
||||
async () => {
|
||||
const auths = await resolveProviderAuths({
|
||||
providers: ["zai"],
|
||||
});
|
||||
expect(auths).toEqual([{ provider: "zai", token: "zai-key" }]);
|
||||
},
|
||||
{
|
||||
ZAI_API_KEY: undefined,
|
||||
Z_AI_API_KEY: "zai-\r\nkey",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to legacy .pi auth file for zai keys", async () => {
|
||||
await withSuiteHome(
|
||||
async (home) => {
|
||||
const legacyDir = path.join(home, ".pi", "agent");
|
||||
await fs.mkdir(legacyDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(legacyDir, "auth.json"),
|
||||
`${JSON.stringify({ "z-ai": { access: "legacy-zai-key" } }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const auths = await resolveProviderAuths({
|
||||
providers: ["zai"],
|
||||
});
|
||||
expect(auths).toEqual([{ provider: "zai", token: "legacy-zai-key" }]);
|
||||
},
|
||||
{
|
||||
ZAI_API_KEY: undefined,
|
||||
Z_AI_API_KEY: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("extracts google oauth token from JSON payload in token profiles", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
await writeAuthProfiles(home, {
|
||||
"google-gemini-cli:default": {
|
||||
type: "token",
|
||||
provider: "google-gemini-cli",
|
||||
token: '{"token":"google-oauth-token"}',
|
||||
},
|
||||
});
|
||||
|
||||
const auths = await resolveProviderAuths({
|
||||
providers: ["google-gemini-cli"],
|
||||
});
|
||||
expect(auths).toEqual([{ provider: "google-gemini-cli", token: "google-oauth-token" }]);
|
||||
}, {});
|
||||
});
|
||||
});
|
||||
|
||||
110
src/infra/provider-usage.format.test.ts
Normal file
110
src/infra/provider-usage.format.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatUsageReportLines,
|
||||
formatUsageSummaryLine,
|
||||
formatUsageWindowSummary,
|
||||
} from "./provider-usage.format.js";
|
||||
import type { ProviderUsageSnapshot, UsageSummary } from "./provider-usage.types.js";
|
||||
|
||||
const now = Date.UTC(2026, 0, 7, 12, 0, 0);
|
||||
|
||||
function makeSnapshot(windows: ProviderUsageSnapshot["windows"]): ProviderUsageSnapshot {
|
||||
return {
|
||||
provider: "anthropic",
|
||||
displayName: "Claude",
|
||||
windows,
|
||||
};
|
||||
}
|
||||
|
||||
describe("provider-usage.format", () => {
|
||||
it("returns null summary for errored or empty snapshots", () => {
|
||||
expect(formatUsageWindowSummary({ ...makeSnapshot([]), error: "HTTP 401" })).toBeNull();
|
||||
expect(formatUsageWindowSummary(makeSnapshot([]))).toBeNull();
|
||||
});
|
||||
|
||||
it("formats reset windows across now/minute/hour/day/date buckets", () => {
|
||||
const summary = formatUsageWindowSummary(
|
||||
makeSnapshot([
|
||||
{ label: "Now", usedPercent: 10, resetAt: now - 1 },
|
||||
{ label: "Minute", usedPercent: 20, resetAt: now + 30 * 60_000 },
|
||||
{ label: "Hour", usedPercent: 30, resetAt: now + 2 * 60 * 60_000 + 15 * 60_000 },
|
||||
{ label: "Day", usedPercent: 40, resetAt: now + (2 * 24 + 3) * 60 * 60_000 },
|
||||
{ label: "Date", usedPercent: 50, resetAt: Date.UTC(2026, 0, 20, 12, 0, 0) },
|
||||
]),
|
||||
{ now, includeResets: true },
|
||||
);
|
||||
|
||||
expect(summary).toContain("Now 90% left ⏱now");
|
||||
expect(summary).toContain("Minute 80% left ⏱30m");
|
||||
expect(summary).toContain("Hour 70% left ⏱2h 15m");
|
||||
expect(summary).toContain("Day 60% left ⏱2d 3h");
|
||||
expect(summary).toMatch(/Date 50% left ⏱[A-Z][a-z]{2} \d{1,2}/);
|
||||
});
|
||||
|
||||
it("honors max windows and reset toggle", () => {
|
||||
const summary = formatUsageWindowSummary(
|
||||
makeSnapshot([
|
||||
{ label: "A", usedPercent: 10, resetAt: now + 60_000 },
|
||||
{ label: "B", usedPercent: 20, resetAt: now + 120_000 },
|
||||
{ label: "C", usedPercent: 30, resetAt: now + 180_000 },
|
||||
]),
|
||||
{ now, maxWindows: 2, includeResets: false },
|
||||
);
|
||||
|
||||
expect(summary).toBe("A 90% left · B 80% left");
|
||||
});
|
||||
|
||||
it("formats summary line from highest-usage window and provider cap", () => {
|
||||
const summary: UsageSummary = {
|
||||
updatedAt: now,
|
||||
providers: [
|
||||
{
|
||||
provider: "anthropic",
|
||||
displayName: "Claude",
|
||||
windows: [
|
||||
{ label: "5h", usedPercent: 20 },
|
||||
{ label: "Week", usedPercent: 70 },
|
||||
],
|
||||
},
|
||||
{
|
||||
provider: "zai",
|
||||
displayName: "z.ai",
|
||||
windows: [{ label: "Day", usedPercent: 10 }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(formatUsageSummaryLine(summary, { now, maxProviders: 1 })).toBe(
|
||||
"📊 Usage: Claude 30% left (Week)",
|
||||
);
|
||||
});
|
||||
|
||||
it("formats report output for empty, error, no-data, and plan entries", () => {
|
||||
expect(formatUsageReportLines({ updatedAt: now, providers: [] })).toEqual([
|
||||
"Usage: no provider usage available.",
|
||||
]);
|
||||
|
||||
const summary: UsageSummary = {
|
||||
updatedAt: now,
|
||||
providers: [
|
||||
{
|
||||
provider: "openai-codex",
|
||||
displayName: "Codex",
|
||||
windows: [],
|
||||
error: "Token expired",
|
||||
plan: "Plus",
|
||||
},
|
||||
{
|
||||
provider: "xiaomi",
|
||||
displayName: "Xiaomi",
|
||||
windows: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(formatUsageReportLines(summary)).toEqual([
|
||||
"Usage:",
|
||||
" Codex (Plus): Token expired",
|
||||
" Xiaomi: no data",
|
||||
]);
|
||||
});
|
||||
});
|
||||
27
src/infra/provider-usage.shared.test.ts
Normal file
27
src/infra/provider-usage.shared.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { clampPercent, resolveUsageProviderId, withTimeout } from "./provider-usage.shared.js";
|
||||
|
||||
describe("provider-usage.shared", () => {
|
||||
it("normalizes supported usage provider ids", () => {
|
||||
expect(resolveUsageProviderId("z-ai")).toBe("zai");
|
||||
expect(resolveUsageProviderId(" GOOGLE-ANTIGRAVITY ")).toBe("google-antigravity");
|
||||
expect(resolveUsageProviderId("unknown-provider")).toBeUndefined();
|
||||
expect(resolveUsageProviderId()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clamps usage percents and handles non-finite values", () => {
|
||||
expect(clampPercent(-5)).toBe(0);
|
||||
expect(clampPercent(120)).toBe(100);
|
||||
expect(clampPercent(Number.NaN)).toBe(0);
|
||||
expect(clampPercent(Number.POSITIVE_INFINITY)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns work result when it resolves before timeout", async () => {
|
||||
await expect(withTimeout(Promise.resolve("ok"), 100, "fallback")).resolves.toBe("ok");
|
||||
});
|
||||
|
||||
it("returns fallback when timeout wins", async () => {
|
||||
const late = new Promise<string>((resolve) => setTimeout(() => resolve("late"), 50));
|
||||
await expect(withTimeout(late, 1, "fallback")).resolves.toBe("fallback");
|
||||
});
|
||||
});
|
||||
@@ -3,28 +3,42 @@ import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js";
|
||||
import { createProviderUsageFetch, makeResponse } from "../test-utils/provider-usage-fetch.js";
|
||||
import {
|
||||
formatUsageReportLines,
|
||||
formatUsageSummaryLine,
|
||||
loadProviderUsageSummary,
|
||||
type UsageSummary,
|
||||
} from "./provider-usage.js";
|
||||
import { ignoredErrors } from "./provider-usage.shared.js";
|
||||
|
||||
const minimaxRemainsEndpoint = "api.minimaxi.com/v1/api/openplatform/coding_plan/remains";
|
||||
const usageNow = Date.UTC(2026, 0, 7, 0, 0, 0);
|
||||
type ProviderAuth = NonNullable<
|
||||
NonNullable<Parameters<typeof loadProviderUsageSummary>[0]>["auth"]
|
||||
>[number];
|
||||
|
||||
function makeResponse(status: number, body: unknown): Response {
|
||||
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
||||
const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" };
|
||||
return new Response(payload, { status, headers });
|
||||
async function loadUsageWithAuth(
|
||||
auth: ProviderAuth[],
|
||||
mockFetch: ReturnType<typeof createProviderUsageFetch>,
|
||||
) {
|
||||
return await loadProviderUsageSummary({
|
||||
now: usageNow,
|
||||
auth,
|
||||
fetch: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
}
|
||||
|
||||
function toRequestUrl(input: Parameters<typeof fetch>[0]): string {
|
||||
return typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
function expectSingleAnthropicProvider(summary: UsageSummary) {
|
||||
expect(summary.providers).toHaveLength(1);
|
||||
const claude = summary.providers[0];
|
||||
expect(claude?.provider).toBe("anthropic");
|
||||
return claude;
|
||||
}
|
||||
|
||||
function createMinimaxOnlyFetch(payload: unknown) {
|
||||
return vi.fn(async (input: string | Request | URL) => {
|
||||
if (toRequestUrl(input).includes(minimaxRemainsEndpoint)) {
|
||||
return createProviderUsageFetch(async (url) => {
|
||||
if (url.includes(minimaxRemainsEndpoint)) {
|
||||
return makeResponse(200, payload);
|
||||
}
|
||||
return makeResponse(404, "not found");
|
||||
@@ -38,11 +52,7 @@ async function expectMinimaxUsage(
|
||||
) {
|
||||
const mockFetch = createMinimaxOnlyFetch(payload);
|
||||
|
||||
const summary = await loadProviderUsageSummary({
|
||||
now: Date.UTC(2026, 0, 7, 0, 0, 0),
|
||||
auth: [{ provider: "minimax", token: "token-1b" }],
|
||||
fetch: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
const summary = await loadUsageWithAuth([{ provider: "minimax", token: "token-1b" }], mockFetch);
|
||||
|
||||
const minimax = summary.providers.find((p) => p.provider === "minimax");
|
||||
expect(minimax?.windows[0]?.usedPercent).toBe(expectedUsedPercent);
|
||||
@@ -113,8 +123,7 @@ describe("provider usage formatting", () => {
|
||||
|
||||
describe("provider usage loading", () => {
|
||||
it("loads usage snapshots with injected auth", async () => {
|
||||
const mockFetch = vi.fn(async (input: string | Request | URL) => {
|
||||
const url = toRequestUrl(input);
|
||||
const mockFetch = createProviderUsageFetch(async (url) => {
|
||||
if (url.includes("api.anthropic.com")) {
|
||||
return makeResponse(200, {
|
||||
five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" },
|
||||
@@ -152,15 +161,14 @@ describe("provider usage loading", () => {
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
|
||||
const summary = await loadProviderUsageSummary({
|
||||
now: Date.UTC(2026, 0, 7, 0, 0, 0),
|
||||
auth: [
|
||||
const summary = await loadUsageWithAuth(
|
||||
[
|
||||
{ provider: "anthropic", token: "token-1" },
|
||||
{ provider: "minimax", token: "token-1b" },
|
||||
{ provider: "zai", token: "token-2" },
|
||||
],
|
||||
fetch: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
mockFetch,
|
||||
);
|
||||
|
||||
expect(summary.providers).toHaveLength(3);
|
||||
const claude = summary.providers.find((p) => p.provider === "anthropic");
|
||||
@@ -259,16 +267,7 @@ describe("provider usage loading", () => {
|
||||
});
|
||||
expect(listProfilesForProvider(store, "anthropic")).toContain("anthropic:default");
|
||||
|
||||
const makeResponse = (status: number, body: unknown): Response => {
|
||||
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
||||
const headers =
|
||||
typeof body === "string" ? undefined : { "Content-Type": "application/json" };
|
||||
return new Response(payload, { status, headers });
|
||||
};
|
||||
|
||||
const mockFetch = vi.fn(async (input: string | Request | URL, init?: RequestInit) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
const mockFetch = createProviderUsageFetch(async (url, init) => {
|
||||
if (url.includes("api.anthropic.com/api/oauth/usage")) {
|
||||
const headers = (init?.headers ?? {}) as Record<string, string>;
|
||||
expect(headers.Authorization).toBe("Bearer token-1");
|
||||
@@ -283,15 +282,13 @@ describe("provider usage loading", () => {
|
||||
});
|
||||
|
||||
const summary = await loadProviderUsageSummary({
|
||||
now: Date.UTC(2026, 0, 7, 0, 0, 0),
|
||||
now: usageNow,
|
||||
providers: ["anthropic"],
|
||||
agentDir,
|
||||
fetch: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(summary.providers).toHaveLength(1);
|
||||
const claude = summary.providers[0];
|
||||
expect(claude?.provider).toBe("anthropic");
|
||||
const claude = expectSingleAnthropicProvider(summary);
|
||||
expect(claude?.windows[0]?.label).toBe("5h");
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
},
|
||||
@@ -308,16 +305,7 @@ describe("provider usage loading", () => {
|
||||
const cookieSnapshot = process.env.CLAUDE_AI_SESSION_KEY;
|
||||
process.env.CLAUDE_AI_SESSION_KEY = "sk-ant-web-1";
|
||||
try {
|
||||
const makeResponse = (status: number, body: unknown): Response => {
|
||||
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
||||
const headers =
|
||||
typeof body === "string" ? undefined : { "Content-Type": "application/json" };
|
||||
return new Response(payload, { status, headers });
|
||||
};
|
||||
|
||||
const mockFetch = vi.fn(async (input: string | Request | URL) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
const mockFetch = createProviderUsageFetch(async (url) => {
|
||||
if (url.includes("api.anthropic.com/api/oauth/usage")) {
|
||||
return makeResponse(403, {
|
||||
type: "error",
|
||||
@@ -340,15 +328,12 @@ describe("provider usage loading", () => {
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
|
||||
const summary = await loadProviderUsageSummary({
|
||||
now: Date.UTC(2026, 0, 7, 0, 0, 0),
|
||||
auth: [{ provider: "anthropic", token: "sk-ant-oauth-1" }],
|
||||
fetch: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
const summary = await loadUsageWithAuth(
|
||||
[{ provider: "anthropic", token: "sk-ant-oauth-1" }],
|
||||
mockFetch,
|
||||
);
|
||||
|
||||
expect(summary.providers).toHaveLength(1);
|
||||
const claude = summary.providers[0];
|
||||
expect(claude?.provider).toBe("anthropic");
|
||||
const claude = expectSingleAnthropicProvider(summary);
|
||||
expect(claude?.windows.some((w) => w.label === "5h")).toBe(true);
|
||||
expect(claude?.windows.some((w) => w.label === "Week")).toBe(true);
|
||||
} finally {
|
||||
@@ -359,4 +344,131 @@ describe("provider usage loading", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("loads snapshots for copilot antigravity gemini codex and xiaomi", async () => {
|
||||
const mockFetch = createProviderUsageFetch(async (url) => {
|
||||
if (url.includes("api.github.com/copilot_internal/user")) {
|
||||
return makeResponse(200, {
|
||||
quota_snapshots: { chat: { percent_remaining: 80 } },
|
||||
copilot_plan: "Copilot Pro",
|
||||
});
|
||||
}
|
||||
if (url.includes("cloudcode-pa.googleapis.com/v1internal:loadCodeAssist")) {
|
||||
return makeResponse(200, {
|
||||
availablePromptCredits: 80,
|
||||
planInfo: { monthlyPromptCredits: 100 },
|
||||
currentTier: { name: "Antigravity Pro" },
|
||||
cloudaicompanionProject: "projects/demo",
|
||||
});
|
||||
}
|
||||
if (url.includes("cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels")) {
|
||||
return makeResponse(200, {
|
||||
models: {
|
||||
"gemini-2.5-pro": {
|
||||
quotaInfo: { remainingFraction: 0.4, resetTime: "2026-01-08T01:00:00Z" },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.includes("cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota")) {
|
||||
return makeResponse(200, {
|
||||
buckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.6 }],
|
||||
});
|
||||
}
|
||||
if (url.includes("chatgpt.com/backend-api/wham/usage")) {
|
||||
return makeResponse(200, {
|
||||
rate_limit: { primary_window: { used_percent: 12, limit_window_seconds: 10800 } },
|
||||
plan_type: "Plus",
|
||||
});
|
||||
}
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
|
||||
const summary = await loadUsageWithAuth(
|
||||
[
|
||||
{ provider: "github-copilot", token: "copilot-token" },
|
||||
{ provider: "google-antigravity", token: "antigravity-token" },
|
||||
{ provider: "google-gemini-cli", token: "gemini-token" },
|
||||
{ provider: "openai-codex", token: "codex-token", accountId: "acc-1" },
|
||||
{ provider: "xiaomi", token: "xiaomi-token" },
|
||||
],
|
||||
mockFetch,
|
||||
);
|
||||
|
||||
expect(summary.providers.map((provider) => provider.provider)).toEqual([
|
||||
"github-copilot",
|
||||
"google-antigravity",
|
||||
"google-gemini-cli",
|
||||
"openai-codex",
|
||||
"xiaomi",
|
||||
]);
|
||||
expect(
|
||||
summary.providers.find((provider) => provider.provider === "github-copilot")?.windows,
|
||||
).toEqual([{ label: "Chat", usedPercent: 20 }]);
|
||||
expect(
|
||||
summary.providers.find((provider) => provider.provider === "google-antigravity")?.windows
|
||||
.length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
summary.providers.find((provider) => provider.provider === "google-gemini-cli")?.windows[0]
|
||||
?.label,
|
||||
).toBe("Pro");
|
||||
expect(
|
||||
summary.providers.find((provider) => provider.provider === "openai-codex")?.windows[0]?.label,
|
||||
).toBe("3h");
|
||||
expect(summary.providers.find((provider) => provider.provider === "xiaomi")?.windows).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it("returns empty provider list when auth resolves to none", async () => {
|
||||
const mockFetch = createProviderUsageFetch(async () => makeResponse(404, "not found"));
|
||||
const summary = await loadUsageWithAuth([], mockFetch);
|
||||
expect(summary).toEqual({ updatedAt: usageNow, providers: [] });
|
||||
});
|
||||
|
||||
it("returns unsupported provider snapshots for unknown provider ids", async () => {
|
||||
const mockFetch = createProviderUsageFetch(async () => makeResponse(404, "not found"));
|
||||
const summary = await loadUsageWithAuth(
|
||||
[{ provider: "unsupported-provider", token: "token-u" }] as unknown as ProviderAuth[],
|
||||
mockFetch,
|
||||
);
|
||||
expect(summary.providers).toHaveLength(1);
|
||||
expect(summary.providers[0]?.error).toBe("Unsupported provider");
|
||||
});
|
||||
|
||||
it("filters errors that are marked as ignored", async () => {
|
||||
const mockFetch = createProviderUsageFetch(async (url) => {
|
||||
if (url.includes("api.anthropic.com/api/oauth/usage")) {
|
||||
return makeResponse(500, "boom");
|
||||
}
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
ignoredErrors.add("HTTP 500");
|
||||
try {
|
||||
const summary = await loadUsageWithAuth(
|
||||
[{ provider: "anthropic", token: "token-a" }],
|
||||
mockFetch,
|
||||
);
|
||||
expect(summary.providers).toEqual([]);
|
||||
} finally {
|
||||
ignoredErrors.delete("HTTP 500");
|
||||
}
|
||||
});
|
||||
|
||||
it("throws when fetch is unavailable", async () => {
|
||||
const previousFetch = globalThis.fetch;
|
||||
vi.stubGlobal("fetch", undefined);
|
||||
try {
|
||||
await expect(
|
||||
loadProviderUsageSummary({
|
||||
now: usageNow,
|
||||
auth: [{ provider: "xiaomi", token: "token-x" }],
|
||||
fetch: undefined,
|
||||
}),
|
||||
).rejects.toThrow("fetch is not available");
|
||||
} finally {
|
||||
vi.stubGlobal("fetch", previousFetch);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user