test: collapse provider plugin suites

This commit is contained in:
Peter Steinberger
2026-03-25 04:22:00 +00:00
parent 6c04ce3092
commit 410c2dba65
23 changed files with 1012 additions and 1041 deletions

View File

@@ -8,6 +8,7 @@ import type {
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js";
import type { OpenClawPluginApi } from "./api.js";
import type { PendingPairingRequest } from "./notify.ts";
const pluginApiMocks = vi.hoisted(() => ({
clearDeviceBootstrapTokens: vi.fn(async () => ({ removed: 2 })),
@@ -385,6 +386,49 @@ describe("device-pair /pair qr", () => {
});
});
describe("device-pair notify pending formatting", () => {
it("includes role and scopes for pending requests", async () => {
const { formatPendingRequests } =
await vi.importActual<typeof import("./notify.ts")>("./notify.ts");
const pending: PendingPairingRequest[] = [
{
requestId: "req-1",
deviceId: "device-1",
displayName: "dev one",
platform: "ios",
role: "operator",
scopes: ["operator.admin", "operator.read"],
remoteIp: "198.51.100.2",
},
];
const text = formatPendingRequests(pending);
expect(text).toContain("Pending device pairing requests:");
expect(text).toContain("name=dev one");
expect(text).toContain("platform=ios");
expect(text).toContain("role=operator");
expect(text).toContain("scopes=operator.admin, operator.read");
expect(text).toContain("ip=198.51.100.2");
});
it("falls back to roles list and no scopes when role/scopes are absent", async () => {
const { formatPendingRequests } =
await vi.importActual<typeof import("./notify.ts")>("./notify.ts");
const pending: PendingPairingRequest[] = [
{
requestId: "req-2",
deviceId: "device-2",
roles: ["node", "operator"],
scopes: [],
},
];
const text = formatPendingRequests(pending);
expect(text).toContain("role=node, operator");
expect(text).toContain("scopes=none");
});
});
describe("device-pair /pair approve", () => {
it("rejects internal gateway callers without operator.pairing", async () => {
vi.mocked(listDevicePairing).mockResolvedValueOnce({

View File

@@ -1,41 +0,0 @@
import { describe, expect, it } from "vitest";
import { formatPendingRequests, type PendingPairingRequest } from "./notify.ts";
describe("device-pair notify pending formatting", () => {
it("includes role and scopes for pending requests", () => {
const pending: PendingPairingRequest[] = [
{
requestId: "req-1",
deviceId: "device-1",
displayName: "dev one",
platform: "ios",
role: "operator",
scopes: ["operator.admin", "operator.read"],
remoteIp: "198.51.100.2",
},
];
const text = formatPendingRequests(pending);
expect(text).toContain("Pending device pairing requests:");
expect(text).toContain("name=dev one");
expect(text).toContain("platform=ios");
expect(text).toContain("role=operator");
expect(text).toContain("scopes=operator.admin, operator.read");
expect(text).toContain("ip=198.51.100.2");
});
it("falls back to roles list and no scopes when role/scopes are absent", () => {
const pending: PendingPairingRequest[] = [
{
requestId: "req-2",
deviceId: "device-2",
roles: ["node", "operator"],
scopes: [],
},
];
const text = formatPendingRequests(pending);
expect(text).toContain("role=node, operator");
expect(text).toContain("scopes=none");
});
});

View File

@@ -1,75 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi } from "./runtime-api.js";
const registerFeishuDocToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuChatToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuWikiToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuDriveToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuPermToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuBitableToolsMock = vi.hoisted(() => vi.fn());
const feishuPluginMock = vi.hoisted(() => ({ id: "feishu-test-plugin" }));
const setFeishuRuntimeMock = vi.hoisted(() => vi.fn());
const registerFeishuSubagentHooksMock = vi.hoisted(() => vi.fn());
vi.mock("./src/channel.js", () => ({
feishuPlugin: feishuPluginMock,
}));
vi.mock("./src/docx.js", () => ({
registerFeishuDocTools: registerFeishuDocToolsMock,
}));
vi.mock("./src/chat.js", () => ({
registerFeishuChatTools: registerFeishuChatToolsMock,
}));
vi.mock("./src/wiki.js", () => ({
registerFeishuWikiTools: registerFeishuWikiToolsMock,
}));
vi.mock("./src/drive.js", () => ({
registerFeishuDriveTools: registerFeishuDriveToolsMock,
}));
vi.mock("./src/perm.js", () => ({
registerFeishuPermTools: registerFeishuPermToolsMock,
}));
vi.mock("./src/bitable.js", () => ({
registerFeishuBitableTools: registerFeishuBitableToolsMock,
}));
vi.mock("./src/runtime.js", () => ({
setFeishuRuntime: setFeishuRuntimeMock,
}));
vi.mock("./src/subagent-hooks.js", () => ({
registerFeishuSubagentHooks: registerFeishuSubagentHooksMock,
}));
describe("feishu plugin register", () => {
it("registers the Feishu channel, tools, and subagent hooks", async () => {
const { default: plugin } = await import("./index.js");
const registerChannel = vi.fn();
const api = {
runtime: { log: vi.fn() },
registerChannel,
on: vi.fn(),
config: {},
registrationMode: "full",
} as unknown as OpenClawPluginApi;
plugin.register(api);
expect(setFeishuRuntimeMock).toHaveBeenCalledWith(api.runtime);
expect(registerChannel).toHaveBeenCalledTimes(1);
expect(registerChannel).toHaveBeenCalledWith({ plugin: feishuPluginMock });
expect(registerFeishuSubagentHooksMock).toHaveBeenCalledWith(api);
expect(registerFeishuDocToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuChatToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuWikiToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuDriveToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuPermToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuBitableToolsMock).toHaveBeenCalledWith(api);
});
});

View File

@@ -1,4 +1,5 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi } from "../runtime-api.js";
import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js";
type CreateFeishuClient = typeof import("./client.js").createFeishuClient;
@@ -33,6 +34,15 @@ const mockBaseHttpInstance = vi.hoisted(() => ({
}));
const proxyEnvKeys = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"] as const;
type ProxyEnvKey = (typeof proxyEnvKeys)[number];
const registerFeishuDocToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuChatToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuWikiToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuDriveToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuPermToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuBitableToolsMock = vi.hoisted(() => vi.fn());
const feishuPluginMock = vi.hoisted(() => ({ id: "feishu-test-plugin" }));
const setFeishuRuntimeMock = vi.hoisted(() => vi.fn());
const registerFeishuSubagentHooksMock = vi.hoisted(() => vi.fn());
let createFeishuClient: CreateFeishuClient;
let createFeishuWSClient: CreateFeishuWSClient;
@@ -45,6 +55,42 @@ let FEISHU_HTTP_TIMEOUT_ENV_VAR: string;
let priorProxyEnv: Partial<Record<ProxyEnvKey, string | undefined>> = {};
let priorFeishuTimeoutEnv: string | undefined;
vi.mock("./channel.js", () => ({
feishuPlugin: feishuPluginMock,
}));
vi.mock("./docx.js", () => ({
registerFeishuDocTools: registerFeishuDocToolsMock,
}));
vi.mock("./chat.js", () => ({
registerFeishuChatTools: registerFeishuChatToolsMock,
}));
vi.mock("./wiki.js", () => ({
registerFeishuWikiTools: registerFeishuWikiToolsMock,
}));
vi.mock("./drive.js", () => ({
registerFeishuDriveTools: registerFeishuDriveToolsMock,
}));
vi.mock("./perm.js", () => ({
registerFeishuPermTools: registerFeishuPermToolsMock,
}));
vi.mock("./bitable.js", () => ({
registerFeishuBitableTools: registerFeishuBitableToolsMock,
}));
vi.mock("./runtime.js", () => ({
setFeishuRuntime: setFeishuRuntimeMock,
}));
vi.mock("./subagent-hooks.js", () => ({
registerFeishuSubagentHooks: registerFeishuSubagentHooksMock,
}));
const baseAccount: ResolvedFeishuAccount = {
accountId: "main",
selectionSource: "explicit",
@@ -290,6 +336,33 @@ describe("createFeishuClient HTTP timeout", () => {
});
});
describe("feishu plugin register", () => {
it("registers the Feishu channel, tools, and subagent hooks", async () => {
const { default: plugin } = await import("../index.js");
const registerChannel = vi.fn();
const api = {
runtime: { log: vi.fn() },
registerChannel,
on: vi.fn(),
config: {},
registrationMode: "full",
} as unknown as OpenClawPluginApi;
plugin.register(api);
expect(setFeishuRuntimeMock).toHaveBeenCalledWith(api.runtime);
expect(registerChannel).toHaveBeenCalledTimes(1);
expect(registerChannel).toHaveBeenCalledWith({ plugin: feishuPluginMock });
expect(registerFeishuSubagentHooksMock).toHaveBeenCalledWith(api);
expect(registerFeishuDocToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuChatToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuWikiToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuDriveToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuPermToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuBitableToolsMock).toHaveBeenCalledWith(api);
});
});
describe("createFeishuWSClient proxy handling", () => {
it("does not set a ws proxy agent when proxy env is absent", () => {
createFeishuWSClient(baseAccount);

View File

@@ -1,84 +0,0 @@
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
import { __testing as firecrawlClientTesting } from "./src/firecrawl-client.js";
describe("firecrawl plugin", () => {
it("parses scrape payloads into wrapped external-content results", () => {
const result = firecrawlClientTesting.parseFirecrawlScrapePayload({
payload: {
success: true,
data: {
markdown: "# Hello\n\nWorld",
metadata: {
title: "Example page",
sourceURL: "https://example.com/final",
statusCode: 200,
},
},
},
url: "https://example.com/start",
extractMode: "text",
maxChars: 1000,
});
expect(result.finalUrl).toBe("https://example.com/final");
expect(result.status).toBe(200);
expect(result.extractor).toBe("firecrawl");
expect(String(result.text)).toContain("Hello");
expect(String(result.text)).toContain("World");
expect(result.truncated).toBe(false);
});
it("extracts search items from flexible Firecrawl payload shapes", () => {
const items = firecrawlClientTesting.resolveSearchItems({
success: true,
data: [
{
title: "Docs",
url: "https://docs.example.com/path",
description: "Reference docs",
markdown: "Body",
},
],
});
expect(items).toEqual([
{
title: "Docs",
url: "https://docs.example.com/path",
description: "Reference docs",
content: "Body",
published: undefined,
siteName: "docs.example.com",
},
]);
});
it("extracts search items from Firecrawl v2 data.web payloads", () => {
const items = firecrawlClientTesting.resolveSearchItems({
success: true,
data: {
web: [
{
title: "API Platform - OpenAI",
url: "https://openai.com/api/",
description: "Build on the OpenAI API platform.",
markdown: "# API Platform",
position: 1,
},
],
},
});
expect(items).toEqual([
{
title: "API Platform - OpenAI",
url: "https://openai.com/api/",
description: "Build on the OpenAI API platform.",
content: "# API Platform",
published: undefined,
siteName: "openai.com",
},
]);
});
});

View File

@@ -65,6 +65,85 @@ describe("firecrawl tools", () => {
expect(applied.plugins?.entries?.firecrawl?.enabled).toBe(true);
});
it("parses scrape payloads into wrapped external-content results", () => {
const result = firecrawlClientTesting.parseFirecrawlScrapePayload({
payload: {
success: true,
data: {
markdown: "# Hello\n\nWorld",
metadata: {
title: "Example page",
sourceURL: "https://example.com/final",
statusCode: 200,
},
},
},
url: "https://example.com/start",
extractMode: "text",
maxChars: 1000,
});
expect(result.finalUrl).toBe("https://example.com/final");
expect(result.status).toBe(200);
expect(result.extractor).toBe("firecrawl");
expect(String(result.text)).toContain("Hello");
expect(String(result.text)).toContain("World");
expect(result.truncated).toBe(false);
});
it("extracts search items from flexible Firecrawl payload shapes", () => {
const items = firecrawlClientTesting.resolveSearchItems({
success: true,
data: [
{
title: "Docs",
url: "https://docs.example.com/path",
description: "Reference docs",
markdown: "Body",
},
],
});
expect(items).toEqual([
{
title: "Docs",
url: "https://docs.example.com/path",
description: "Reference docs",
content: "Body",
published: undefined,
siteName: "docs.example.com",
},
]);
});
it("extracts search items from Firecrawl v2 data.web payloads", () => {
const items = firecrawlClientTesting.resolveSearchItems({
success: true,
data: {
web: [
{
title: "API Platform - OpenAI",
url: "https://openai.com/api/",
description: "Build on the OpenAI API platform.",
markdown: "# API Platform",
position: 1,
},
],
},
});
expect(items).toEqual([
{
title: "API Platform - OpenAI",
url: "https://openai.com/api/",
description: "Build on the OpenAI API platform.",
content: "# API Platform",
published: undefined,
siteName: "openai.com",
},
]);
});
it("maps generic provider args into firecrawl search params", async () => {
const provider = createFirecrawlWebSearchProvider();
const tool = provider.createTool({

View File

@@ -1,39 +0,0 @@
import { describe, expect, it } from "vitest";
import { buildCopilotModelDefinition, getDefaultCopilotModelIds } from "./models-defaults.js";
describe("github-copilot model defaults", () => {
describe("getDefaultCopilotModelIds", () => {
it("includes claude-sonnet-4.6", () => {
expect(getDefaultCopilotModelIds()).toContain("claude-sonnet-4.6");
});
it("includes claude-sonnet-4.5", () => {
expect(getDefaultCopilotModelIds()).toContain("claude-sonnet-4.5");
});
it("returns a mutable copy", () => {
const a = getDefaultCopilotModelIds();
const b = getDefaultCopilotModelIds();
expect(a).not.toBe(b);
expect(a).toEqual(b);
});
});
describe("buildCopilotModelDefinition", () => {
it("builds a valid definition for claude-sonnet-4.6", () => {
const def = buildCopilotModelDefinition("claude-sonnet-4.6");
expect(def.id).toBe("claude-sonnet-4.6");
expect(def.api).toBe("openai-responses");
});
it("trims whitespace from model id", () => {
const def = buildCopilotModelDefinition(" gpt-4o ");
expect(def.id).toBe("gpt-4o");
});
it("throws on empty model id", () => {
expect(() => buildCopilotModelDefinition("")).toThrow("Model id required");
expect(() => buildCopilotModelDefinition(" ")).toThrow("Model id required");
});
});
});

View File

@@ -1,4 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
createProviderUsageFetch,
makeResponse,
} from "../../test/helpers/extensions/provider-usage-fetch.js";
import { buildCopilotModelDefinition, getDefaultCopilotModelIds } from "./models-defaults.js";
import { fetchCopilotUsage } from "./usage.js";
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
@@ -15,9 +21,24 @@ vi.mock("openclaw/plugin-sdk/provider-models", () => ({
normalizeModelCompat: (model: Record<string, unknown>) => model,
}));
const loadJsonFile = vi.fn();
const saveJsonFile = vi.fn();
vi.mock("openclaw/plugin-sdk/json-store", () => ({
loadJsonFile,
saveJsonFile,
}));
vi.mock("openclaw/plugin-sdk/state-paths", () => ({
resolveStateDir: () => "/tmp/openclaw-state",
}));
import type { ProviderResolveDynamicModelContext } from "openclaw/plugin-sdk/core";
import { resolveCopilotForwardCompatModel } from "./models.js";
let deriveCopilotApiBaseUrlFromToken: typeof import("./token.js").deriveCopilotApiBaseUrlFromToken;
let resolveCopilotApiToken: typeof import("./token.js").resolveCopilotApiToken;
function createMockCtx(
modelId: string,
registryModels: Record<string, Record<string, unknown>> = {},
@@ -40,6 +61,43 @@ function requireResolvedModel(ctx: ProviderResolveDynamicModelContext) {
return result;
}
describe("github-copilot model defaults", () => {
describe("getDefaultCopilotModelIds", () => {
it("includes claude-sonnet-4.6", () => {
expect(getDefaultCopilotModelIds()).toContain("claude-sonnet-4.6");
});
it("includes claude-sonnet-4.5", () => {
expect(getDefaultCopilotModelIds()).toContain("claude-sonnet-4.5");
});
it("returns a mutable copy", () => {
const a = getDefaultCopilotModelIds();
const b = getDefaultCopilotModelIds();
expect(a).not.toBe(b);
expect(a).toEqual(b);
});
});
describe("buildCopilotModelDefinition", () => {
it("builds a valid definition for claude-sonnet-4.6", () => {
const def = buildCopilotModelDefinition("claude-sonnet-4.6");
expect(def.id).toBe("claude-sonnet-4.6");
expect(def.api).toBe("openai-responses");
});
it("trims whitespace from model id", () => {
const def = buildCopilotModelDefinition(" gpt-4o ");
expect(def.id).toBe("gpt-4o");
});
it("throws on empty model id", () => {
expect(() => buildCopilotModelDefinition("")).toThrow("Model id required");
expect(() => buildCopilotModelDefinition(" ")).toThrow("Model id required");
});
});
});
describe("resolveCopilotForwardCompatModel", () => {
it("returns undefined for empty modelId", () => {
expect(resolveCopilotForwardCompatModel(createMockCtx(""))).toBeUndefined();
@@ -108,3 +166,141 @@ describe("resolveCopilotForwardCompatModel", () => {
}
});
});
describe("fetchCopilotUsage", () => {
it("returns HTTP errors for failed requests", async () => {
const mockFetch = createProviderUsageFetch(async () => makeResponse(500, "boom"));
const result = await fetchCopilotUsage("token", 5000, mockFetch);
expect(result.error).toBe("HTTP 500");
expect(result.windows).toHaveLength(0);
});
it("parses premium/chat usage from remaining percentages", async () => {
const mockFetch = createProviderUsageFetch(async (_url, init) => {
const headers = (init?.headers as Record<string, string> | undefined) ?? {};
expect(headers.Authorization).toBe("token token");
expect(headers["X-Github-Api-Version"]).toBe("2025-04-01");
return makeResponse(200, {
quota_snapshots: {
premium_interactions: { percent_remaining: 20 },
chat: { percent_remaining: 75 },
},
copilot_plan: "pro",
});
});
const result = await fetchCopilotUsage("token", 5000, mockFetch);
expect(result.plan).toBe("pro");
expect(result.windows).toEqual([
{ label: "Premium", usedPercent: 80 },
{ label: "Chat", usedPercent: 25 },
]);
});
it("defaults missing snapshot values and clamps invalid remaining percentages", async () => {
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(200, {
quota_snapshots: {
premium_interactions: { percent_remaining: null },
chat: { percent_remaining: 140 },
},
}),
);
const result = await fetchCopilotUsage("token", 5000, mockFetch);
expect(result.windows).toEqual([
{ label: "Premium", usedPercent: 100 },
{ label: "Chat", usedPercent: 0 },
]);
expect(result.plan).toBeUndefined();
});
it("returns an empty window list when quota snapshots are missing", async () => {
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(200, {
copilot_plan: "free",
}),
);
const result = await fetchCopilotUsage("token", 5000, mockFetch);
expect(result).toEqual({
provider: "github-copilot",
displayName: "Copilot",
windows: [],
plan: "free",
});
});
});
describe("github-copilot token", () => {
const cachePath = "/tmp/openclaw-state/credentials/github-copilot.token.json";
beforeEach(async () => {
vi.resetModules();
loadJsonFile.mockClear();
saveJsonFile.mockClear();
({ deriveCopilotApiBaseUrlFromToken, resolveCopilotApiToken } = await import("./token.js"));
});
it("derives baseUrl from token", async () => {
expect(deriveCopilotApiBaseUrlFromToken("token;proxy-ep=proxy.example.com;")).toBe(
"https://api.example.com",
);
expect(deriveCopilotApiBaseUrlFromToken("token;proxy-ep=https://proxy.foo.bar;")).toBe(
"https://api.foo.bar",
);
});
it("uses cache when token is still valid", async () => {
const now = Date.now();
loadJsonFile.mockReturnValue({
token: "cached;proxy-ep=proxy.example.com;",
expiresAt: now + 60 * 60 * 1000,
updatedAt: now,
});
const fetchImpl = vi.fn();
const res = await resolveCopilotApiToken({
githubToken: "gh",
cachePath,
loadJsonFileImpl: loadJsonFile,
saveJsonFileImpl: saveJsonFile,
fetchImpl: fetchImpl as unknown as typeof fetch,
});
expect(res.token).toBe("cached;proxy-ep=proxy.example.com;");
expect(res.baseUrl).toBe("https://api.example.com");
expect(String(res.source)).toContain("cache:");
expect(fetchImpl).not.toHaveBeenCalled();
});
it("fetches and stores token when cache is missing", async () => {
loadJsonFile.mockReturnValue(undefined);
const fetchImpl = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
token: "fresh;proxy-ep=https://proxy.contoso.test;",
expires_at: Math.floor(Date.now() / 1000) + 3600,
}),
});
const res = await resolveCopilotApiToken({
githubToken: "gh",
cachePath,
loadJsonFileImpl: loadJsonFile,
saveJsonFileImpl: saveJsonFile,
fetchImpl: fetchImpl as unknown as typeof fetch,
});
expect(res.token).toBe("fresh;proxy-ep=https://proxy.contoso.test;");
expect(res.baseUrl).toBe("https://api.contoso.test");
expect(saveJsonFile).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,84 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const loadJsonFile = vi.fn();
const saveJsonFile = vi.fn();
vi.mock("openclaw/plugin-sdk/json-store", () => ({
loadJsonFile,
saveJsonFile,
}));
vi.mock("openclaw/plugin-sdk/state-paths", () => ({
resolveStateDir: () => "/tmp/openclaw-state",
}));
let deriveCopilotApiBaseUrlFromToken: typeof import("./token.js").deriveCopilotApiBaseUrlFromToken;
let resolveCopilotApiToken: typeof import("./token.js").resolveCopilotApiToken;
describe("github-copilot token", () => {
const cachePath = "/tmp/openclaw-state/credentials/github-copilot.token.json";
beforeEach(async () => {
vi.resetModules();
loadJsonFile.mockClear();
saveJsonFile.mockClear();
({ deriveCopilotApiBaseUrlFromToken, resolveCopilotApiToken } = await import("./token.js"));
});
it("derives baseUrl from token", async () => {
expect(deriveCopilotApiBaseUrlFromToken("token;proxy-ep=proxy.example.com;")).toBe(
"https://api.example.com",
);
expect(deriveCopilotApiBaseUrlFromToken("token;proxy-ep=https://proxy.foo.bar;")).toBe(
"https://api.foo.bar",
);
});
it("uses cache when token is still valid", async () => {
const now = Date.now();
loadJsonFile.mockReturnValue({
token: "cached;proxy-ep=proxy.example.com;",
expiresAt: now + 60 * 60 * 1000,
updatedAt: now,
});
const fetchImpl = vi.fn();
const res = await resolveCopilotApiToken({
githubToken: "gh",
cachePath,
loadJsonFileImpl: loadJsonFile,
saveJsonFileImpl: saveJsonFile,
fetchImpl: fetchImpl as unknown as typeof fetch,
});
expect(res.token).toBe("cached;proxy-ep=proxy.example.com;");
expect(res.baseUrl).toBe("https://api.example.com");
expect(String(res.source)).toContain("cache:");
expect(fetchImpl).not.toHaveBeenCalled();
});
it("fetches and stores token when cache is missing", async () => {
loadJsonFile.mockReturnValue(undefined);
const fetchImpl = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
token: "fresh;proxy-ep=https://proxy.contoso.test;",
expires_at: Math.floor(Date.now() / 1000) + 3600,
}),
});
const res = await resolveCopilotApiToken({
githubToken: "gh",
cachePath,
loadJsonFileImpl: loadJsonFile,
saveJsonFileImpl: saveJsonFile,
fetchImpl: fetchImpl as unknown as typeof fetch,
});
expect(res.token).toBe("fresh;proxy-ep=https://proxy.contoso.test;");
expect(res.baseUrl).toBe("https://api.contoso.test");
expect(saveJsonFile).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,76 +0,0 @@
import { describe, expect, it } from "vitest";
import {
createProviderUsageFetch,
makeResponse,
} from "../../test/helpers/extensions/provider-usage-fetch.js";
import { fetchCopilotUsage } from "./usage.js";
describe("fetchCopilotUsage", () => {
it("returns HTTP errors for failed requests", async () => {
const mockFetch = createProviderUsageFetch(async () => makeResponse(500, "boom"));
const result = await fetchCopilotUsage("token", 5000, mockFetch);
expect(result.error).toBe("HTTP 500");
expect(result.windows).toHaveLength(0);
});
it("parses premium/chat usage from remaining percentages", async () => {
const mockFetch = createProviderUsageFetch(async (_url, init) => {
const headers = (init?.headers as Record<string, string> | undefined) ?? {};
expect(headers.Authorization).toBe("token token");
expect(headers["X-Github-Api-Version"]).toBe("2025-04-01");
return makeResponse(200, {
quota_snapshots: {
premium_interactions: { percent_remaining: 20 },
chat: { percent_remaining: 75 },
},
copilot_plan: "pro",
});
});
const result = await fetchCopilotUsage("token", 5000, mockFetch);
expect(result.plan).toBe("pro");
expect(result.windows).toEqual([
{ label: "Premium", usedPercent: 80 },
{ label: "Chat", usedPercent: 25 },
]);
});
it("defaults missing snapshot values and clamps invalid remaining percentages", async () => {
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(200, {
quota_snapshots: {
premium_interactions: { percent_remaining: null },
chat: { percent_remaining: 140 },
},
}),
);
const result = await fetchCopilotUsage("token", 5000, mockFetch);
expect(result.windows).toEqual([
{ label: "Premium", usedPercent: 100 },
{ label: "Chat", usedPercent: 0 },
]);
expect(result.plan).toBeUndefined();
});
it("returns an empty window list when quota snapshots are missing", async () => {
const mockFetch = createProviderUsageFetch(async () =>
makeResponse(200, {
copilot_plan: "free",
}),
);
const result = await fetchCopilotUsage("token", 5000, mockFetch);
expect(result).toEqual({
provider: "github-copilot",
displayName: "Copilot",
windows: [],
plan: "free",
});
});
});

View File

@@ -1,6 +1,7 @@
import * as providerAuth from "openclaw/plugin-sdk/provider-auth";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildGoogleImageGenerationProvider } from "./image-generation-provider.js";
import { __testing as geminiWebSearchTesting } from "./src/gemini-web-search-provider.js";
function mockGoogleApiKeyAuth() {
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
@@ -282,4 +283,20 @@ describe("Google image-generation provider", () => {
expect.any(Object),
);
});
it("prefers scoped configured Gemini API keys over environment fallbacks", () => {
expect(
geminiWebSearchTesting.resolveGeminiApiKey({
apiKey: "gemini-secret",
}),
).toBe("gemini-secret");
});
it("falls back to the default Gemini model when unset or blank", () => {
expect(geminiWebSearchTesting.resolveGeminiModel()).toBe("gemini-2.5-flash");
expect(geminiWebSearchTesting.resolveGeminiModel({ model: " " })).toBe("gemini-2.5-flash");
expect(geminiWebSearchTesting.resolveGeminiModel({ model: "gemini-2.5-pro" })).toBe(
"gemini-2.5-pro",
);
});
});

View File

@@ -1,18 +0,0 @@
import { describe, expect, it } from "vitest";
import { __testing } from "./gemini-web-search-provider.js";
describe("gemini web search provider", () => {
it("prefers scoped configured api keys over environment fallbacks", () => {
expect(
__testing.resolveGeminiApiKey({
apiKey: "gemini-secret",
}),
).toBe("gemini-secret");
});
it("falls back to the default Gemini model when unset or blank", () => {
expect(__testing.resolveGeminiModel()).toBe("gemini-2.5-flash");
expect(__testing.resolveGeminiModel({ model: " " })).toBe("gemini-2.5-flash");
expect(__testing.resolveGeminiModel({ model: "gemini-2.5-pro" })).toBe("gemini-2.5-pro");
});
});

View File

@@ -11,6 +11,7 @@ import {
setProcessPlatform,
snapshotPlatformPathEnv,
} from "./test-helpers.js";
import { resolveWindowsLobsterSpawn } from "./windows-spawn.js";
const spawnState = vi.hoisted(() => ({
queue: [] as Array<{ stdout: string; stderr?: string; exitCode?: number }>,
@@ -75,6 +76,19 @@ function fakeCtx(overrides: Partial<OpenClawPluginToolContext> = {}): OpenClawPl
};
}
async function expectUnwrappedShim(params: {
scriptPath: string;
shimPath: string;
shimLine: string;
}) {
await createWindowsCmdShimFixture(params);
const target = resolveWindowsLobsterSpawn(params.shimPath, ["run", "noop"], process.env);
expect(target.command).toBe(process.execPath);
expect(target.argv).toEqual([params.scriptPath, "run", "noop"]);
expect(target.windowsHide).toBe(true);
}
describe("lobster plugin tool", () => {
let tempDir = "";
const originalProcessState = snapshotPlatformPathEnv();
@@ -317,3 +331,97 @@ describe("lobster plugin tool", () => {
expect(factoryTool(fakeCtx({ sandboxed: false }))?.name).toBe("lobster");
});
});
describe("resolveWindowsLobsterSpawn", () => {
let tempDir = "";
const originalProcessState = snapshotPlatformPathEnv();
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lobster-win-spawn-"));
setProcessPlatform("win32");
});
afterEach(async () => {
restorePlatformPathEnv(originalProcessState);
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
tempDir = "";
}
});
it("unwraps cmd shim with %dp0% token", async () => {
const scriptPath = path.join(tempDir, "shim-dist", "lobster-cli.cjs");
const shimPath = path.join(tempDir, "shim", "lobster.cmd");
await expectUnwrappedShim({
shimPath,
scriptPath,
shimLine: `"%dp0%\\..\\shim-dist\\lobster-cli.cjs" %*`,
});
});
it("unwraps cmd shim with %~dp0% token", async () => {
const scriptPath = path.join(tempDir, "shim-dist", "lobster-cli.cjs");
const shimPath = path.join(tempDir, "shim", "lobster.cmd");
await expectUnwrappedShim({
shimPath,
scriptPath,
shimLine: `"%~dp0%\\..\\shim-dist\\lobster-cli.cjs" %*`,
});
});
it("ignores node.exe shim entries and picks lobster script", async () => {
const shimDir = path.join(tempDir, "shim-with-node");
const scriptPath = path.join(tempDir, "shim-dist-node", "lobster-cli.cjs");
const shimPath = path.join(shimDir, "lobster.cmd");
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
await fs.mkdir(shimDir, { recursive: true });
await fs.writeFile(path.join(shimDir, "node.exe"), "", "utf8");
await fs.writeFile(scriptPath, "module.exports = {};\n", "utf8");
await fs.writeFile(
shimPath,
`@echo off\r\n"%~dp0%\\node.exe" "%~dp0%\\..\\shim-dist-node\\lobster-cli.cjs" %*\r\n`,
"utf8",
);
const target = resolveWindowsLobsterSpawn(shimPath, ["run", "noop"], process.env);
expect(target.command).toBe(process.execPath);
expect(target.argv).toEqual([scriptPath, "run", "noop"]);
expect(target.windowsHide).toBe(true);
});
it("resolves lobster.cmd from PATH and unwraps npm layout shim", async () => {
const binDir = path.join(tempDir, "node_modules", ".bin");
const packageDir = path.join(tempDir, "node_modules", "lobster");
const scriptPath = path.join(packageDir, "dist", "cli.js");
const shimPath = path.join(binDir, "lobster.cmd");
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
await fs.mkdir(binDir, { recursive: true });
await fs.writeFile(shimPath, "@echo off\r\n", "utf8");
await fs.writeFile(
path.join(packageDir, "package.json"),
JSON.stringify({ name: "lobster", version: "0.0.0", bin: { lobster: "dist/cli.js" } }),
"utf8",
);
await fs.writeFile(scriptPath, "module.exports = {};\n", "utf8");
const env = {
...process.env,
PATH: `${binDir};${process.env.PATH ?? ""}`,
PATHEXT: ".CMD;.EXE",
};
const target = resolveWindowsLobsterSpawn("lobster", ["run", "noop"], env);
expect(target.command).toBe(process.execPath);
expect(target.argv).toEqual([scriptPath, "run", "noop"]);
expect(target.windowsHide).toBe(true);
});
it("fails fast when wrapper cannot be resolved without shell execution", async () => {
const badShimPath = path.join(tempDir, "bad-shim", "lobster.cmd");
await fs.mkdir(path.dirname(badShimPath), { recursive: true });
await fs.writeFile(badShimPath, "@echo off\r\nREM no entrypoint\r\n", "utf8");
expect(() => resolveWindowsLobsterSpawn(badShimPath, ["run", "noop"], process.env)).toThrow(
/without shell execution/,
);
});
});

View File

@@ -1,118 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
createWindowsCmdShimFixture,
restorePlatformPathEnv,
setProcessPlatform,
snapshotPlatformPathEnv,
} from "./test-helpers.js";
import { resolveWindowsLobsterSpawn } from "./windows-spawn.js";
describe("resolveWindowsLobsterSpawn", () => {
let tempDir = "";
const originalProcessState = snapshotPlatformPathEnv();
async function expectUnwrappedShim(params: {
scriptPath: string;
shimPath: string;
shimLine: string;
}) {
await createWindowsCmdShimFixture(params);
const target = resolveWindowsLobsterSpawn(params.shimPath, ["run", "noop"], process.env);
expect(target.command).toBe(process.execPath);
expect(target.argv).toEqual([params.scriptPath, "run", "noop"]);
expect(target.windowsHide).toBe(true);
}
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lobster-win-spawn-"));
setProcessPlatform("win32");
});
afterEach(async () => {
restorePlatformPathEnv(originalProcessState);
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
tempDir = "";
}
});
it("unwraps cmd shim with %dp0% token", async () => {
const scriptPath = path.join(tempDir, "shim-dist", "lobster-cli.cjs");
const shimPath = path.join(tempDir, "shim", "lobster.cmd");
await expectUnwrappedShim({
shimPath,
scriptPath,
shimLine: `"%dp0%\\..\\shim-dist\\lobster-cli.cjs" %*`,
});
});
it("unwraps cmd shim with %~dp0% token", async () => {
const scriptPath = path.join(tempDir, "shim-dist", "lobster-cli.cjs");
const shimPath = path.join(tempDir, "shim", "lobster.cmd");
await expectUnwrappedShim({
shimPath,
scriptPath,
shimLine: `"%~dp0%\\..\\shim-dist\\lobster-cli.cjs" %*`,
});
});
it("ignores node.exe shim entries and picks lobster script", async () => {
const shimDir = path.join(tempDir, "shim-with-node");
const scriptPath = path.join(tempDir, "shim-dist-node", "lobster-cli.cjs");
const shimPath = path.join(shimDir, "lobster.cmd");
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
await fs.mkdir(shimDir, { recursive: true });
await fs.writeFile(path.join(shimDir, "node.exe"), "", "utf8");
await fs.writeFile(scriptPath, "module.exports = {};\n", "utf8");
await fs.writeFile(
shimPath,
`@echo off\r\n"%~dp0%\\node.exe" "%~dp0%\\..\\shim-dist-node\\lobster-cli.cjs" %*\r\n`,
"utf8",
);
const target = resolveWindowsLobsterSpawn(shimPath, ["run", "noop"], process.env);
expect(target.command).toBe(process.execPath);
expect(target.argv).toEqual([scriptPath, "run", "noop"]);
expect(target.windowsHide).toBe(true);
});
it("resolves lobster.cmd from PATH and unwraps npm layout shim", async () => {
const binDir = path.join(tempDir, "node_modules", ".bin");
const packageDir = path.join(tempDir, "node_modules", "lobster");
const scriptPath = path.join(packageDir, "dist", "cli.js");
const shimPath = path.join(binDir, "lobster.cmd");
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
await fs.mkdir(binDir, { recursive: true });
await fs.writeFile(shimPath, "@echo off\r\n", "utf8");
await fs.writeFile(
path.join(packageDir, "package.json"),
JSON.stringify({ name: "lobster", version: "0.0.0", bin: { lobster: "dist/cli.js" } }),
"utf8",
);
await fs.writeFile(scriptPath, "module.exports = {};\n", "utf8");
const env = {
...process.env,
PATH: `${binDir};${process.env.PATH ?? ""}`,
PATHEXT: ".CMD;.EXE",
};
const target = resolveWindowsLobsterSpawn("lobster", ["run", "noop"], env);
expect(target.command).toBe(process.execPath);
expect(target.argv).toEqual([scriptPath, "run", "noop"]);
expect(target.windowsHide).toBe(true);
});
it("fails fast when wrapper cannot be resolved without shell execution", async () => {
const badShimPath = path.join(tempDir, "bad-shim", "lobster.cmd");
await fs.mkdir(path.dirname(badShimPath), { recursive: true });
await fs.writeFile(badShimPath, "@echo off\r\nREM no entrypoint\r\n", "utf8");
expect(() => resolveWindowsLobsterSpawn(badShimPath, ["run", "noop"], process.env)).toThrow(
/without shell execution/,
);
});
});

View File

@@ -12,6 +12,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
import { createLanceDbRuntimeLoader, type LanceDbRuntimeLogger } from "./lancedb-runtime.js";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "test-key";
const HAS_OPENAI_KEY = Boolean(process.env.OPENAI_API_KEY);
@@ -30,6 +31,23 @@ type MemoryPluginTestConfig = {
autoRecall?: boolean;
};
const TEST_RUNTIME_MANIFEST = {
name: "openclaw-memory-lancedb-runtime",
private: true as const,
type: "module" as const,
dependencies: {
"@lancedb/lancedb": "^0.27.1",
},
};
type LanceDbModule = typeof import("@lancedb/lancedb");
type RuntimeManifest = {
name: string;
private: true;
type: "module";
dependencies: Record<string, string>;
};
function installTmpDirHarness(params: { prefix: string }) {
let tmpDir = "";
let dbPath = "";
@@ -51,6 +69,47 @@ function installTmpDirHarness(params: { prefix: string }) {
};
}
function createMockModule(): LanceDbModule {
return {
connect: vi.fn(),
} as unknown as LanceDbModule;
}
function createRuntimeLoader(
overrides: {
env?: NodeJS.ProcessEnv;
importBundled?: () => Promise<LanceDbModule>;
importResolved?: (resolvedPath: string) => Promise<LanceDbModule>;
resolveRuntimeEntry?: (params: {
runtimeDir: string;
manifest: RuntimeManifest;
}) => string | null;
installRuntime?: (params: {
runtimeDir: string;
manifest: RuntimeManifest;
env: NodeJS.ProcessEnv;
logger?: LanceDbRuntimeLogger;
}) => Promise<string>;
} = {},
) {
return createLanceDbRuntimeLoader({
env: overrides.env ?? ({} as NodeJS.ProcessEnv),
resolveStateDir: () => "/tmp/openclaw-state",
runtimeManifest: TEST_RUNTIME_MANIFEST,
importBundled:
overrides.importBundled ??
(async () => {
throw new Error("Cannot find package '@lancedb/lancedb'");
}),
importResolved: overrides.importResolved ?? (async () => createMockModule()),
resolveRuntimeEntry: overrides.resolveRuntimeEntry ?? (() => null),
installRuntime:
overrides.installRuntime ??
(async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`),
});
}
describe("memory plugin e2e", () => {
const { getDbPath } = installTmpDirHarness({ prefix: "openclaw-memory-test-" });
@@ -289,6 +348,122 @@ describe("memory plugin e2e", () => {
});
});
describe("lancedb runtime loader", () => {
test("uses the bundled module when it is already available", async () => {
const bundledModule = createMockModule();
const importBundled = vi.fn(async () => bundledModule);
const importResolved = vi.fn(async () => createMockModule());
const resolveRuntimeEntry = vi.fn(() => null);
const installRuntime = vi.fn(async () => "/tmp/openclaw-state/plugin-runtimes/lancedb.js");
const loader = createRuntimeLoader({
importBundled,
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load()).resolves.toBe(bundledModule);
expect(resolveRuntimeEntry).not.toHaveBeenCalled();
expect(installRuntime).not.toHaveBeenCalled();
expect(importResolved).not.toHaveBeenCalled();
});
test("reuses an existing user runtime install before attempting a reinstall", async () => {
const runtimeModule = createMockModule();
const importResolved = vi.fn(async () => runtimeModule);
const resolveRuntimeEntry = vi.fn(
() => "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/runtime-entry.js",
);
const installRuntime = vi.fn(
async () => "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/runtime-entry.js",
);
const loader = createRuntimeLoader({
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load()).resolves.toBe(runtimeModule);
expect(resolveRuntimeEntry).toHaveBeenCalledWith(
expect.objectContaining({
runtimeDir: "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
}),
);
expect(installRuntime).not.toHaveBeenCalled();
});
test("installs LanceDB into user state when the bundled runtime is unavailable", async () => {
const runtimeModule = createMockModule();
const logger: LanceDbRuntimeLogger = {
warn: vi.fn(),
info: vi.fn(),
};
const importResolved = vi.fn(async () => runtimeModule);
const resolveRuntimeEntry = vi.fn(() => null);
const installRuntime = vi.fn(
async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`,
);
const loader = createRuntimeLoader({
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load(logger)).resolves.toBe(runtimeModule);
expect(installRuntime).toHaveBeenCalledWith(
expect.objectContaining({
runtimeDir: "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
manifest: TEST_RUNTIME_MANIFEST,
}),
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
"installing runtime deps under /tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
),
);
});
test("fails fast in nix mode instead of attempting auto-install", async () => {
const installRuntime = vi.fn(
async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`,
);
const loader = createRuntimeLoader({
env: { OPENCLAW_NIX_MODE: "1" } as NodeJS.ProcessEnv,
installRuntime,
});
await expect(loader.load()).rejects.toThrow(
"memory-lancedb: failed to load LanceDB and Nix mode disables auto-install.",
);
expect(installRuntime).not.toHaveBeenCalled();
});
test("clears the cached failure so later calls can retry the install", async () => {
const runtimeModule = createMockModule();
const installRuntime = vi
.fn()
.mockRejectedValueOnce(new Error("network down"))
.mockResolvedValueOnce(
"/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb/node_modules/@lancedb/lancedb/index.js",
);
const importResolved = vi.fn(async () => runtimeModule);
const loader = createRuntimeLoader({
installRuntime,
importResolved,
});
await expect(loader.load()).rejects.toThrow("network down");
await expect(loader.load()).resolves.toBe(runtimeModule);
expect(installRuntime).toHaveBeenCalledTimes(2);
});
});
// Live tests that require OpenAI API key and actually use LanceDB
describeLive("memory plugin live tests", () => {
const { getDbPath } = installTmpDirHarness({ prefix: "openclaw-memory-live-" });

View File

@@ -1,176 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { createLanceDbRuntimeLoader, type LanceDbRuntimeLogger } from "./lancedb-runtime.js";
const TEST_RUNTIME_MANIFEST = {
name: "openclaw-memory-lancedb-runtime",
private: true as const,
type: "module" as const,
dependencies: {
"@lancedb/lancedb": "^0.27.1",
},
};
type LanceDbModule = typeof import("@lancedb/lancedb");
type RuntimeManifest = {
name: string;
private: true;
type: "module";
dependencies: Record<string, string>;
};
function createMockModule(): LanceDbModule {
return {
connect: vi.fn(),
} as unknown as LanceDbModule;
}
function createLoader(
overrides: {
env?: NodeJS.ProcessEnv;
importBundled?: () => Promise<LanceDbModule>;
importResolved?: (resolvedPath: string) => Promise<LanceDbModule>;
resolveRuntimeEntry?: (params: {
runtimeDir: string;
manifest: RuntimeManifest;
}) => string | null;
installRuntime?: (params: {
runtimeDir: string;
manifest: RuntimeManifest;
env: NodeJS.ProcessEnv;
logger?: LanceDbRuntimeLogger;
}) => Promise<string>;
} = {},
) {
return createLanceDbRuntimeLoader({
env: overrides.env ?? ({} as NodeJS.ProcessEnv),
resolveStateDir: () => "/tmp/openclaw-state",
runtimeManifest: TEST_RUNTIME_MANIFEST,
importBundled:
overrides.importBundled ??
(async () => {
throw new Error("Cannot find package '@lancedb/lancedb'");
}),
importResolved: overrides.importResolved ?? (async () => createMockModule()),
resolveRuntimeEntry: overrides.resolveRuntimeEntry ?? (() => null),
installRuntime:
overrides.installRuntime ??
(async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`),
});
}
describe("lancedb runtime loader", () => {
it("uses the bundled module when it is already available", async () => {
const bundledModule = createMockModule();
const importBundled = vi.fn(async () => bundledModule);
const importResolved = vi.fn(async () => createMockModule());
const resolveRuntimeEntry = vi.fn(() => null);
const installRuntime = vi.fn(async () => "/tmp/openclaw-state/plugin-runtimes/lancedb.js");
const loader = createLoader({
importBundled,
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load()).resolves.toBe(bundledModule);
expect(resolveRuntimeEntry).not.toHaveBeenCalled();
expect(installRuntime).not.toHaveBeenCalled();
expect(importResolved).not.toHaveBeenCalled();
});
it("reuses an existing user runtime install before attempting a reinstall", async () => {
const runtimeModule = createMockModule();
const importResolved = vi.fn(async () => runtimeModule);
const resolveRuntimeEntry = vi.fn(
() => "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/runtime-entry.js",
);
const installRuntime = vi.fn(
async () => "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/runtime-entry.js",
);
const loader = createLoader({
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load()).resolves.toBe(runtimeModule);
expect(resolveRuntimeEntry).toHaveBeenCalledWith(
expect.objectContaining({
runtimeDir: "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
}),
);
expect(installRuntime).not.toHaveBeenCalled();
});
it("installs LanceDB into user state when the bundled runtime is unavailable", async () => {
const runtimeModule = createMockModule();
const logger: LanceDbRuntimeLogger = {
warn: vi.fn(),
info: vi.fn(),
};
const importResolved = vi.fn(async () => runtimeModule);
const resolveRuntimeEntry = vi.fn(() => null);
const installRuntime = vi.fn(
async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`,
);
const loader = createLoader({
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load(logger)).resolves.toBe(runtimeModule);
expect(installRuntime).toHaveBeenCalledWith(
expect.objectContaining({
runtimeDir: "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
manifest: TEST_RUNTIME_MANIFEST,
}),
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
"installing runtime deps under /tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
),
);
});
it("fails fast in nix mode instead of attempting auto-install", async () => {
const installRuntime = vi.fn(
async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`,
);
const loader = createLoader({
env: { OPENCLAW_NIX_MODE: "1" } as NodeJS.ProcessEnv,
installRuntime,
});
await expect(loader.load()).rejects.toThrow(
"memory-lancedb: failed to load LanceDB and Nix mode disables auto-install.",
);
expect(installRuntime).not.toHaveBeenCalled();
});
it("clears the cached failure so later calls can retry the install", async () => {
const runtimeModule = createMockModule();
const installRuntime = vi
.fn()
.mockRejectedValueOnce(new Error("network down"))
.mockResolvedValueOnce(
"/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb/node_modules/@lancedb/lancedb/index.js",
);
const importResolved = vi.fn(async () => runtimeModule);
const loader = createLoader({
installRuntime,
importResolved,
});
await expect(loader.load()).rejects.toThrow("network down");
await expect(loader.load()).resolves.toBe(runtimeModule);
expect(installRuntime).toHaveBeenCalledTimes(2);
});
});

View File

@@ -1,83 +0,0 @@
import * as providerAuth from "openclaw/plugin-sdk/provider-auth";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js";
describe("OpenAI image-generation provider", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("generates PNG buffers from the OpenAI Images API", async () => {
const resolveApiKeySpy = vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "sk-test",
source: "env",
mode: "api-key",
});
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: [
{
b64_json: Buffer.from("png-data").toString("base64"),
revised_prompt: "revised",
},
],
}),
});
vi.stubGlobal("fetch", fetchMock);
const provider = buildOpenAIImageGenerationProvider();
const authStore = { version: 1, profiles: {} };
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
prompt: "draw a cat",
cfg: {},
authStore,
});
expect(resolveApiKeySpy).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai",
store: authStore,
}),
);
expect(fetchMock).toHaveBeenCalledWith(
"https://api.openai.com/v1/images/generations",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
model: "gpt-image-1",
prompt: "draw a cat",
n: 1,
size: "1024x1024",
}),
}),
);
expect(result).toEqual({
images: [
{
buffer: Buffer.from("png-data"),
mimeType: "image/png",
fileName: "image-1.png",
revisedPrompt: "revised",
},
],
model: "gpt-image-1",
});
});
it("rejects reference-image edits for now", async () => {
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-1",
prompt: "Edit this image",
cfg: {},
inputImages: [{ buffer: Buffer.from("x"), mimeType: "image/png" }],
}),
).rejects.toThrow("does not support reference-image edits");
});
});

