Files
openclaw/extensions/codex/src/web-search-provider.test.ts
Jason (Json) 1e0062b44a feat: add Codex hosted web search (#93446)
Adds Codex as a selectable hosted web-search provider, routes native Codex search safely across model overrides, and isolates bounded hosted-search workers from configured tools.\n\nVerification: focused post-merge regression suite passed 202/202 tests on exact head 23824af49a.
2026-06-16 00:38:16 -06:00

385 lines
12 KiB
TypeScript

import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it, vi } from "vitest";
import { createCodexWebSearchProvider as createContractCodexWebSearchProvider } from "../web-search-contract-api.js";
import type { CodexAppServerClient } from "./app-server/client.js";
import type { CodexAppServerStartOptions } from "./app-server/config.js";
import type { CodexServerNotification, JsonValue } from "./app-server/protocol.js";
import { createCodexWebSearchProvider } from "./web-search-provider.js";
function codexModel(
options: {
id?: string;
model?: string;
inputModalities?: string[];
isDefault?: boolean;
} = {},
) {
const id = options.id ?? "gpt-5.5";
return {
id,
model: options.model ?? id,
upgrade: null,
upgradeInfo: null,
availabilityNux: null,
displayName: "gpt-5.5",
description: "GPT-5.5",
hidden: false,
supportedReasoningEfforts: [{ reasoningEffort: "low", description: "fast" }],
defaultReasoningEffort: "low",
inputModalities: options.inputModalities ?? ["text", "image"],
supportsPersonality: false,
additionalSpeedTiers: [],
isDefault: options.isDefault ?? true,
};
}
function threadStartResult() {
return {
thread: {
id: "thread-1",
sessionId: "session-1",
forkedFromId: null,
preview: "",
ephemeral: true,
modelProvider: "openai",
createdAt: 1,
updatedAt: 1,
status: { type: "idle" },
path: null,
cwd: "/tmp/openclaw-agent",
cliVersion: "0.125.0",
source: "unknown",
agentNickname: null,
agentRole: null,
gitInfo: null,
name: null,
turns: [],
},
model: "gpt-5.5",
modelProvider: "openai",
serviceTier: null,
cwd: "/tmp/openclaw-agent",
instructionSources: [],
approvalPolicy: "on-request",
approvalsReviewer: "user",
sandbox: { type: "dangerFullAccess" },
permissionProfile: null,
reasoningEffort: null,
};
}
function turnStartResult(status = "inProgress") {
return {
turn: {
id: "turn-1",
status,
items: [],
error: null,
startedAt: null,
completedAt: null,
durationMs: null,
},
};
}
function createFakeClient(options?: {
emitWebSearch?: boolean;
models?: ReturnType<typeof codexModel>[];
}) {
const notifications = new Set<(notification: CodexServerNotification) => void>();
const requests: Array<{ method: string; params?: JsonValue }> = [];
const request = vi.fn(async (method: string, params?: JsonValue) => {
requests.push({ method, params });
if (method === "model/list") {
return { data: options?.models ?? [codexModel()], nextCursor: null };
}
if (method === "thread/start") {
return threadStartResult();
}
if (method === "turn/start") {
for (const notify of notifications) {
if (options?.emitWebSearch !== false) {
notify({
method: "item/completed",
params: {
threadId: "thread-1",
turnId: "turn-1",
item: {
id: "search-1",
type: "webSearch",
query: "plumbers in Edmonton Alberta",
action: {
type: "search",
query: "plumbers in Edmonton Alberta",
queries: ["plumbers in Edmonton Alberta"],
},
},
},
});
}
notify({
method: "item/agentMessage/delta",
params: {
threadId: "thread-1",
turnId: "turn-1",
itemId: "msg-1",
delta: "Two current providers: Example One and Example Two.",
},
});
notify({
method: "turn/completed",
params: {
threadId: "thread-1",
turnId: "turn-1",
turn: turnStartResult("completed").turn,
},
});
}
return turnStartResult();
}
return {};
});
const client = {
request,
addNotificationHandler(handler: (notification: CodexServerNotification) => void) {
notifications.add(handler);
return () => notifications.delete(handler);
},
addRequestHandler() {
return () => {};
},
} as unknown as CodexAppServerClient;
return { client, requests };
}
function createConfig(): OpenClawConfig {
return {
tools: {
web: {
search: {
provider: "codex",
timeoutSeconds: 30,
openaiCodex: {
enabled: true,
mode: "live",
allowedDomains: ["example.com"],
contextSize: "high",
userLocation: {
country: "CA",
region: "Alberta",
city: "Edmonton",
timezone: "America/Edmonton",
},
},
},
},
},
};
}
describe("codex web search provider", () => {
it("registers a selectable keyless provider contract", () => {
const provider = createContractCodexWebSearchProvider();
expect(provider.id).toBe("codex");
expect(provider.label).toBe("Codex Hosted Search");
expect(provider.requiresCredential).toBe(false);
expect(provider.envVars).toEqual([]);
expect(provider.autoDetectOrder).toBe(900);
expect(provider.applySelectionConfig?.({}).plugins?.entries?.codex?.enabled).toBe(true);
});
it("honors the explicit Codex hosted-search opt-out", () => {
const provider = createCodexWebSearchProvider();
expect(
provider.createTool({
searchConfig: { provider: "codex", openaiCodex: { enabled: false } },
}),
).toBeNull();
});
it("fails closed when configured app-server transport cannot be isolated", async () => {
const { client } = createFakeClient();
const provider = createCodexWebSearchProvider({
resolvePluginConfig: () => ({
appServer: {
transport: "websocket",
url: "ws://127.0.0.1:4501",
},
}),
clientFactory: async () => client,
});
const config = createConfig();
const tool = provider.createTool({
config,
searchConfig: config.tools?.web?.search,
agentDir: "/tmp/openclaw-agent",
});
await expect(tool?.execute({ query: "plumbers in Edmonton Alberta" })).rejects.toThrow(
"Bounded Codex turns require stdio transport so native tools can be isolated.",
);
});
it("runs an isolated grounded Codex search with configured restrictions", async () => {
const { client, requests } = createFakeClient();
let isolatedStartOptions: CodexAppServerStartOptions | undefined;
const provider = createCodexWebSearchProvider({
resolvePluginConfig: () => ({
appServer: {
args: [
"app-server",
"--listen",
"stdio://",
"-c",
"mcp_servers.external.command='unsafe'",
],
clearEnv: ["CODEX_HOME", "KEEP_CLEARED"],
},
}),
clientFactory: async (startOptions) => {
isolatedStartOptions = startOptions;
return client;
},
});
const config = createConfig();
const tool = provider.createTool({
config,
searchConfig: config.tools?.web?.search,
agentDir: "/tmp/openclaw-agent",
});
const result = await tool?.execute({ query: "plumbers in Edmonton Alberta" });
expect(result).toMatchObject({
query: "plumbers in Edmonton Alberta",
provider: "codex",
model: "gpt-5.5",
externalContent: {
untrusted: true,
source: "web_search",
provider: "codex",
wrapped: true,
},
searches: [
{
query: "plumbers in Edmonton Alberta",
queries: ["plumbers in Edmonton Alberta"],
},
],
});
expect(result?.content).toContain("Two current providers");
expect(requests.map((entry) => entry.method)).toEqual([
"model/list",
"thread/start",
"turn/start",
]);
expect(requests[1]?.params).toMatchObject({
model: "gpt-5.5",
modelProvider: "openai",
cwd: expect.any(String),
approvalPolicy: "on-request",
sandbox: "read-only",
environments: [],
dynamicTools: [],
ephemeral: true,
config: {
"features.code_mode": false,
"features.code_mode_only": false,
"features.hooks": false,
"features.standalone_web_search": false,
notify: [],
web_search: "live",
"tools.web_search.allowed_domains": ["example.com"],
"tools.web_search.context_size": "high",
"tools.web_search.location.country": "CA",
"tools.web_search.location.region": "Alberta",
"tools.web_search.location.city": "Edmonton",
"tools.web_search.location.timezone": "America/Edmonton",
},
});
const threadStartCwd = (requests[1]?.params as { cwd?: string } | undefined)?.cwd;
const isolatedCodexHome = isolatedStartOptions?.env?.CODEX_HOME;
expect(threadStartCwd).not.toBe("/tmp/openclaw-agent");
expect(isolatedStartOptions?.args).toEqual(["app-server", "--listen", "stdio://"]);
expect(isolatedStartOptions?.clearEnv).toEqual([
"KEEP_CLEARED",
"OPENCLAW_CODEX_APP_SERVER_ARGS",
]);
expect(isolatedCodexHome).toEqual(expect.any(String));
if (!threadStartCwd || !isolatedCodexHome) {
throw new Error("expected isolated Codex home and workspace");
}
expect(path.dirname(threadStartCwd)).toBe(path.dirname(isolatedCodexHome));
});
it("selects the live default text-capable model", async () => {
const { client, requests } = createFakeClient({
models: [
codexModel({ id: "available-first", isDefault: false }),
codexModel({ id: "available-default", model: "available-default-wire" }),
],
});
const provider = createCodexWebSearchProvider({
clientFactory: async () => client,
});
const config = createConfig();
const tool = provider.createTool({
config,
searchConfig: config.tools?.web?.search,
agentDir: "/tmp/openclaw-agent",
});
const result = await tool?.execute({ query: "plumbers in Edmonton Alberta" });
expect(result?.model).toBe("available-default-wire");
expect(requests[1]?.params).toEqual(
expect.objectContaining({ model: "available-default-wire" }),
);
expect(requests[2]?.params).toEqual(
expect.objectContaining({ model: "available-default-wire" }),
);
});
it("fails closed when the live catalog has no text-capable model", async () => {
const { client, requests } = createFakeClient({
models: [codexModel({ id: "image-only", inputModalities: ["image"] })],
});
const provider = createCodexWebSearchProvider({
clientFactory: async () => client,
});
const config = createConfig();
const tool = provider.createTool({
config,
searchConfig: config.tools?.web?.search,
agentDir: "/tmp/openclaw-agent",
});
await expect(tool?.execute({ query: "plumbers in Edmonton Alberta" })).rejects.toThrow(
"Codex app-server has no model supporting text input.",
);
expect(requests.map((entry) => entry.method)).toEqual(["model/list"]);
});
it("fails closed when Codex returns an ungrounded answer", async () => {
const { client } = createFakeClient({ emitWebSearch: false });
const provider = createCodexWebSearchProvider({
clientFactory: async () => client,
});
const config = createConfig();
const tool = provider.createTool({
config,
searchConfig: config.tools?.web?.search,
agentDir: "/tmp/openclaw-agent",
});
await expect(tool?.execute({ query: "plumbers in Edmonton Alberta" })).rejects.toThrow(
"Codex hosted search completed without invoking web search.",
);
});
});