Files
openclaw/extensions/xai/web-search.test.ts
Peter Steinberger bb46b79d3c refactor: internalize OpenClaw agent runtime (#85341)
* refactor: extract agent core package

Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts.

* refactor: extract shared llm runtime

Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout.

* refactor: remove pi runtime internals

Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code.

* refactor: tighten agent session runtime

Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts.

* refactor: remove static model and pi auth paths

Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities.

* refactor: remove legacy provider compat paths

* docs: remove agent parity notes

* fix: skip provider wildcard metadata parsing

* refactor: share session extension sdk loading

* refactor: inline acpx proxy error formatter

* refactor: fold edit recovery into edit tool

* fix: accept extension batch separator

* test: align startup provider plugin expectations

* fix: restore provider-scoped release discovery

* test: align static asset packaging expectations

* fix: run static provider catalogs during scoped discovery

* fix: add provider entry catalogs for scoped live discovery

* fix: load lightweight provider catalog entries

* fix: refresh provider-scoped plugin metadata

* fix: keep provider catalog entries on release live path

* fix: keep static manifest models in release live checks

* fix: harden release model discovery

* fix: reduce OpenAI live cache probe reasoning

* fix: disable OpenAI cache probe reasoning

* ci: extend OpenAI gateway live timeout

* fix: extend live gateway model budget

* fix: stabilize release validation regressions

* fix: honor provider aliases in model rows

* fix: stabilize release validation lanes

* fix: stabilize release memory qa

* ci: stabilize release validation lanes

* ci: prefer ipv4 for live docker node calls

* fix: restore shared tool-call stream wrapper

* ci: remove legacy pi test shard alias

* fix: clean up embedded agent test drift

* fix: stabilize runtime alias status

* fix: clean up embedded agent ci drift

* fix: restore release ci invariants

* fix: clean up post-rebase runtime drift

* fix: restore release ci checks

* fix: restore release ci after rebase

* fix: remove stale pi runtime path

* test: align compaction runtime expectations

* test: update plugin prerelease expectations

* fix: handle claude live tool approvals

* fix: stabilize release validation gates

* fix: finish agent runtime import

* test: finish post-rebase agent runtime mocks

* fix: keep codex compaction native

* fix: stabilize codex app-server hook tests

* test: isolate codex diagnostic active run

* test: remove codex diagnostic completion race

# Conflicts:
#	extensions/codex/src/app-server/run-attempt.test.ts

* ci: fix full release manifest performance run id

* refactor: narrow llm plugin sdk boundary

* chore: drop generated google boundary stamps

* fix: repair rebase fallout

* fix: clean up rebased runtime references

* fix: decode codex jwt payloads as base64url

* fix: preserve shipped pi runtime alias

* fix: add scoped sdk virtual modules

* fix: decode llm codex oauth jwt as base64url

* fix: avoid stale vertex adc negative cache

* fix: harden tool arg decoding and codeql path

* fix: keep vertex adc negative checks live

* refactor: consolidate codex jwt and edit helpers

* fix: await codex oauth node runtime imports

* fix: preserve sdk tool and notice contracts

* fix: preserve shipped compat config boundaries

* fix: align codex oauth callback host

* fix: terminate agent-core loop streams on failure

* fix: keep codex oauth callback alive during fallback

* ci: include session tools in critical codeql scans

* fix: keep Cloudflare Anthropic provider auth header

* docs: redirect legacy pi runtime pages

* fix: honor bundled web provider compat discovery

* fix: protect session output spill files

* fix: keep legacy agent dir env blocked

* fix: contain auto-discovered skill symlinks

* fix: harden agent core sdk proxy surfaces

* fix: restore approval reaction sdk compat

* fix: keep live docker runs bounded

* fix: keep codex oauth redirect host aligned

* fix: resolve post-rebase agent runtime drift

* fix: redact anthropic oauth parse failures

* fix: preserve responses strict tool shaping

* fix: repair agent runtime rebase cleanup

* docs: redirect retired parity pages

* fix: bound auto-discovered resources to roots

* fix: repair post-rebase agent test drift

* fix: preserve bundled provider allowlist migration

* fix: preserve manifest-owned provider aliases

* fix: declare photon image dependency

* fix: keep provider headers out of proxy body

* fix: preserve shipped env aliases

* fix: refresh control ui i18n generated state

* fix: quote read fallback paths

* fix: preview edits through configured backend

* test: satisfy core test typecheck

* fix: preserve ZAI usage auth fallback

* test: repair codex diagnostic test

* fix: repair agent runtime rebase drift

* test: finish embedded runner import rename

* fix: repair agent runtime rebase integrations

* test: align compaction oauth fallback expectations

* fix: allow sdk-auth session models

* fix: update doctor tool schema import

* fix: preserve bedrock plugin region

* fix: stream harmony-like prose immediately

* ci: include session runtime in codeql shards

* fix: repair latest rebase integrations

* fix: honor explicit codex websocket transport

* fix: keep openai-compatible credentials provider-scoped

* fix: refresh sdk api baseline after rebase

* fix: route cli runtime aliases through openclaw harness

* test: rename stale harness mock expectation

* test: rename embedded agent overflow calls

* test: clean embedded auth test wording

* test: use openclaw stream types in deepinfra cache test

* fix: refresh sdk api baseline on latest main

* fix: honor bundled discovery compat allowlists

* fix: refresh sdk api baseline after latest rebase

* fix: remove stale rebase imports

* test: rename stale model catalog mock

* test: mock renamed doctor runtime modules

* fix: map canonical kimi env auth

* fix: use internal model registry in bench script

* fix: migrate deepinfra provider catalog entry

* fix: enforce builtin tool suppression

* fix: route compaction auth and proxy payloads safely

* refactor: prune unused llm registry leftovers

* test: update codex hooks session import

* test: fix model picker ci coverage

* test: align model picker auth mock types
2026-05-27 19:24:04 +01:00

1243 lines
38 KiB
TypeScript

import { createTestWizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime";
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
import { createNonExitingRuntime } from "openclaw/plugin-sdk/runtime-env";
import { withEnv, withEnvAsync, withFetchPreconnect } from "openclaw/plugin-sdk/test-env";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildXaiCatalogModels, resolveXaiCatalogEntry } from "./model-definitions.js";
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
import { resolveFallbackXaiAuth } from "./src/tool-auth-shared.js";
import { wrapXaiWebSearchError } from "./src/web-search-shared.js";
import { testing } from "./test-api.js";
import { createXaiWebSearchProvider as createXaiWebSearchContractProvider } from "./web-search-contract-api.js";
import { createXaiWebSearchProvider } from "./web-search.js";
const providerAuthRuntimeMocks = vi.hoisted(() => ({
resolveApiKeyForProvider: vi.fn(),
}));
const providerAuthMocks = vi.hoisted(() => ({
ensureAuthProfileStore: vi.fn(),
listUsableProviderAuthProfileIds: vi.fn(() => ({ agentDir: "", profileIds: [] as string[] })),
}));
vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => {
const original = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth")>();
return {
...original,
ensureAuthProfileStore: providerAuthMocks.ensureAuthProfileStore,
listUsableProviderAuthProfileIds: providerAuthMocks.listUsableProviderAuthProfileIds,
};
});
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", async (importOriginal) => {
const original =
await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth-runtime")>();
return {
...original,
resolveApiKeyForProvider: providerAuthRuntimeMocks.resolveApiKeyForProvider,
};
});
vi.mock("openclaw/plugin-sdk/provider-web-search", async (importOriginal) => {
const original = await importOriginal<typeof import("openclaw/plugin-sdk/provider-web-search")>();
return {
...original,
postTrustedWebToolsJson: async (
params: {
url: string;
apiKey: string;
body: Record<string, unknown>;
extraHeaders?: Record<string, string>;
},
parseResponse: (response: Response) => Promise<unknown>,
) => {
const response = await globalThis.fetch(params.url, {
method: "POST",
headers: {
...params.extraHeaders,
Accept: "application/json",
Authorization: `Bearer ${params.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(params.body),
});
if (!response.ok) {
const detail =
typeof response.text === "function"
? await response.text()
: response.statusText || String(response.status);
throw new Error(`xAI API error (${response.status}): ${detail || response.statusText}`);
}
return await parseResponse(response);
},
};
});
const {
extractXaiWebSearchContent,
resolveXaiInlineCitations,
resolveXaiToolSearchConfig,
resolveXaiWebSearchCredential,
resolveXaiWebSearchModel,
resolveXaiWebSearchTimeoutSeconds,
} = testing;
function installXaiWebSearchFetch() {
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
output: [
{
type: "message",
content: [{ type: "output_text", text: "Grounded Grok answer" }],
},
],
}),
} as Response),
);
global.fetch = withFetchPreconnect(mockFetch);
return mockFetch;
}
function firstFetchUrl(mockFetch: ReturnType<typeof installXaiWebSearchFetch>) {
const [call] = mockFetch.mock.calls;
if (!call) {
throw new Error("expected xai web search fetch call");
}
const [url] = call;
return String(url);
}
function fetchCallHeader(
mockFetch: { mock: { calls: unknown[][] } },
index: number,
name: string,
): string | undefined {
const init = mockFetch.mock.calls[index]?.[1] as RequestInit | undefined;
const headers = init?.headers;
if (!headers) {
return undefined;
}
if (headers instanceof Headers) {
return headers.get(name) ?? undefined;
}
if (Array.isArray(headers)) {
const lowerName = name.toLowerCase();
return headers.find(([key]) => key.toLowerCase() === lowerName)?.[1];
}
return (headers as Record<string, string | undefined>)[name];
}
function expectCatalogEntry(
modelId: string,
expected: {
id?: string;
reasoning?: boolean;
input?: string[];
contextWindow?: number;
maxTokens?: number;
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
},
) {
const entry = resolveXaiCatalogEntry(modelId);
expect(entry?.id).toBe(expected.id ?? modelId);
if ("reasoning" in expected) {
expect(entry?.reasoning).toBe(expected.reasoning);
}
if (expected.input) {
expect(entry?.input).toEqual(expected.input);
}
if (expected.contextWindow !== undefined) {
expect(entry?.contextWindow).toBe(expected.contextWindow);
}
if (expected.maxTokens !== undefined) {
expect(entry?.maxTokens).toBe(expected.maxTokens);
}
if (expected.cost) {
expect(entry?.cost).toEqual(expected.cost);
}
}
afterEach(() => {
vi.restoreAllMocks();
providerAuthMocks.ensureAuthProfileStore.mockReset();
providerAuthMocks.listUsableProviderAuthProfileIds.mockReset();
providerAuthMocks.listUsableProviderAuthProfileIds.mockReturnValue({
agentDir: "",
profileIds: [],
});
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockReset();
});
describe("xai web search config resolution", () => {
it("advertises xAI auth profiles in runtime and setup contracts", () => {
expect(createXaiWebSearchProvider().authProviderId).toBe("xai");
expect(createXaiWebSearchContractProvider().authProviderId).toBe("xai");
});
it("prefers configured api keys and resolves grok scoped defaults", () => {
expect(resolveXaiWebSearchCredential({ grok: { apiKey: "xai-secret" } })).toBe("xai-secret");
expect(resolveXaiWebSearchModel()).toBe("grok-4-1-fast");
expect(resolveXaiInlineCitations()).toBe(false);
});
it("uses config apiKey when provided", () => {
expect(resolveXaiWebSearchCredential({ grok: { apiKey: "xai-test-key" } })).toBe(
"xai-test-key",
);
});
it("returns undefined when no apiKey is available", () => {
withEnv({ XAI_API_KEY: undefined }, () => {
expect(resolveXaiWebSearchCredential({})).toBeUndefined();
});
});
it("resolves env SecretRefs without requiring a runtime snapshot", () => {
withEnv({ XAI_WEB_SEARCH_KEY: "xai-env-ref-key" }, () => {
expect(
resolveXaiWebSearchCredential({
grok: {
apiKey: {
source: "env",
provider: "default",
id: "XAI_WEB_SEARCH_KEY",
},
},
}),
).toBe("xai-env-ref-key");
});
});
it("merges canonical plugin config into the tool search config", () => {
const searchConfig = resolveXaiToolSearchConfig({
config: {
plugins: {
entries: {
xai: {
enabled: true,
config: {
webSearch: {
apiKey: "plugin-key",
inlineCitations: true,
model: "grok-4-fast-reasoning",
},
},
},
},
},
},
searchConfig: { provider: "grok" },
});
expect(resolveXaiWebSearchCredential(searchConfig)).toBe("plugin-key");
expect(resolveXaiInlineCitations(searchConfig)).toBe(true);
expect(resolveXaiWebSearchModel(searchConfig)).toBe("grok-4-fast");
});
it("treats unresolved non-env SecretRefs as missing credentials instead of using env fallback", async () => {
await withEnvAsync({ XAI_API_KEY: "ambient-xai-test-key" }, async () => {
const provider = createXaiWebSearchProvider();
const maybeTool = provider.createTool({
config: {
plugins: {
entries: {
xai: {
enabled: true,
config: {
webSearch: {
apiKey: {
source: "file",
provider: "vault",
id: "/providers/xai/web-search",
},
},
},
},
},
},
},
});
if (!maybeTool) {
throw new Error("expected xai web search tool");
}
const result = await maybeTool.execute({ query: "OpenClaw" });
expect(result.error).toBe("missing_xai_api_key");
expect(result.message).toContain("use web_fetch for a specific URL or the browser tool");
});
});
it("uses xAI OAuth auth before API-key fallback for web search", async () => {
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue({
apiKey: "oauth-web-search-token",
source: "profile:xai:default",
mode: "oauth",
profileId: "xai:default",
});
const mockFetch = installXaiWebSearchFetch();
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
agents: {
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
},
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "configured-xai-key",
},
},
},
},
},
},
});
if (!tool) {
throw new Error("Expected xAI web search tool");
}
await tool.execute({ query: "OpenClaw Grok OAuth web search" });
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenCalledWith(
expect.objectContaining({
provider: "xai",
agentDir: "/tmp/openclaw-xai-main-agent",
}),
);
expect(fetchCallHeader(mockFetch, 0, "Authorization")).toBe("Bearer oauth-web-search-token");
});
it("uses the active agentDir for xAI OAuth web search auth", async () => {
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockResolvedValue({
apiKey: "active-agent-oauth-token",
source: "profile:xai:active",
mode: "oauth",
profileId: "xai:active",
});
const mockFetch = installXaiWebSearchFetch();
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
agentDir: "/tmp/openclaw-xai-active-agent",
config: {
agents: {
list: [
{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" },
{ id: "side", agentDir: "/tmp/openclaw-xai-active-agent" },
],
},
},
});
if (!tool) {
throw new Error("Expected xAI web search tool");
}
await tool.execute({ query: "OpenClaw Grok active agent OAuth web search" });
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenCalledWith(
expect.objectContaining({
provider: "xai",
agentDir: "/tmp/openclaw-xai-active-agent",
}),
);
expect(fetchCallHeader(mockFetch, 0, "Authorization")).toBe("Bearer active-agent-oauth-token");
});
it("refreshes xAI OAuth auth and retries web search after a 401", async () => {
providerAuthRuntimeMocks.resolveApiKeyForProvider
.mockResolvedValueOnce({
apiKey: "expired-oauth-token",
source: "profile:xai:default",
mode: "oauth",
profileId: "xai:default",
})
.mockResolvedValueOnce({
apiKey: "fresh-oauth-token",
source: "profile:xai:default",
mode: "oauth",
profileId: "xai:default",
});
const mockFetch = vi
.fn()
.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
text: () => Promise.resolve("expired"),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
output: [
{
type: "message",
content: [{ type: "output_text", text: "Fresh OAuth Grok answer" }],
},
],
}),
} as Response);
global.fetch = withFetchPreconnect(mockFetch);
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
agents: {
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
},
tools: {
web: {
search: {
provider: "grok",
},
},
},
},
});
if (!tool) {
throw new Error("Expected xAI web search tool");
}
const result = await tool.execute({ query: "OpenClaw Grok OAuth refresh test" });
expect(result.content).toContain("Fresh OAuth Grok answer");
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
provider: "xai",
agentDir: "/tmp/openclaw-xai-main-agent",
profileId: "xai:default",
lockedProfile: true,
forceRefresh: true,
}),
);
expect(fetchCallHeader(mockFetch, 0, "Authorization")).toBe("Bearer expired-oauth-token");
expect(fetchCallHeader(mockFetch, 1, "Authorization")).toBe("Bearer fresh-oauth-token");
});
it("falls back to xAI API-key auth when OAuth refresh cannot recover", async () => {
providerAuthRuntimeMocks.resolveApiKeyForProvider
.mockResolvedValueOnce({
apiKey: "expired-oauth-token",
source: "profile:xai:default",
mode: "oauth",
profileId: "xai:default",
})
.mockResolvedValueOnce({
apiKey: "expired-oauth-token",
source: "profile:xai:default",
mode: "oauth",
profileId: "xai:default",
})
.mockResolvedValueOnce({
apiKey: "xai-env-fallback-key",
source: "XAI_API_KEY",
mode: "api-key",
});
const mockFetch = vi
.fn()
.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
text: () => Promise.resolve("revoked"),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
output: [
{
type: "message",
content: [{ type: "output_text", text: "API key fallback Grok answer" }],
},
],
}),
} as Response);
global.fetch = withFetchPreconnect(mockFetch);
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
agents: {
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
},
tools: {
web: {
search: {
provider: "grok",
},
},
},
},
});
if (!tool) {
throw new Error("Expected xAI web search tool");
}
const result = await tool.execute({ query: "OpenClaw Grok API fallback test" });
expect(result.content).toContain("API key fallback Grok answer");
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
provider: "xai",
agentDir: "/tmp/openclaw-xai-main-agent",
credentialPrecedence: "env-first",
}),
);
expect(fetchCallHeader(mockFetch, 1, "Authorization")).toBe("Bearer xai-env-fallback-key");
});
it("falls back to an xAI API-key auth profile when stale OAuth remains first", async () => {
providerAuthRuntimeMocks.resolveApiKeyForProvider
.mockResolvedValueOnce({
apiKey: "expired-oauth-token",
source: "profile:xai:default",
mode: "oauth",
profileId: "xai:default",
})
.mockResolvedValueOnce({
apiKey: "expired-oauth-token",
source: "profile:xai:default",
mode: "oauth",
profileId: "xai:default",
})
.mockResolvedValueOnce({
apiKey: "expired-oauth-token",
source: "profile:xai:default",
mode: "oauth",
profileId: "xai:default",
})
.mockResolvedValueOnce({
apiKey: "xai-profile-api-key",
source: "profile:xai:key",
mode: "api-key",
profileId: "xai:key",
});
providerAuthMocks.listUsableProviderAuthProfileIds.mockReturnValue({
agentDir: "/tmp/openclaw-xai-main-agent",
profileIds: ["xai:default", "xai:key"],
});
providerAuthMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
active: "xai:default",
profiles: {
"xai:default": {
provider: "xai",
type: "oauth",
access: "expired-oauth-token",
refresh: "refresh-oauth-token",
expires: Date.now() + 3_600_000,
},
"xai:key": {
provider: "xai",
type: "api_key",
keyRef: { source: "env", id: "XAI_API_KEY" },
},
},
});
const mockFetch = vi
.fn()
.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
text: () => Promise.resolve("revoked"),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
output: [
{
type: "message",
content: [{ type: "output_text", text: "Profile API key Grok answer" }],
},
],
}),
} as Response);
global.fetch = withFetchPreconnect(mockFetch);
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
agents: {
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
},
tools: {
web: {
search: {
provider: "grok",
},
},
},
},
});
if (!tool) {
throw new Error("Expected xAI web search tool");
}
const result = await tool.execute({ query: "OpenClaw Grok profile fallback test" });
expect(result.content).toContain("Profile API key Grok answer");
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenNthCalledWith(
4,
expect.objectContaining({
provider: "xai",
agentDir: "/tmp/openclaw-xai-main-agent",
profileId: "xai:key",
lockedProfile: true,
}),
);
expect(fetchCallHeader(mockFetch, 1, "Authorization")).toBe("Bearer xai-profile-api-key");
});
it("falls back to env auth after a stale xAI API-key auth profile returns unauthorized", async () => {
providerAuthRuntimeMocks.resolveApiKeyForProvider
.mockResolvedValueOnce({
apiKey: "stale-profile-key",
source: "profile:xai:default",
mode: "api-key",
profileId: "xai:default",
})
.mockResolvedValueOnce({
apiKey: "xai-env-fallback-key",
source: "XAI_API_KEY",
mode: "api-key",
});
const mockFetch = vi
.fn()
.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
text: () => Promise.resolve("stale api key"),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
output: [
{
type: "message",
content: [{ type: "output_text", text: "Env fallback Grok answer" }],
},
],
}),
} as Response);
global.fetch = withFetchPreconnect(mockFetch);
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
agents: {
list: [{ id: "main", default: true, agentDir: "/tmp/openclaw-xai-main-agent" }],
},
tools: {
web: {
search: {
provider: "grok",
},
},
},
},
});
if (!tool) {
throw new Error("Expected xAI web search tool");
}
const result = await tool.execute({ query: "OpenClaw Grok API-key fallback test" });
expect(result.content).toContain("Env fallback Grok answer");
expect(providerAuthRuntimeMocks.resolveApiKeyForProvider).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
provider: "xai",
agentDir: "/tmp/openclaw-xai-main-agent",
credentialPrecedence: "env-first",
}),
);
expect(fetchCallHeader(mockFetch, 0, "Authorization")).toBe("Bearer stale-profile-key");
expect(fetchCallHeader(mockFetch, 1, "Authorization")).toBe("Bearer xai-env-fallback-key");
});
it("offers plugin-owned xSearch setup after Grok is selected", async () => {
const provider = createXaiWebSearchProvider();
const select = vi.fn().mockResolvedValueOnce("yes").mockResolvedValueOnce("grok-4-1-fast");
const prompter = createTestWizardPrompter({
select: select as never,
});
const next = await provider.runSetup?.({
config: {
plugins: {
entries: {
xai: {
enabled: true,
config: {
webSearch: {
apiKey: "xai-test-key",
},
},
},
},
},
tools: {
web: {
search: {
provider: "grok",
enabled: true,
},
},
},
},
runtime: createNonExitingRuntime(),
prompter,
});
const xSearch = next?.plugins?.entries?.xai?.config?.xSearch as
| { enabled?: boolean; model?: string }
| undefined;
expect(xSearch?.enabled).toBe(true);
expect(xSearch?.model).toBe("grok-4-1-fast");
});
it("keeps explicit xSearch disablement untouched during provider-owned setup", async () => {
const provider = createXaiWebSearchProvider();
const config = {
plugins: {
entries: {
xai: {
config: {
xSearch: {
enabled: false,
},
},
},
},
},
tools: {
web: {
search: {
provider: "grok",
enabled: true,
},
},
},
};
const prompter = createTestWizardPrompter();
const next = await provider.runSetup?.({
config,
runtime: createNonExitingRuntime(),
prompter,
});
expect(next).toEqual(config);
expect(prompter.note).not.toHaveBeenCalled();
});
it("reuses the plugin web search api key for provider auth fallback", () => {
expect(
resolveFallbackXaiAuth({
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-provider-fallback", // pragma: allowlist secret
},
},
},
},
},
} as never),
).toEqual({
apiKey: "xai-provider-fallback",
source: "plugins.entries.xai.config.webSearch.apiKey",
});
});
it("reuses the legacy grok web search api key for provider auth fallback", () => {
expect(
resolveFallbackXaiAuth({
tools: {
web: {
search: {
grok: {
apiKey: "xai-legacy-fallback", // pragma: allowlist secret
},
},
},
},
} as never),
).toEqual({
apiKey: "xai-legacy-fallback",
source: "tools.web.search.grok.apiKey",
});
});
it("returns a managed marker for SecretRef-backed plugin auth fallback", () => {
expect(
resolveFallbackXaiAuth({
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/api-key" },
},
},
},
},
},
} as never),
).toEqual({
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
});
});
it("uses default model when not specified", () => {
expect(resolveXaiWebSearchModel({})).toBe("grok-4-1-fast");
expect(resolveXaiWebSearchModel(undefined)).toBe("grok-4-1-fast");
});
it("uses a Grok-specific 60s default timeout while preserving overrides", () => {
expect(resolveXaiWebSearchTimeoutSeconds({})).toBe(60);
expect(resolveXaiWebSearchTimeoutSeconds(undefined)).toBe(60);
expect(resolveXaiWebSearchTimeoutSeconds({ timeoutSeconds: 15 })).toBe(15);
});
it("uses config model when provided", () => {
expect(resolveXaiWebSearchModel({ grok: { model: "grok-4-fast-reasoning" } })).toBe(
"grok-4-fast",
);
});
it("routes Grok web search through plugin webSearch.baseUrl", async () => {
const mockFetch = installXaiWebSearchFetch();
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-config-test",
baseUrl: "https://api.x.ai/proxy/v1/",
},
},
},
},
},
},
searchConfig: { provider: "grok" },
});
if (!tool) {
throw new Error("Expected xAI web search tool");
}
await tool.execute({ query: "OpenClaw Grok proxy test" });
expect(firstFetchUrl(mockFetch)).toBe("https://api.x.ai/proxy/v1/responses");
});
it("reports malformed xAI web search JSON as a provider error", async () => {
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
Promise.resolve({
ok: true,
json: () => Promise.reject(new SyntaxError("Unexpected token")),
} as Response),
);
global.fetch = withFetchPreconnect(mockFetch);
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-test-key", // pragma: allowlist secret
},
},
},
},
},
},
});
if (!tool) {
throw new Error("Expected tool definition");
}
await expect(tool.execute({ query: "OpenClaw" })).rejects.toThrow(
"xAI web search failed: malformed JSON response",
);
});
it("rejects xAI web search success JSON without answer text", async () => {
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ output: [] }),
} as Response),
);
global.fetch = withFetchPreconnect(mockFetch);
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-test-key", // pragma: allowlist secret
},
},
},
},
},
},
});
if (!tool) {
throw new Error("Expected tool definition");
}
await expect(tool.execute({ query: "OpenClaw" })).rejects.toThrow(
"xAI web search failed: malformed JSON response",
);
});
it("normalizes deprecated grok 4.20 beta model ids to GA ids", () => {
expect(
resolveXaiWebSearchModel({
grok: { model: "grok-4.20-experimental-beta-0304-reasoning" },
}),
).toBe("grok-4.20-beta-latest-reasoning");
expect(
resolveXaiWebSearchModel({
grok: { model: "grok-4.20-experimental-beta-0304-non-reasoning" },
}),
).toBe("grok-4.20-beta-latest-non-reasoning");
});
it("defaults inlineCitations to false", () => {
expect(resolveXaiInlineCitations({})).toBe(false);
expect(resolveXaiInlineCitations(undefined)).toBe(false);
});
it("respects inlineCitations config", () => {
expect(resolveXaiInlineCitations({ grok: { inlineCitations: true } })).toBe(true);
expect(resolveXaiInlineCitations({ grok: { inlineCitations: false } })).toBe(false);
});
it("builds wrapped payloads with optional inline citations", () => {
const payload = testing.buildXaiWebSearchPayload({
query: "q",
provider: "grok",
model: "grok-4-fast",
tookMs: 12,
content: "body",
citations: ["https://a.test"],
});
expect(payload.query).toBe("q");
expect(payload.provider).toBe("grok");
expect(payload.model).toBe("grok-4-fast");
expect(payload.tookMs).toBe(12);
expect(payload.citations).toEqual(["https://a.test"]);
const externalContent = payload.externalContent as { wrapped?: boolean } | undefined;
expect(externalContent?.wrapped).toBe(true);
});
it("converts internal xAI timeout aborts into structured tool errors", () => {
const abort = new DOMException("This operation was aborted", "AbortError");
expect(() => wrapXaiWebSearchError(abort, 60)).toThrow("xAI web search timed out after 60s");
try {
wrapXaiWebSearchError(abort, 60);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).name).toBe("Error");
expect((error as Error).cause).toBe(abort);
}
});
});
describe("xai web search response parsing", () => {
it("extracts content from Responses API message blocks", () => {
const result = extractXaiWebSearchContent({
output: [
{
type: "message",
content: [{ type: "output_text", text: "hello from output" }],
},
],
});
expect(result.text).toBe("hello from output");
expect(result.annotationCitations).toStrictEqual([]);
});
it("extracts url_citation annotations from content blocks", () => {
const result = extractXaiWebSearchContent({
output: [
{
type: "message",
content: [
{
type: "output_text",
text: "hello with citations",
annotations: [
{ type: "url_citation", url: "https://example.com/a" },
{ type: "url_citation", url: "https://example.com/b" },
{ type: "url_citation", url: "https://example.com/a" },
],
},
],
},
],
});
expect(result.text).toBe("hello with citations");
expect(result.annotationCitations).toEqual(["https://example.com/a", "https://example.com/b"]);
});
it("falls back to deprecated output_text", () => {
const result = extractXaiWebSearchContent({ output_text: "hello from output_text" });
expect(result.text).toBe("hello from output_text");
expect(result.annotationCitations).toStrictEqual([]);
});
it("returns undefined text when no content found", () => {
const result = extractXaiWebSearchContent({});
expect(result.text).toBeUndefined();
expect(result.annotationCitations).toStrictEqual([]);
});
it("extracts output_text blocks directly in output array", () => {
const result = extractXaiWebSearchContent({
output: [
{ type: "web_search_call" },
{
type: "output_text",
text: "direct output text",
annotations: [{ type: "url_citation", url: "https://example.com/direct" }],
},
],
});
expect(result.text).toBe("direct output text");
expect(result.annotationCitations).toEqual(["https://example.com/direct"]);
});
});
describe("xai provider models", () => {
it("publishes only current selectable chat models newest first", () => {
expect(buildXaiCatalogModels().map((model) => model.id)).toEqual([
"grok-build-0.1",
"grok-4.3",
"grok-4.20-beta-latest-reasoning",
"grok-4.20-beta-latest-non-reasoning",
]);
});
it("publishes Grok 4.3 as the default chat model", () => {
expectCatalogEntry("grok-4.3", {
id: "grok-4.3",
reasoning: true,
input: ["text", "image"],
contextWindow: 1_000_000,
maxTokens: 64_000,
cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
});
});
it("keeps retired Grok fast slugs resolving for compatibility", () => {
expectCatalogEntry("grok-4-1-fast", {
id: "grok-4-1-fast",
reasoning: true,
input: ["text", "image"],
contextWindow: 2_000_000,
maxTokens: 30_000,
});
});
it("resolves Grok Build and its official code aliases", () => {
const expected = {
id: "grok-build-0.1",
reasoning: true,
input: ["text", "image"],
contextWindow: 256_000,
cost: { input: 1, output: 2, cacheRead: 0.2, cacheWrite: 0 },
};
expectCatalogEntry("grok-build-0.1", expected);
expectCatalogEntry("grok-code-fast-1", expected);
expectCatalogEntry("grok-code-fast", expected);
expectCatalogEntry("grok-code-fast-1-0825", expected);
});
it("publishes Grok 4.20 reasoning and non-reasoning models", () => {
expectCatalogEntry("grok-4.20-beta-latest-reasoning", {
id: "grok-4.20-beta-latest-reasoning",
reasoning: true,
input: ["text", "image"],
contextWindow: 2_000_000,
});
expectCatalogEntry("grok-4.20-beta-latest-non-reasoning", {
id: "grok-4.20-beta-latest-non-reasoning",
reasoning: false,
contextWindow: 2_000_000,
});
});
it("keeps older Grok aliases resolving with current limits", () => {
expectCatalogEntry("grok-4-1-fast-reasoning", {
id: "grok-4-1-fast-reasoning",
reasoning: true,
contextWindow: 2_000_000,
maxTokens: 30_000,
});
expectCatalogEntry("grok-4.20-reasoning", {
id: "grok-4.20-reasoning",
reasoning: true,
contextWindow: 2_000_000,
maxTokens: 30_000,
});
});
it("publishes the remaining Grok 3 family in the OpenClaw catalog", () => {
expectCatalogEntry("grok-3-mini-fast", {
id: "grok-3-mini-fast",
reasoning: true,
contextWindow: 131_072,
maxTokens: 8_192,
});
expectCatalogEntry("grok-3-fast", {
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.3")).toBe(true);
expect(isModernXaiModel("grok-build-0.1")).toBe(true);
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(false);
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-responses",
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-responses",
baseUrl: "https://api.x.ai/v1",
},
},
});
const grok43Alias = resolveXaiForwardCompatModel({
providerId: "xai",
ctx: {
provider: "xai",
modelId: "grok-4.3-latest",
modelRegistry: { find: () => null } as never,
providerConfig: {
api: "openai-responses",
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-responses",
baseUrl: "https://api.x.ai/v1",
},
},
});
expect(grok41?.provider).toBe("xai");
expect(grok41?.id).toBe("grok-4-1-fast");
expect(grok41?.api).toBe("openai-responses");
expect(grok41?.baseUrl).toBe("https://api.x.ai/v1");
expect(grok41?.reasoning).toBe(true);
expect(grok41?.contextWindow).toBe(2_000_000);
expect(grok41?.maxTokens).toBe(30_000);
expect(grok43Alias?.provider).toBe("xai");
expect(grok43Alias?.id).toBe("grok-4.3-latest");
expect(grok43Alias?.api).toBe("openai-responses");
expect(grok43Alias?.baseUrl).toBe("https://api.x.ai/v1");
expect(grok43Alias?.reasoning).toBe(true);
expect(grok43Alias?.thinkingLevelMap).toEqual({
off: null,
minimal: "low",
low: "low",
medium: "medium",
high: "high",
xhigh: "high",
});
expect(grok43Alias?.input).toEqual(["text", "image"]);
expect(grok43Alias?.contextWindow).toBe(1_000_000);
expect(grok43Alias?.maxTokens).toBe(64_000);
expect(grok420?.provider).toBe("xai");
expect(grok420?.id).toBe("grok-4.20-beta-latest-reasoning");
expect(grok420?.api).toBe("openai-responses");
expect(grok420?.baseUrl).toBe("https://api.x.ai/v1");
expect(grok420?.reasoning).toBe(true);
expect(grok420?.input).toEqual(["text", "image"]);
expect(grok420?.contextWindow).toBe(2_000_000);
expect(grok420?.maxTokens).toBe(30_000);
expect(grok3Mini?.provider).toBe("xai");
expect(grok3Mini?.id).toBe("grok-3-mini-fast");
expect(grok3Mini?.api).toBe("openai-responses");
expect(grok3Mini?.baseUrl).toBe("https://api.x.ai/v1");
expect(grok3Mini?.reasoning).toBe(true);
expect(grok3Mini?.contextWindow).toBe(131_072);
expect(grok3Mini?.maxTokens).toBe(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-responses",
baseUrl: "https://api.x.ai/v1",
},
},
});
expect(model).toBeUndefined();
});
});