import { describe, expect, it, vi } from "vitest"; import { createWizardPrompter } from "../../test/helpers/wizard-prompter.js"; import { createNonExitingRuntime } from "../runtime.js"; import { runSearchSetupFlow } from "./search-setup.js"; const mockGrokProvider = vi.hoisted(() => ({ id: "grok", pluginId: "xai", label: "Grok", hint: "Search with xAI", docsUrl: "https://docs.openclaw.ai/tools/web", requiresCredential: true, credentialLabel: "xAI API key", placeholder: "xai-...", signupUrl: "https://x.ai/api", envVars: ["XAI_API_KEY"], onboardingScopes: ["text-inference"], credentialPath: "plugins.entries.xai.config.webSearch.apiKey", getCredentialValue: (search?: Record) => search?.apiKey, setCredentialValue: (searchConfigTarget: Record, value: unknown) => { searchConfigTarget.apiKey = value; }, getConfiguredCredentialValue: (config?: Record) => ( config?.plugins as | { entries?: Record< string, { config?: { webSearch?: { apiKey?: unknown }; }; } >; } | undefined )?.entries?.xai?.config?.webSearch?.apiKey, setConfiguredCredentialValue: (configTarget: Record, value: unknown) => { const plugins = (configTarget.plugins ??= {}) as Record; const entries = (plugins.entries ??= {}) as Record; const xaiEntry = (entries.xai ??= {}) as Record; const xaiConfig = (xaiEntry.config ??= {}) as Record; const webSearch = (xaiConfig.webSearch ??= {}) as Record; webSearch.apiKey = value; }, runSetup: async ({ config, prompter, }: { config: Record; prompter: { select: (params: Record) => Promise }; }) => { const enableXSearch = await prompter.select({ message: "Enable x_search", options: [ { value: "yes", label: "Yes" }, { value: "no", label: "No" }, ], }); if (enableXSearch !== "yes") { return config; } const model = await prompter.select({ message: "Grok model", options: [{ value: "grok-4-1-fast", label: "grok-4-1-fast" }], }); const pluginEntries = (config.plugins as { entries?: Record } | undefined) ?.entries; const existingXaiEntry = pluginEntries?.xai as Record | undefined; const existingXaiConfig = ( pluginEntries?.xai as { config?: Record } | undefined )?.config; return { ...config, plugins: { ...(config.plugins as Record | undefined), entries: { ...pluginEntries, xai: { ...existingXaiEntry, config: { ...existingXaiConfig, xSearch: { enabled: true, model, }, }, }, }, }, }; }, })); vi.mock("../plugins/bundled-web-search.js", () => ({ listBundledWebSearchProviders: () => [mockGrokProvider], resolveBundledWebSearchPluginId: (providerId: string | undefined) => providerId === "grok" ? "xai" : undefined, })); vi.mock("../plugins/web-search-providers.runtime.js", () => ({ resolvePluginWebSearchProviders: () => [mockGrokProvider], })); describe("runSearchSetupFlow", () => { it("runs provider-owned setup after selecting Grok web search", async () => { const select = vi .fn() .mockResolvedValueOnce("grok") .mockResolvedValueOnce("yes") .mockResolvedValueOnce("grok-4-1-fast"); const text = vi.fn().mockResolvedValue("xai-test-key"); const prompter = createWizardPrompter({ select: select as never, text: text as never, }); const next = await runSearchSetupFlow( { plugins: { allow: ["xai"] } }, createNonExitingRuntime(), prompter, ); expect(next.plugins?.entries?.xai?.config?.webSearch).toMatchObject({ apiKey: "xai-test-key", }); expect(next.tools?.web?.search).toMatchObject({ provider: "grok", enabled: true, }); expect(next.plugins?.entries?.xai?.config?.xSearch).toMatchObject({ enabled: true, model: "grok-4-1-fast", }); }); it("preserves disabled web_search state while still allowing provider-owned x_search setup", async () => { const select = vi .fn() .mockResolvedValueOnce("grok") .mockResolvedValueOnce("yes") .mockResolvedValueOnce("grok-4-1-fast"); const prompter = createWizardPrompter({ select: select as never, }); const next = await runSearchSetupFlow( { plugins: { allow: ["xai"], entries: { xai: { enabled: true, config: { webSearch: { apiKey: "xai-test-key", }, }, }, }, }, tools: { web: { search: { provider: "grok", enabled: false, }, }, }, }, createNonExitingRuntime(), prompter, ); expect(next.tools?.web?.search).toMatchObject({ provider: "grok", enabled: false, }); expect(next.plugins?.entries?.xai?.config?.xSearch).toMatchObject({ enabled: true, model: "grok-4-1-fast", }); }); });