Files
openclaw/src/flows/search-setup.test.ts
2026-05-03 00:48:37 -07:00

377 lines
11 KiB
TypeScript

import { beforeEach, 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",
credentialNote: "Configure Grok web search prerequisites before entering the credential.",
getCredentialValue: (search?: Record<string, unknown>) => search?.apiKey,
setCredentialValue: (searchConfigTarget: Record<string, unknown>, value: unknown) => {
searchConfigTarget.apiKey = value;
},
getConfiguredCredentialValue: (config?: Record<string, unknown>) =>
(
config?.plugins as
| {
entries?: Record<
string,
{
config?: {
webSearch?: { apiKey?: unknown };
};
}
>;
}
| undefined
)?.entries?.xai?.config?.webSearch?.apiKey,
setConfiguredCredentialValue: (configTarget: Record<string, unknown>, value: unknown) => {
const plugins = (configTarget.plugins ??= {}) as Record<string, unknown>;
const entries = (plugins.entries ??= {}) as Record<string, unknown>;
const xaiEntry = (entries.xai ??= {}) as Record<string, unknown>;
const xaiConfig = (xaiEntry.config ??= {}) as Record<string, unknown>;
const webSearch = (xaiConfig.webSearch ??= {}) as Record<string, unknown>;
webSearch.apiKey = value;
},
runSetup: async ({
config,
prompter,
}: {
config: Record<string, unknown>;
prompter: { select: (params: Record<string, unknown>) => Promise<string> };
}) => {
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<string, unknown> } | undefined)
?.entries;
const existingXaiEntry = pluginEntries?.xai as Record<string, unknown> | undefined;
const existingXaiConfig = (
pluginEntries?.xai as { config?: Record<string, unknown> } | undefined
)?.config;
return {
...config,
plugins: {
...(config.plugins as Record<string, unknown> | undefined),
entries: {
...pluginEntries,
xai: {
...existingXaiEntry,
config: {
...existingXaiConfig,
xSearch: {
enabled: true,
model,
},
},
},
},
},
};
},
}));
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
resolvePluginWebSearchProviders: () => [mockGrokProvider],
}));
const ensureOnboardingPluginInstalled = vi.hoisted(() =>
vi.fn(
async ({
cfg,
entry,
}: {
cfg: { plugins?: { installs?: Record<string, unknown> } };
entry: { pluginId: string; install: { npmSpec?: string } };
}) => ({
cfg: {
...cfg,
plugins: {
...cfg.plugins,
installs: {
...cfg.plugins?.installs,
[entry.pluginId]: {
source: "npm",
spec: entry.install.npmSpec,
installPath: `/tmp/openclaw-plugins/${entry.pluginId}`,
},
},
},
},
installed: true,
pluginId: entry.pluginId,
status: "installed",
}),
),
);
vi.mock("../commands/onboarding-plugin-install.js", () => ({
ensureOnboardingPluginInstalled,
}));
describe("runSearchSetupFlow", () => {
beforeEach(() => {
ensureOnboardingPluginInstalled.mockClear();
});
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("shows provider credential notes before plaintext credential prompts", async () => {
const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no");
const text = vi.fn().mockResolvedValue("xai-test-key");
const note = vi.fn(async () => {});
const prompter = createWizardPrompter({
note: note as never,
select: select as never,
text: text as never,
});
await runSearchSetupFlow({ plugins: { allow: ["xai"] } }, createNonExitingRuntime(), prompter);
expect(note).toHaveBeenCalledWith(mockGrokProvider.credentialNote, mockGrokProvider.label);
expect(text).toHaveBeenCalledTimes(1);
expect(note.mock.invocationCallOrder[1]).toBeLessThan(text.mock.invocationCallOrder[0]);
});
it("shows provider credential notes before SecretRef setup notes", async () => {
const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no");
const note = vi.fn(async () => {});
const prompter = createWizardPrompter({
note: note as never,
select: select as never,
});
await runSearchSetupFlow({ plugins: { allow: ["xai"] } }, createNonExitingRuntime(), prompter, {
secretInputMode: "ref",
});
expect(note).toHaveBeenNthCalledWith(
2,
mockGrokProvider.credentialNote,
mockGrokProvider.label,
);
expect(note).toHaveBeenNthCalledWith(
3,
expect.stringContaining("Secret references enabled"),
"Web search",
);
});
it("skips provider credential notes in quickstart fast path", async () => {
const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no");
const note = vi.fn(async () => {});
const prompter = createWizardPrompter({
note: note as never,
select: select as never,
});
await runSearchSetupFlow(
{
plugins: {
allow: ["xai"],
entries: {
xai: {
enabled: true,
config: {
webSearch: {
apiKey: "xai-test-key",
},
},
},
},
},
},
createNonExitingRuntime(),
prompter,
{ quickstartDefaults: true },
);
expect(note).not.toHaveBeenCalledWith(mockGrokProvider.credentialNote, mockGrokProvider.label);
});
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",
});
});
it("installs an external catalog search provider before enabling it", async () => {
const select = vi.fn().mockResolvedValueOnce("brave");
const text = vi.fn().mockResolvedValue("brave-test-key");
const prompter = createWizardPrompter({
select: select as never,
text: text as never,
});
const next = await runSearchSetupFlow({}, createNonExitingRuntime(), prompter);
expect(ensureOnboardingPluginInstalled).toHaveBeenCalledWith(
expect.objectContaining({
entry: expect.objectContaining({
pluginId: "brave",
label: "Brave",
trustedSourceLinkedOfficialInstall: true,
install: expect.objectContaining({
npmSpec: "@openclaw/brave-plugin",
}),
}),
autoConfirmSingleSource: true,
}),
);
expect(next.tools?.web?.search).toMatchObject({
provider: "brave",
enabled: true,
});
expect(next.plugins?.entries?.brave?.config?.webSearch).toMatchObject({
apiKey: "brave-test-key",
});
expect(next.plugins?.installs?.brave).toMatchObject({
source: "npm",
spec: "@openclaw/brave-plugin",
});
});
it("installs an external catalog search provider when web search stays disabled", async () => {
const select = vi.fn().mockResolvedValueOnce("brave");
const text = vi.fn().mockResolvedValue("brave-disabled-key");
const prompter = createWizardPrompter({
select: select as never,
text: text as never,
});
const next = await runSearchSetupFlow(
{
tools: {
web: {
search: {
provider: "brave",
enabled: false,
},
},
},
},
createNonExitingRuntime(),
prompter,
);
expect(ensureOnboardingPluginInstalled).toHaveBeenCalledWith(
expect.objectContaining({
entry: expect.objectContaining({
pluginId: "brave",
label: "Brave",
trustedSourceLinkedOfficialInstall: true,
install: expect.objectContaining({
npmSpec: "@openclaw/brave-plugin",
}),
}),
autoConfirmSingleSource: true,
}),
);
expect(next.tools?.web?.search).toMatchObject({
provider: "brave",
enabled: false,
});
expect(next.plugins?.entries?.brave?.config?.webSearch).toMatchObject({
apiKey: "brave-disabled-key",
});
expect(next.plugins?.entries?.brave?.enabled).toBeUndefined();
expect(next.plugins?.installs?.brave).toMatchObject({
source: "npm",
spec: "@openclaw/brave-plugin",
});
});
});