View File

@@ -2,7 +2,8 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import OpenAI from "openai";
import { describe, expect, it } from "vitest";
import * as providerAuth from "openclaw/plugin-sdk/provider-auth";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../src/config/config.js";
import { loadConfig } from "../../src/config/config.js";
import { encodePngRgba, fillPixel } from "../../src/media/png-encode.js";
@@ -11,8 +12,24 @@ import {
registerProviderPlugin,
requireRegisteredProvider,
} from "../../test/helpers/extensions/provider-registration.js";
import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js";
import plugin from "./index.js";
const runtimeMocks = vi.hoisted(() => ({
ensureGlobalUndiciEnvProxyDispatcher: vi.fn(),
getOAuthApiKey: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/infra-runtime", () => ({
ensureGlobalUndiciEnvProxyDispatcher: runtimeMocks.ensureGlobalUndiciEnvProxyDispatcher,
}));
vi.mock("@mariozechner/pi-ai/oauth", () => ({
getOAuthApiKey: runtimeMocks.getOAuthApiKey,
}));
import { getOAuthApiKey as getCodexOAuthApiKey } from "./openai-codex-provider.runtime.js";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
const LIVE_MODEL_ID = process.env.OPENCLAW_LIVE_OPENAI_PLUGIN_MODEL?.trim() || "gpt-5.4-nano";
const LIVE_IMAGE_MODEL = process.env.OPENCLAW_LIVE_OPENAI_IMAGE_MODEL?.trim() || "gpt-image-1";
@@ -164,6 +181,14 @@ async function createTempAgentDir(): Promise<string> {
}
describe("openai plugin", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("registers the expected provider surfaces", () => {
const { providers, speechProviders, mediaProviders, imageProviders } = registerOpenAIPlugin();
@@ -179,6 +204,109 @@ describe("openai plugin", () => {
expect(mediaProviders).toHaveLength(1);
expect(imageProviders).toHaveLength(1);
});
it("generates PNG buffers from the OpenAI Images API", async () => {
const resolveApiKeySpy = vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "sk-test",
source: "env",
mode: "api-key",
});
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: [
{
b64_json: Buffer.from("png-data").toString("base64"),
revised_prompt: "revised",
},
],
}),
});
vi.stubGlobal("fetch", fetchMock);
const provider = buildOpenAIImageGenerationProvider();
const authStore = { version: 1, profiles: {} };
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
prompt: "draw a cat",
cfg: {},
authStore,
});
expect(resolveApiKeySpy).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai",
store: authStore,
}),
);
expect(fetchMock).toHaveBeenCalledWith(
"https://api.openai.com/v1/images/generations",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
model: "gpt-image-1",
prompt: "draw a cat",
n: 1,
size: "1024x1024",
}),
}),
);
expect(result).toEqual({
images: [
{
buffer: Buffer.from("png-data"),
mimeType: "image/png",
fileName: "image-1.png",
revisedPrompt: "revised",
},
],
model: "gpt-image-1",
});
});
it("rejects reference-image edits for now", async () => {
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-1",
prompt: "Edit this image",
cfg: {},
inputImages: [{ buffer: Buffer.from("x"), mimeType: "image/png" }],
}),
).rejects.toThrow("does not support reference-image edits");
});
it("bootstraps the env proxy dispatcher before refreshing oauth credentials", async () => {
const refreshed = {
newCredentials: {
access: "next-access",
refresh: "next-refresh",
expires: Date.now() + 60_000,
},
};
runtimeMocks.getOAuthApiKey.mockResolvedValue(refreshed);
await expect(
getCodexOAuthApiKey("openai-codex", {
"openai-codex": {
provider: "openai-codex",
type: "oauth",
access: "access-token",
refresh: "refresh-token",
expires: Date.now(),
},
}),
).resolves.toBe(refreshed);
expect(runtimeMocks.ensureGlobalUndiciEnvProxyDispatcher).toHaveBeenCalledOnce();
expect(runtimeMocks.getOAuthApiKey).toHaveBeenCalledOnce();
expect(
runtimeMocks.ensureGlobalUndiciEnvProxyDispatcher.mock.invocationCallOrder[0],
).toBeLessThan(runtimeMocks.getOAuthApiKey.mock.invocationCallOrder[0]);
});
});
describeLive("openai plugin live", () => {

View File

@@ -1,51 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
ensureGlobalUndiciEnvProxyDispatcher: vi.fn(),
getOAuthApiKey: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/infra-runtime", () => ({
ensureGlobalUndiciEnvProxyDispatcher: mocks.ensureGlobalUndiciEnvProxyDispatcher,
}));
vi.mock("@mariozechner/pi-ai/oauth", () => ({
getOAuthApiKey: mocks.getOAuthApiKey,
}));
import { getOAuthApiKey } from "./openai-codex-provider.runtime.js";
describe("openai-codex-provider.runtime", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("bootstraps the env proxy dispatcher before refreshing oauth credentials", async () => {
const refreshed = {
newCredentials: {
access: "next-access",
refresh: "next-refresh",
expires: Date.now() + 60_000,
},
};
mocks.getOAuthApiKey.mockResolvedValue(refreshed);
await expect(
getOAuthApiKey("openai-codex", {
"openai-codex": {
provider: "openai-codex",
type: "oauth",
access: "access-token",
refresh: "refresh-token",
expires: Date.now(),
},
}),
).resolves.toBe(refreshed);
expect(mocks.ensureGlobalUndiciEnvProxyDispatcher).toHaveBeenCalledOnce();
expect(mocks.getOAuthApiKey).toHaveBeenCalledOnce();
expect(mocks.ensureGlobalUndiciEnvProxyDispatcher.mock.invocationCallOrder[0]).toBeLessThan(
mocks.getOAuthApiKey.mock.invocationCallOrder[0],
);
});
});

