// Xai tests cover x search plugin behavior. import { withFetchPreconnect } from "openclaw/plugin-sdk/test-env"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createXSearchTool } from "./x-search.js"; function jsonResponse(payload: unknown, init: ResponseInit = {}): Response { return new Response(JSON.stringify(payload), { status: 200, headers: { "Content-Type": "application/json" }, ...init, }); } function installXSearchFetch(payload?: Record) { const mockFetch = vi.fn((_input?: unknown, _init?: unknown) => Promise.resolve( jsonResponse( payload ?? { output: [ { type: "message", content: [ { type: "output_text", text: "Found X posts", annotations: [{ type: "url_citation", url: "https://x.com/openclaw/status/1" }], }, ], }, ], citations: ["https://x.com/openclaw/status/1"], }, ), ), ); global.fetch = withFetchPreconnect(mockFetch); return mockFetch; } function firstFetchCall(mockFetch: ReturnType) { const [call] = mockFetch.mock.calls; if (!call) { throw new Error("expected x_search fetch call"); } return call; } function firstFetchUrl(mockFetch: ReturnType) { const [url] = firstFetchCall(mockFetch); return String(url); } function firstFetchInit(mockFetch: ReturnType): RequestInit { const [, init] = firstFetchCall(mockFetch); if (!init || typeof init !== "object" || Array.isArray(init)) { throw new Error("expected x_search fetch init"); } return init as RequestInit; } function firstAuthorizationHeader(mockFetch: ReturnType) { const headers = firstFetchInit(mockFetch).headers; if (!headers || typeof headers !== "object" || Array.isArray(headers)) { throw new Error("expected x_search request headers"); } return (headers as Record).Authorization; } function parseFirstRequestBody(mockFetch: ReturnType) { const requestBody = firstFetchInit(mockFetch).body; return JSON.parse(typeof requestBody === "string" ? requestBody : "{}") as Record< string, unknown >; } afterEach(() => { vi.restoreAllMocks(); }); describe("xai x_search tool", () => { it("describes query as the required instruction for the Grok X-search agent", () => { const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-plugin-key", // pragma: allowlist secret }, }, }, }, }, }, }); const parameters = tool?.parameters as | { properties?: { query?: { description?: string } } } | undefined; const queryDescription = parameters?.properties?.query?.description; expect(queryDescription).toContain("Natural-language instruction"); expect(queryDescription).toContain("Grok X-search agent"); expect(queryDescription).toContain("meaningful and non-empty"); expect(queryDescription).not.toContain("allowed_x_handles"); }); it("enables x_search when runtime config carries the shared xAI key", () => { const tool = createXSearchTool({ config: {}, runtimeConfig: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "x-search-runtime-key", // pragma: allowlist secret }, }, }, }, }, }, }); expect(tool?.name).toBe("x_search"); }); it("enables x_search from an xAI auth profile and uses it for requests", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: {}, auth: { hasAuthForProvider: (providerId) => providerId === "xai", resolveApiKeyForProvider: async (providerId) => providerId === "xai" ? "xai-profile-key" : undefined, // pragma: allowlist secret }, }); expect(tool?.name).toBe("x_search"); await tool?.execute?.("x-search:auth-profile", { query: "auth profile search", }); expect(firstAuthorizationHeader(mockFetch)).toBe("Bearer xai-profile-key"); }); it("enables x_search when the xAI plugin web search key is configured", () => { const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-plugin-key", // pragma: allowlist secret }, }, }, }, }, }, }); expect(tool?.name).toBe("x_search"); }); it("uses the xAI Responses x_search tool with structured filters", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-config-test", // pragma: allowlist secret }, xSearch: { model: "grok-4-1-fast-non-reasoning", maxTurns: 2, }, }, }, }, }, }, }); const result = await tool?.execute?.("x-search:1", { query: "dinner recipes", allowed_x_handles: ["openclaw"], excluded_x_handles: ["spam"], from_date: "2026-03-01", to_date: "2026-03-20", enable_image_understanding: true, }); expect(mockFetch).toHaveBeenCalled(); expect(firstFetchUrl(mockFetch)).toContain("api.x.ai/v1/responses"); const body = parseFirstRequestBody(mockFetch); expect(body.model).toBe("grok-4-1-fast-non-reasoning"); expect(body.max_turns).toBe(2); expect(body.tools).toEqual([ { type: "x_search", allowed_x_handles: ["openclaw"], excluded_x_handles: ["spam"], from_date: "2026-03-01", to_date: "2026-03-20", enable_image_understanding: true, }, ]); expect((result?.details as { citations?: string[] } | undefined)?.citations).toEqual([ "https://x.com/openclaw/status/1", ]); }); it("routes x_search through plugin-owned xSearch.baseUrl", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-config-test", // pragma: allowlist secret }, xSearch: { enabled: true, baseUrl: "https://api.x.ai/xai-search/v1/", }, }, }, }, }, }, }); await tool?.execute?.("x-search:plugin-base-url", { query: "base url route", }); expect(firstFetchUrl(mockFetch)).toBe("https://api.x.ai/xai-search/v1/responses"); }); it("falls back to Grok web search baseUrl for x_search", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { tools: { web: { search: { grok: { apiKey: "xai-legacy-key", // pragma: allowlist secret baseUrl: "https://api.x.ai/legacy/v1/", }, }, }, }, }, }); await tool?.execute?.("x-search:legacy-grok-base-url", { query: "legacy base url route", }); expect(firstFetchUrl(mockFetch)).toBe("https://api.x.ai/legacy/v1/responses"); }); it("shares plugin webSearch.baseUrl with x_search when xSearch.baseUrl is unset", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-plugin-key", // pragma: allowlist secret baseUrl: "https://api.x.ai/shared/v1/", }, xSearch: { enabled: true, }, }, }, }, }, }, }); await tool?.execute?.("x-search:web-search-base-url", { query: "shared base url route", }); expect(firstFetchUrl(mockFetch)).toBe("https://api.x.ai/shared/v1/responses"); }); it("reuses the xAI plugin web search key for x_search requests", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-plugin-key", // pragma: allowlist secret }, }, }, }, }, }, }); await tool?.execute?.("x-search:plugin-key", { query: "latest post from huntharo", }); expect(firstAuthorizationHeader(mockFetch)).toBe("Bearer xai-plugin-key"); }); it("reports malformed x_search JSON as a provider error", async () => { const mockFetch = vi.fn((_input?: unknown, _init?: unknown) => Promise.resolve( new Response("{ nope", { status: 200, headers: { "Content-Type": "application/json" }, }), ), ); global.fetch = withFetchPreconnect(mockFetch); const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-plugin-key", // pragma: allowlist secret }, xSearch: { enabled: true, }, }, }, }, }, }, }); await expect( tool?.execute?.("x-search:malformed-json", { query: "malformed x_search response probe", }), ).rejects.toThrow("xAI X search failed: malformed JSON response"); }); it("rejects x_search success JSON without answer text", async () => { const mockFetch = vi.fn((_input?: unknown, _init?: unknown) => Promise.resolve(jsonResponse({ output: [] })), ); global.fetch = withFetchPreconnect(mockFetch); const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-plugin-key", // pragma: allowlist secret }, xSearch: { enabled: true, }, }, }, }, }, }, }); await expect( tool?.execute?.("x-search:missing-text", { query: "malformed x_search missing text probe", }), ).rejects.toThrow("xAI X search failed: malformed JSON response"); }); it("prefers the active runtime config for shared xAI keys", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" }, }, }, }, }, }, }, runtimeConfig: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "x-search-runtime-key", // pragma: allowlist secret }, }, }, }, }, }, }); await tool?.execute?.("x-search:runtime-key", { query: "runtime key search", }); expect(firstAuthorizationHeader(mockFetch)).toBe("Bearer x-search-runtime-key"); }); it("reuses the legacy grok web search key for x_search requests", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { tools: { web: { search: { grok: { apiKey: "xai-legacy-key", // pragma: allowlist secret }, }, }, }, }, }); await tool?.execute?.("x-search:legacy-key", { query: "latest legacy-key post from huntharo", }); expect(firstAuthorizationHeader(mockFetch)).toBe("Bearer xai-legacy-key"); }); it("uses migrated runtime auth when the source config still carries legacy x_search apiKey", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { tools: { web: { x_search: { apiKey: "legacy-x-search-key", // pragma: allowlist secret enabled: true, } as Record, }, }, }, runtimeConfig: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "migrated-runtime-key", // pragma: allowlist secret }, }, }, }, }, }, }); await tool?.execute?.("x-search:migrated-runtime-key", { query: "migrated runtime auth", }); expect(firstAuthorizationHeader(mockFetch)).toBe("Bearer migrated-runtime-key"); }); it("rejects invalid date ordering before calling xAI", async () => { const mockFetch = installXSearchFetch(); const tool = createXSearchTool({ config: { plugins: { entries: { xai: { config: { webSearch: { apiKey: "xai-config-test", // pragma: allowlist secret }, }, }, }, }, }, }); await expect( tool?.execute?.("x-search:bad-dates", { query: "dinner recipes", from_date: "2026-03-20", to_date: "2026-03-01", }), ).rejects.toThrow(/from_date must be on or before to_date/i); expect(mockFetch).not.toHaveBeenCalled(); }); });