mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
perf(test): trim infra provider and approval suites
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import {
|
||||
parseRawSessionConversationRef,
|
||||
parseThreadSessionSuffix,
|
||||
} from "../sessions/session-key-utils.js";
|
||||
import { withTempDirSync } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
doesApprovalRequestMatchChannelAccount,
|
||||
@@ -17,6 +21,36 @@ import {
|
||||
import type { ExecApprovalRequest } from "./exec-approvals.js";
|
||||
import type { PluginApprovalRequest } from "./plugin-approvals.js";
|
||||
|
||||
vi.mock("../channels/plugins/session-conversation.js", () => ({
|
||||
resolveSessionConversationRef(sessionKey: string | undefined | null) {
|
||||
const raw = parseRawSessionConversationRef(sessionKey);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseThreadSessionSuffix(raw.rawId);
|
||||
const id = (parsed.baseSessionKey ?? raw.rawId).trim();
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
channel: raw.channel,
|
||||
kind: raw.kind,
|
||||
rawId: raw.rawId,
|
||||
id,
|
||||
threadId: parsed.threadId,
|
||||
baseSessionKey: `${raw.prefix}:${id}`,
|
||||
baseConversationId: id,
|
||||
parentConversationCandidates: parsed.threadId ? [id] : [],
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./outbound/targets.js", async () => {
|
||||
return await vi.importActual<typeof import("./outbound/targets-session.js")>(
|
||||
"./outbound/targets-session.js",
|
||||
);
|
||||
});
|
||||
|
||||
const baseRequest: ExecApprovalRequest = {
|
||||
id: "req-1",
|
||||
request: {
|
||||
|
||||
@@ -10,41 +10,63 @@ import {
|
||||
|
||||
type ProviderAuth = ProviderUsageAuth<typeof loadProviderUsageSummary>;
|
||||
const googleGeminiCliProvider = "google-gemini-cli" as unknown as ProviderAuth["provider"];
|
||||
const resolveProviderUsageSnapshotWithPluginMock = vi.fn(async () => null);
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
|
||||
"../plugins/provider-runtime.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
resolveProviderUsageSnapshotWithPlugin: (...args: unknown[]) =>
|
||||
resolveProviderUsageSnapshotWithPluginMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
describe("provider-usage.load", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockReset();
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("loads snapshots for copilot 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",
|
||||
});
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockImplementation(async ({ provider }) => {
|
||||
switch (provider) {
|
||||
case "github-copilot":
|
||||
return {
|
||||
provider,
|
||||
displayName: "GitHub Copilot",
|
||||
windows: [{ label: "Chat", usedPercent: 20 }],
|
||||
};
|
||||
case googleGeminiCliProvider:
|
||||
return {
|
||||
provider,
|
||||
displayName: "Gemini CLI",
|
||||
windows: [{ label: "Pro", usedPercent: 40 }],
|
||||
};
|
||||
case "openai-codex":
|
||||
return {
|
||||
provider,
|
||||
displayName: "Codex",
|
||||
windows: [{ label: "3h", usedPercent: 12 }],
|
||||
};
|
||||
case "xiaomi":
|
||||
return {
|
||||
provider,
|
||||
displayName: "Xiaomi",
|
||||
windows: [],
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
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 mockFetch = createProviderUsageFetch(async () => {
|
||||
throw new Error("legacy fetch should not run");
|
||||
});
|
||||
|
||||
const summary = await loadUsageWithAuth(
|
||||
@@ -77,6 +99,7 @@ describe("provider-usage.load", () => {
|
||||
expect(summary.providers.find((provider) => provider.provider === "xiaomi")?.windows).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns empty provider list when auth resolves to none", async () => {
|
||||
@@ -97,11 +120,14 @@ describe("provider-usage.load", () => {
|
||||
});
|
||||
|
||||
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");
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockResolvedValueOnce({
|
||||
provider: "anthropic",
|
||||
displayName: "Claude",
|
||||
windows: [],
|
||||
error: "HTTP 500",
|
||||
});
|
||||
const mockFetch = createProviderUsageFetch(async () => {
|
||||
throw new Error("legacy fetch should not run");
|
||||
});
|
||||
ignoredErrors.add("HTTP 500");
|
||||
try {
|
||||
|
||||
@@ -1,62 +1,36 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { createProviderUsageFetch, makeResponse } from "../test-utils/provider-usage-fetch.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
formatUsageReportLines,
|
||||
formatUsageSummaryLine,
|
||||
loadProviderUsageSummary,
|
||||
type UsageSummary,
|
||||
} from "./provider-usage.js";
|
||||
import { loadUsageWithAuth, usageNow } from "./provider-usage.test-support.js";
|
||||
import { createProviderUsageFetch } from "../test-utils/provider-usage-fetch.js";
|
||||
import { loadUsageWithAuth } from "./provider-usage.test-support.js";
|
||||
|
||||
const minimaxRemainsEndpoint = "api.minimaxi.com/v1/api/openplatform/coding_plan/remains";
|
||||
const resolveProviderUsageSnapshotWithPluginMock = vi.fn(async () => null);
|
||||
|
||||
function expectSingleAnthropicProvider(summary: UsageSummary) {
|
||||
expect(summary.providers).toHaveLength(1);
|
||||
const claude = summary.providers[0];
|
||||
expect(claude?.provider).toBe("anthropic");
|
||||
return claude;
|
||||
}
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: () => ({}),
|
||||
}));
|
||||
|
||||
function createMinimaxOnlyFetch(payload: unknown) {
|
||||
return createProviderUsageFetch(async (url) => {
|
||||
if (url.includes(minimaxRemainsEndpoint)) {
|
||||
return makeResponse(200, payload);
|
||||
}
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
}
|
||||
|
||||
async function expectMinimaxUsage(
|
||||
payload: unknown,
|
||||
expected: {
|
||||
usedPercent: number;
|
||||
plan?: string;
|
||||
label?: string;
|
||||
},
|
||||
) {
|
||||
const mockFetch = createMinimaxOnlyFetch(payload);
|
||||
|
||||
const summary = await loadUsageWithAuth(
|
||||
loadProviderUsageSummary,
|
||||
[{ provider: "minimax", token: "token-1b" }],
|
||||
mockFetch,
|
||||
vi.mock("../plugins/provider-runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
|
||||
"../plugins/provider-runtime.js",
|
||||
);
|
||||
|
||||
const minimax = summary.providers.find((p) => p.provider === "minimax");
|
||||
expect(minimax?.windows[0]?.usedPercent).toBe(expected.usedPercent);
|
||||
expect(minimax?.windows[0]?.label).toBe(expected.label ?? "5h");
|
||||
if (expected.plan !== undefined) {
|
||||
expect(minimax?.plan).toBe(expected.plan);
|
||||
}
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
}
|
||||
return {
|
||||
...actual,
|
||||
resolveProviderUsageSnapshotWithPlugin: (...args: unknown[]) =>
|
||||
resolveProviderUsageSnapshotWithPluginMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
describe("provider usage formatting", () => {
|
||||
beforeEach(() => {
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockReset();
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("returns null when no usage is available", () => {
|
||||
const summary: UsageSummary = { updatedAt: 0, providers: [] };
|
||||
expect(formatUsageSummaryLine(summary)).toBeNull();
|
||||
@@ -117,42 +91,34 @@ describe("provider usage formatting", () => {
|
||||
|
||||
describe("provider usage loading", () => {
|
||||
it("loads usage snapshots with injected auth", async () => {
|
||||
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" },
|
||||
});
|
||||
resolveProviderUsageSnapshotWithPluginMock.mockImplementation(async ({ provider }) => {
|
||||
switch (provider) {
|
||||
case "anthropic":
|
||||
return {
|
||||
provider,
|
||||
displayName: "Claude",
|
||||
windows: [{ label: "5h", usedPercent: 20 }],
|
||||
};
|
||||
case "minimax":
|
||||
return {
|
||||
provider,
|
||||
displayName: "MiniMax",
|
||||
windows: [{ label: "5h", usedPercent: 75 }],
|
||||
plan: "Coding Plan",
|
||||
};
|
||||
case "zai":
|
||||
return {
|
||||
provider,
|
||||
displayName: "Z.ai",
|
||||
windows: [{ label: "3h", usedPercent: 25 }],
|
||||
plan: "Pro",
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (url.includes("api.z.ai")) {
|
||||
return makeResponse(200, {
|
||||
success: true,
|
||||
code: 200,
|
||||
data: {
|
||||
planName: "Pro",
|
||||
limits: [
|
||||
{
|
||||
type: "TOKENS_LIMIT",
|
||||
percentage: 25,
|
||||
unit: 3,
|
||||
number: 6,
|
||||
nextResetTime: "2026-01-07T06:00:00Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.includes(minimaxRemainsEndpoint)) {
|
||||
return makeResponse(200, {
|
||||
base_resp: { status_code: 0, status_msg: "ok" },
|
||||
data: {
|
||||
total: 200,
|
||||
remain: 50,
|
||||
reset_at: "2026-01-07T05:00:00Z",
|
||||
plan_name: "Coding Plan",
|
||||
},
|
||||
});
|
||||
}
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
const mockFetch = createProviderUsageFetch(async () => {
|
||||
throw new Error("legacy fetch should not run");
|
||||
});
|
||||
|
||||
const summary = await loadUsageWithAuth(
|
||||
@@ -172,179 +138,6 @@ describe("provider usage loading", () => {
|
||||
expect(claude?.windows[0]?.label).toBe("5h");
|
||||
expect(minimax?.windows[0]?.usedPercent).toBe(75);
|
||||
expect(zai?.plan).toBe("Pro");
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "handles nested MiniMax usage payloads",
|
||||
payload: {
|
||||
base_resp: { status_code: 0, status_msg: "ok" },
|
||||
data: {
|
||||
plan_name: "Coding Plan",
|
||||
usage: {
|
||||
prompt_limit: 200,
|
||||
prompt_remain: 50,
|
||||
next_reset_time: "2026-01-07T05:00:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: { usedPercent: 75, plan: "Coding Plan" },
|
||||
},
|
||||
{
|
||||
name: "prefers MiniMax count-based usage when percent looks inverted",
|
||||
payload: {
|
||||
base_resp: { status_code: 0, status_msg: "ok" },
|
||||
data: {
|
||||
prompt_limit: 200,
|
||||
prompt_remain: 150,
|
||||
usage_percent: 75,
|
||||
next_reset_time: "2026-01-07T05:00:00Z",
|
||||
},
|
||||
},
|
||||
expected: { usedPercent: 25 },
|
||||
},
|
||||
{
|
||||
name: "handles MiniMax model_remains usage payloads",
|
||||
payload: {
|
||||
base_resp: { status_code: 0, status_msg: "ok" },
|
||||
model_remains: [
|
||||
{
|
||||
start_time: 1736217600,
|
||||
end_time: 1736235600,
|
||||
remains_time: 600,
|
||||
current_interval_total_count: 120,
|
||||
// API field is remaining quota, not consumed (MiniMax-M2#99).
|
||||
current_interval_usage_count: 30,
|
||||
model_name: "MiniMax-M2.5",
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: { usedPercent: 75 },
|
||||
},
|
||||
{
|
||||
name: "keeps payload-level MiniMax plan metadata when the usage candidate is nested",
|
||||
payload: {
|
||||
base_resp: { status_code: 0, status_msg: "ok" },
|
||||
data: {
|
||||
plan_name: "Payload Plan",
|
||||
nested: {
|
||||
usage_ratio: "0.4",
|
||||
window_hours: 2,
|
||||
next_reset_time: "2026-01-07T05:00:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: { usedPercent: 40, plan: "Payload Plan", label: "2h" },
|
||||
},
|
||||
])("$name", async ({ payload, expected }) => {
|
||||
await expectMinimaxUsage(payload, expected);
|
||||
});
|
||||
|
||||
it("discovers Claude usage from token auth profiles", async () => {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
const agentDir = path.join(
|
||||
process.env.OPENCLAW_STATE_DIR ?? path.join(tempHome, ".openclaw"),
|
||||
"agents",
|
||||
"main",
|
||||
"agent",
|
||||
);
|
||||
fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
order: { anthropic: ["anthropic:default"] },
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
token: "token-1",
|
||||
expires: Date.UTC(2100, 0, 1, 0, 0, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
expect(listProfilesForProvider(store, "anthropic")).toContain("anthropic:default");
|
||||
|
||||
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");
|
||||
return makeResponse(200, {
|
||||
five_hour: {
|
||||
utilization: 20,
|
||||
resets_at: "2026-01-07T01:00:00Z",
|
||||
},
|
||||
});
|
||||
}
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
|
||||
const summary = await loadProviderUsageSummary({
|
||||
now: usageNow,
|
||||
providers: ["anthropic"],
|
||||
agentDir,
|
||||
fetch: mockFetch as unknown as typeof fetch,
|
||||
config: {},
|
||||
});
|
||||
|
||||
const claude = expectSingleAnthropicProvider(summary);
|
||||
expect(claude?.windows[0]?.label).toBe("5h");
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
},
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_STATE_DIR: (home) => path.join(home, ".openclaw"),
|
||||
},
|
||||
prefix: "openclaw-provider-usage-",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to claude.ai web usage when OAuth scope is missing", async () => {
|
||||
await withEnvAsync({ CLAUDE_AI_SESSION_KEY: "sk-ant-web-1" }, async () => {
|
||||
const mockFetch = createProviderUsageFetch(async (url) => {
|
||||
if (url.includes("api.anthropic.com/api/oauth/usage")) {
|
||||
return makeResponse(403, {
|
||||
type: "error",
|
||||
error: {
|
||||
type: "permission_error",
|
||||
message: "OAuth token does not meet scope requirement user:profile",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.includes("claude.ai/api/organizations/org-1/usage")) {
|
||||
return makeResponse(200, {
|
||||
five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" },
|
||||
seven_day: { utilization: 40, resets_at: "2026-01-08T01:00:00Z" },
|
||||
seven_day_opus: { utilization: 5 },
|
||||
});
|
||||
}
|
||||
if (url.includes("claude.ai/api/organizations")) {
|
||||
return makeResponse(200, [{ uuid: "org-1", name: "Test" }]);
|
||||
}
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
|
||||
const summary = await loadUsageWithAuth(
|
||||
loadProviderUsageSummary,
|
||||
[{ provider: "anthropic", token: "sk-ant-oauth-1" }],
|
||||
mockFetch,
|
||||
);
|
||||
|
||||
const claude = expectSingleAnthropicProvider(summary);
|
||||
expect(claude?.windows.some((w) => w.label === "5h")).toBe(true);
|
||||
expect(claude?.windows.some((w) => w.label === "Week")).toBe(true);
|
||||
});
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user