View File

@@ -1,37 +0,0 @@
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
describe("tavily plugin", () => {
it("registers web search provider and two tools", () => {
const registrations: {
webSearchProviders: unknown[];
tools: unknown[];
} = { webSearchProviders: [], tools: [] };
const mockApi = {
registerWebSearchProvider(provider: unknown) {
registrations.webSearchProviders.push(provider);
},
registerTool(tool: unknown) {
registrations.tools.push(tool);
},
config: {},
};
plugin.register(mockApi as never);
expect(plugin.id).toBe("tavily");
expect(plugin.name).toBe("Tavily Plugin");
expect(registrations.webSearchProviders).toHaveLength(1);
expect(registrations.tools).toHaveLength(2);
const provider = registrations.webSearchProviders[0] as Record<string, unknown>;
expect(provider.id).toBe("tavily");
expect(provider.autoDetectOrder).toBe(70);
expect(provider.envVars).toEqual(["TAVILY_API_KEY"]);
const toolNames = registrations.tools.map((t) => (t as Record<string, unknown>).name);
expect(toolNames).toContain("tavily_search");
expect(toolNames).toContain("tavily_extract");
});
});

View File

@@ -1,6 +1,7 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import plugin from "../index.js";
import {
DEFAULT_TAVILY_BASE_URL,
DEFAULT_TAVILY_EXTRACT_TIMEOUT_SECONDS,
@@ -63,6 +64,39 @@ describe("tavily tools", () => {
expect(applied.plugins?.entries?.tavily?.enabled).toBe(true);
});
it("registers web search provider and two tools", () => {
const registrations: {
webSearchProviders: unknown[];
tools: unknown[];
} = { webSearchProviders: [], tools: [] };
const mockApi = {
registerWebSearchProvider(provider: unknown) {
registrations.webSearchProviders.push(provider);
},
registerTool(tool: unknown) {
registrations.tools.push(tool);
},
config: {},
};
plugin.register(mockApi as never);
expect(plugin.id).toBe("tavily");
expect(plugin.name).toBe("Tavily Plugin");
expect(registrations.webSearchProviders).toHaveLength(1);
expect(registrations.tools).toHaveLength(2);
const provider = registrations.webSearchProviders[0] as Record<string, unknown>;
expect(provider.id).toBe("tavily");
expect(provider.autoDetectOrder).toBe(70);
expect(provider.envVars).toEqual(["TAVILY_API_KEY"]);
const toolNames = registrations.tools.map((t) => (t as Record<string, unknown>).name);
expect(toolNames).toContain("tavily_search");
expect(toolNames).toContain("tavily_extract");
});
it("maps generic provider args into Tavily search params", async () => {
const provider = createTavilyWebSearchProvider();
const tool = provider.createTool({

View File

@@ -1,157 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveXaiCatalogEntry } from "./model-definitions.js";
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
describe("xai provider models", () => {
it("publishes the newer Grok fast and code models in the bundled catalog", () => {
expect(resolveXaiCatalogEntry("grok-4-1-fast")).toMatchObject({
id: "grok-4-1-fast",
reasoning: true,
input: ["text", "image"],
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expect(resolveXaiCatalogEntry("grok-code-fast-1")).toMatchObject({
id: "grok-code-fast-1",
reasoning: true,
contextWindow: 256_000,
maxTokens: 10_000,
});
});
it("publishes Grok 4.20 reasoning and non-reasoning models", () => {
expect(resolveXaiCatalogEntry("grok-4.20-beta-latest-reasoning")).toMatchObject({
id: "grok-4.20-beta-latest-reasoning",
reasoning: true,
input: ["text", "image"],
contextWindow: 2_000_000,
});
expect(resolveXaiCatalogEntry("grok-4.20-beta-latest-non-reasoning")).toMatchObject({
id: "grok-4.20-beta-latest-non-reasoning",
reasoning: false,
contextWindow: 2_000_000,
});
});
it("keeps older Grok aliases resolving with current limits", () => {
expect(resolveXaiCatalogEntry("grok-4-1-fast-reasoning")).toMatchObject({
id: "grok-4-1-fast-reasoning",
reasoning: true,
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expect(resolveXaiCatalogEntry("grok-4.20-reasoning")).toMatchObject({
id: "grok-4.20-reasoning",
reasoning: true,
contextWindow: 2_000_000,
maxTokens: 30_000,
});
});
it("publishes the remaining Grok 3 family that Pi still carries", () => {
expect(resolveXaiCatalogEntry("grok-3-mini-fast")).toMatchObject({
id: "grok-3-mini-fast",
reasoning: true,
contextWindow: 131_072,
maxTokens: 8_192,
});
expect(resolveXaiCatalogEntry("grok-3-fast")).toMatchObject({
id: "grok-3-fast",
reasoning: false,
contextWindow: 131_072,
maxTokens: 8_192,
});
});
it("marks current Grok families as modern while excluding multi-agent ids", () => {
expect(isModernXaiModel("grok-4.20-beta-latest-reasoning")).toBe(true);
expect(isModernXaiModel("grok-code-fast-1")).toBe(true);
expect(isModernXaiModel("grok-3-mini-fast")).toBe(true);
expect(isModernXaiModel("grok-4.20-multi-agent-experimental-beta-0304")).toBe(false);
});
it("builds forward-compatible runtime models for newer Grok ids", () => {
const grok41 = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-4-1-fast",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
},
});
const grok420 = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-4.20-beta-latest-reasoning",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
},
});
const grok3Mini = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-3-mini-fast",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
},
});
expect(grok41).toMatchObject({
provider: "xai",
id: "grok-4-1-fast",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expect(grok420).toMatchObject({
provider: "xai",
id: "grok-4.20-beta-latest-reasoning",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
input: ["text", "image"],
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expect(grok3Mini).toMatchObject({
provider: "xai",
id: "grok-3-mini-fast",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
contextWindow: 131_072,
maxTokens: 8_192,
});
});
it("refuses the unsupported multi-agent endpoint ids", () => {
const model = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-4.20-multi-agent-experimental-beta-0304",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
},
});
expect(model).toBeUndefined();
});
});

