mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-19 06:51:57 +00:00
* feat(usage): route claude-cli to Anthropic plan usage with plan label and billing * feat(webchat): redesign context popover with plan-usage bars and API-cost gating * fix(usage): match plan usage across CLI provider aliases and source plan label from synced auth profile * docs(plugins): document plan metadata on usage auth token * chore(usage): document provider-level billing attribution limits, fix overview test types * fix(webchat): avoid map-spread in quota group collection * test(webchat): deflake subscription popover reset fixture
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
// Covers shared provider usage helpers.
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js";
|
|
import { clampPercent, resolveUsageProviderId, withTimeout } from "./provider-usage.shared.js";
|
|
|
|
describe("provider-usage.shared", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it.each([
|
|
{ value: "clawrouter", expected: "clawrouter" },
|
|
{ value: "deepseek", expected: "deepseek" },
|
|
{ value: "zai", expected: "zai" },
|
|
{ value: "z-ai", expected: "z-ai" },
|
|
{ value: " GOOGLE-GEMINI-CLI ", expected: "google-gemini-cli" },
|
|
{ value: "minimax-portal", expected: "minimax" },
|
|
{ value: "minimax-cn", expected: "minimax" },
|
|
{ value: "minimax-portal-cn", expected: "minimax" },
|
|
{ value: "claude-cli", expected: "anthropic" },
|
|
{ value: " CLAUDE-CLI ", expected: "anthropic" },
|
|
{ value: " XIAOMI-TOKEN-PLAN ", expected: "xiaomi-token-plan" },
|
|
{ value: "unknown-provider", expected: "unknown-provider" },
|
|
{ value: undefined, expected: undefined },
|
|
{ value: null, expected: undefined },
|
|
])("normalizes provider ids for %j", ({ value, expected }) => {
|
|
expect(resolveUsageProviderId(value)).toBe(expected);
|
|
});
|
|
|
|
it("maps canonical OpenAI subscription profiles to Codex usage windows", () => {
|
|
expect(resolveUsageProviderId("openai", { credentialType: "oauth" })).toBe("openai");
|
|
expect(resolveUsageProviderId("openai", { credentialType: "token" })).toBe("openai");
|
|
expect(resolveUsageProviderId("openai", { credentialType: "api_key" })).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
{ value: -5, expected: 0 },
|
|
{ value: 42, expected: 42 },
|
|
{ value: 120, expected: 100 },
|
|
{ value: Number.NaN, expected: 0 },
|
|
{ value: Number.POSITIVE_INFINITY, expected: 0 },
|
|
])("clamps usage percents for %j", ({ value, expected }) => {
|
|
expect(clampPercent(value)).toBe(expected);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "returns work result when it resolves before timeout",
|
|
promise: () => Promise.resolve("ok"),
|
|
expected: "ok",
|
|
},
|
|
{
|
|
name: "propagates work errors before timeout",
|
|
promise: () => Promise.reject(new Error("boom")),
|
|
error: "boom",
|
|
},
|
|
])("$name", async ({ promise, expected, error }) => {
|
|
if (error) {
|
|
await expect(withTimeout(promise(), 100, "fallback")).rejects.toThrow(error);
|
|
return;
|
|
}
|
|
await expect(withTimeout(promise(), 100, "fallback")).resolves.toBe(expected);
|
|
});
|
|
|
|
it("returns fallback when timeout wins", async () => {
|
|
vi.useFakeTimers();
|
|
const late = new Promise<string>((resolve) => {
|
|
setTimeout(() => resolve("late"), 50);
|
|
});
|
|
const result = withTimeout(late, 1, "fallback");
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
await expect(result).resolves.toBe("fallback");
|
|
});
|
|
|
|
it("clamps oversized timeout delays before scheduling", async () => {
|
|
vi.useFakeTimers();
|
|
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
|
|
|
const result = withTimeout(new Promise<string>(() => {}), Number.MAX_SAFE_INTEGER, "fallback");
|
|
|
|
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
|
|
|
|
await vi.advanceTimersByTimeAsync(MAX_TIMER_TIMEOUT_MS);
|
|
await expect(result).resolves.toBe("fallback");
|
|
});
|
|
|
|
it("clears the timeout after successful work", async () => {
|
|
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
|
|
|
|
await expect(withTimeout(Promise.resolve("ok"), 100, "fallback")).resolves.toBe("ok");
|
|
|
|
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|