test: add codex-mcp thread/item coverage

This commit is contained in:
Mohamed Boudra
2025-12-24 18:25:12 +07:00
parent da2155e3be
commit f65a134ac0
2 changed files with 143 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import { randomUUID } from "node:crypto";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -36,6 +36,35 @@ function useTempCodexSessionDir(): () => void {
};
}
function writeTestMcpServerScript(cwd: string): string {
const scriptPath = path.join(cwd, "mcp-stdio-server.mjs");
const script = [
"import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';",
"import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';",
"import { z } from 'zod';",
"",
"const server = new McpServer({ name: 'test', version: '0.0.1' });",
"server.registerTool(",
" 'echo',",
" {",
" title: 'Echo tool',",
" description: 'Returns the input text',",
" inputSchema: { text: z.string() },",
" outputSchema: { text: z.string() }",
" },",
" async ({ text }) => ({",
" content: [],",
" structuredContent: { text }",
" })",
");",
"const transport = new StdioServerTransport();",
"await server.connect(transport);",
"",
].join("\n");
writeFileSync(scriptPath, script, "utf8");
return scriptPath;
}
async function loadCodexMcpAgentClient(): Promise<{
new (): AgentClient;
}> {
@@ -223,6 +252,117 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
180_000
);
test(
"maps thread/item events for file changes, MCP tools, web search, and todo lists",
async () => {
const cwd = tmpCwd();
const restoreSessionDir = useTempCodexSessionDir();
const mcpServerScript = writeTestMcpServerScript(cwd);
const { CodexMcpAgentClient } = await loadCodexMcpAgentClient();
const client = new CodexMcpAgentClient();
const config = {
provider: "codex-mcp",
cwd,
modeId: "full-access",
extra: {
codex: {
mcp_servers: {
test: {
command: process.execPath,
args: [mcpServerScript],
},
},
},
},
} as AgentSessionConfig;
let session: AgentSession | null = null;
const timelineItems: AgentTimelineItem[] = [];
const rawItemTypes = new Set<string>();
let sawThreadEvent = false;
let sawItemEvent = false;
try {
session = await client.createSession(config);
const prompt = [
"Use the web_search tool to search for \"OpenAI\".",
"Use the todo_list tool to create a list with exactly two items: alpha, beta.",
"Call the MCP tool test.echo with input {\"text\":\"hello\"}.",
"Use apply_patch to create a file named mcp-thread.log containing the single line 'ok'.",
"After all tools finish, reply DONE and stop.",
].join("\n");
for await (const event of session.stream(prompt)) {
if (event.type === "provider_event" && providerFromEvent(event) === "codex-mcp") {
const raw = event.raw as { type?: string; item?: { type?: string } } | undefined;
if (raw?.type && typeof raw.type === "string") {
if (raw.type.startsWith("thread.") || raw.type.startsWith("turn.")) {
sawThreadEvent = true;
}
if (raw.type.startsWith("item.")) {
sawItemEvent = true;
if (raw.item?.type) {
rawItemTypes.add(raw.item.type);
}
}
}
}
if (event.type === "timeline" && providerFromEvent(event) === "codex-mcp") {
timelineItems.push(event.item);
}
if (event.type === "turn_completed" || event.type === "turn_failed") {
break;
}
}
expect(sawThreadEvent).toBe(true);
expect(sawItemEvent).toBe(true);
expect(rawItemTypes.has("file_change")).toBe(true);
expect(rawItemTypes.has("mcp_tool_call")).toBe(true);
expect(rawItemTypes.has("web_search")).toBe(true);
expect(rawItemTypes.has("todo_list")).toBe(true);
expect(
timelineItems.some(
(item) => item.type === "tool_call" && item.server === "file_change"
)
).toBe(true);
expect(
timelineItems.some(
(item) =>
item.type === "tool_call" &&
item.server === "test" &&
item.tool === "echo"
)
).toBe(true);
expect(
timelineItems.some(
(item) =>
item.type === "tool_call" &&
item.server === "web_search" &&
item.tool === "web_search"
)
).toBe(true);
expect(
timelineItems.some(
(item) =>
item.type === "todo" &&
Array.isArray(item.items) &&
item.items.length >= 2
)
).toBe(true);
} finally {
await session?.close();
rmSync(cwd, { recursive: true, force: true });
restoreSessionDir();
}
},
180_000
);
test(
"emits an error timeline item for failed MCP turns",
async () => {

View File

@@ -31,7 +31,8 @@ Build a new Codex MCP provider sidebyside with the existing Codex SDK prov
- **Done (2025-12-24 18:18)**: Audited Codex MCP E2E coverage and env requirements; identified missing thread/item event mapping coverage, permission abort path coverage, and codex CLI env docs.
- [ ] **Test (E2E)**: Add coverage for thread/item events and non-command tool-call mapping (file_change, mcp_tool_call, web_search, todo_list).
- [x] **Test (E2E)**: Add coverage for thread/item events and non-command tool-call mapping (file_change, mcp_tool_call, web_search, todo_list).
- **Done (2025-12-24 18:24)**: Added Codex MCP E2E coverage for thread/item mapping and non-command tool calls. Tests failed locally due to Codex CLI/model/permission behavior mismatches (see test output).
- [ ] **Test (E2E)**: Add coverage for permission abort path (respondToPermission interrupt) and clarify read-only/untrusted gating behavior for Codex MCP.