View File

@@ -4,6 +4,8 @@ import {
} from "openclaw/plugin-sdk/provider-web-search";
import { describe, expect, it } from "vitest";
import { withEnv } from "../../test/helpers/extensions/env.js";
import { resolveXaiCatalogEntry } from "./model-definitions.js";
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
import { __testing as grokProviderTesting } from "./src/grok-web-search-provider.js";
import { __testing } from "./web-search.js";
@@ -159,3 +161,157 @@ describe("xai web search response parsing", () => {
expect(result.annotationCitations).toEqual(["https://example.com/direct"]);
});
});
describe("xai provider models", () => {
it("publishes the newer Grok fast and code models in the bundled catalog", () => {
expect(resolveXaiCatalogEntry("grok-4-1-fast")).toMatchObject({
id: "grok-4-1-fast",
reasoning: true,
input: ["text", "image"],
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expect(resolveXaiCatalogEntry("grok-code-fast-1")).toMatchObject({
id: "grok-code-fast-1",
reasoning: true,
contextWindow: 256_000,
maxTokens: 10_000,
});
});
it("publishes Grok 4.20 reasoning and non-reasoning models", () => {
expect(resolveXaiCatalogEntry("grok-4.20-beta-latest-reasoning")).toMatchObject({
id: "grok-4.20-beta-latest-reasoning",
reasoning: true,
input: ["text", "image"],
contextWindow: 2_000_000,
});
expect(resolveXaiCatalogEntry("grok-4.20-beta-latest-non-reasoning")).toMatchObject({
id: "grok-4.20-beta-latest-non-reasoning",
reasoning: false,
contextWindow: 2_000_000,
});
});
it("keeps older Grok aliases resolving with current limits", () => {
expect(resolveXaiCatalogEntry("grok-4-1-fast-reasoning")).toMatchObject({
id: "grok-4-1-fast-reasoning",
reasoning: true,
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expect(resolveXaiCatalogEntry("grok-4.20-reasoning")).toMatchObject({
id: "grok-4.20-reasoning",
reasoning: true,
contextWindow: 2_000_000,
maxTokens: 30_000,
});
});
it("publishes the remaining Grok 3 family that Pi still carries", () => {
expect(resolveXaiCatalogEntry("grok-3-mini-fast")).toMatchObject({
id: "grok-3-mini-fast",
reasoning: true,
contextWindow: 131_072,
maxTokens: 8_192,
});
expect(resolveXaiCatalogEntry("grok-3-fast")).toMatchObject({
id: "grok-3-fast",
reasoning: false,
contextWindow: 131_072,
maxTokens: 8_192,
});
});
it("marks current Grok families as modern while excluding multi-agent ids", () => {
expect(isModernXaiModel("grok-4.20-beta-latest-reasoning")).toBe(true);
expect(isModernXaiModel("grok-code-fast-1")).toBe(true);
expect(isModernXaiModel("grok-3-mini-fast")).toBe(true);
expect(isModernXaiModel("grok-4.20-multi-agent-experimental-beta-0304")).toBe(false);
});
it("builds forward-compatible runtime models for newer Grok ids", () => {
const grok41 = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-4-1-fast",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
},
});
const grok420 = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-4.20-beta-latest-reasoning",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
},
});
const grok3Mini = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-3-mini-fast",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
},
});
expect(grok41).toMatchObject({
provider: "xai",
id: "grok-4-1-fast",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expect(grok420).toMatchObject({
provider: "xai",
id: "grok-4.20-beta-latest-reasoning",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
input: ["text", "image"],
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expect(grok3Mini).toMatchObject({
provider: "xai",
id: "grok-3-mini-fast",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
reasoning: true,
contextWindow: 131_072,
maxTokens: 8_192,
});
});
it("refuses the unsupported multi-agent endpoint ids", () => {
const model = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-4.20-multi-agent-experimental-beta-0304",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
},
});
expect(model).toBeUndefined();
});
});