Files
openclaw/extensions/codex/src/app-server/schema-normalization-runtime-contract.test.ts
pashpashpash 34fb96622e Support MCP hooks in the Codex harness (#71707)
* codex harness mcp hook parity

* tighten codex hook parity floor

* prove security-style mcp hook blocking

* bound native hook relay key handling

* clarify permission relay defers to provider

* harden native hook relay approvals

* fix(agents): bound native hook relay JSON work budget

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-25 21:35:47 +01:00

169 lines
4.8 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
createParameterFreeTool,
createPermissiveTool,
normalizedParameterFreeSchema,
} from "../../../../test/helpers/agents/schema-normalization-runtime-contract.js";
import { createCodexTestModel } from "./test-support.js";
import { startOrResumeThread } from "./thread-lifecycle.js";
let tempDir: string;
function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAttemptParams {
return {
prompt: "hello",
sessionId: "session-1",
sessionKey: "agent:main:session-1",
sessionFile,
workspaceDir,
runId: "run-1",
provider: "codex",
modelId: "gpt-5.4",
model: createCodexTestModel("codex"),
thinkLevel: "medium",
disableTools: true,
timeoutMs: 5_000,
authStorage: {} as never,
modelRegistry: {} as never,
} as EmbeddedRunAttemptParams;
}
function createAppServerOptions(): Parameters<typeof startOrResumeThread>[0]["appServer"] {
return {
start: {
transport: "stdio",
command: "codex",
args: ["app-server"],
headers: {},
},
requestTimeoutMs: 60_000,
approvalPolicy: "never",
approvalsReviewer: "user",
sandbox: "workspace-write",
};
}
function threadStartResult(threadId = "thread-1") {
return {
thread: {
id: threadId,
forkedFromId: null,
preview: "",
ephemeral: false,
modelProvider: "openai",
createdAt: 1,
updatedAt: 1,
status: { type: "idle" },
path: null,
cwd: tempDir,
cliVersion: "0.125.0",
source: "unknown",
agentNickname: null,
agentRole: null,
gitInfo: null,
name: null,
turns: [],
},
model: "gpt-5.4",
modelProvider: "openai",
serviceTier: null,
cwd: tempDir,
instructionSources: [],
approvalPolicy: "never",
approvalsReviewer: "user",
sandbox: { type: "dangerFullAccess" },
permissionProfile: null,
reasoningEffort: null,
};
}
describe("Codex app-server dynamic tool schema boundary contract", () => {
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-schema-contract-"));
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
it("passes prepared executable dynamic tool schemas through thread start unchanged", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const parameterFreeTool = createParameterFreeTool("message");
const dynamicTool = {
name: parameterFreeTool.name,
description: parameterFreeTool.description,
inputSchema: normalizedParameterFreeSchema(),
};
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
return threadStartResult();
}
throw new Error(`unexpected method: ${method}`);
});
await startOrResumeThread({
client: { request } as never,
params: createParams(sessionFile, workspaceDir),
cwd: workspaceDir,
dynamicTools: [dynamicTool],
appServer: createAppServerOptions(),
});
expect(request).toHaveBeenCalledWith(
"thread/start",
expect.objectContaining({
dynamicTools: [dynamicTool],
}),
);
});
it("treats dynamic tool schema changes as thread-fingerprint changes", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const appServer = createAppServerOptions();
let nextThreadId = 1;
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
return threadStartResult(`thread-${nextThreadId++}`);
}
throw new Error(`unexpected method: ${method}`);
});
await startOrResumeThread({
client: { request } as never,
params: createParams(sessionFile, workspaceDir),
cwd: workspaceDir,
dynamicTools: [
{
name: "message",
description: "Permissive test tool",
inputSchema: { type: "object" },
},
],
appServer,
});
const permissiveTool = createPermissiveTool("message");
await startOrResumeThread({
client: { request } as never,
params: createParams(sessionFile, workspaceDir),
cwd: workspaceDir,
dynamicTools: [
{
name: permissiveTool.name,
description: permissiveTool.description,
inputSchema: permissiveTool.parameters,
},
],
appServer,
});
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/start"]);
});
});