test: add codex mcp tool call coverage
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# Codex MCP Tool Call Coverage E2E Failures
|
||||||
|
|
||||||
|
## Test Command
|
||||||
|
|
||||||
|
`npm run test --workspace=@paseo/server -- codex-mcp-agent.test.ts -t "captures tool call inputs/outputs"`
|
||||||
|
|
||||||
|
## Failing Assertions
|
||||||
|
|
||||||
|
Test: `CodexMcpAgentClient (MCP integration) > captures tool call inputs/outputs for commands, file changes, file reads, MCP tools, and web search`
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:532`
|
||||||
|
- `fileChangeCalls.some((item) => stringifyUnknown(item.output).includes("tool-create.txt"))`
|
||||||
|
- Output does not contain the file path; indicates `file_change` tool output is missing file metadata.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:535`
|
||||||
|
- `const readCall = toolCalls.find((item) => item.tool === "read_file")`
|
||||||
|
- No `read_file` tool call captured.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:536`
|
||||||
|
- `stringifyUnknown(readCall?.input)` did not include `tool-create.txt`.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:537`
|
||||||
|
- `stringifyUnknown(readCall?.output)` did not include `beta`.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:542`
|
||||||
|
- `const mcpCall = toolCalls.find((item) => item.server === "test" && item.tool === "echo")`
|
||||||
|
- No MCP tool call captured.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:543`
|
||||||
|
- `stringifyUnknown(mcpCall?.input)` did not include `mcp-ok`.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:544`
|
||||||
|
- `stringifyUnknown(mcpCall?.output)` did not include `mcp-ok`.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:549`
|
||||||
|
- `const webSearchCall = toolCalls.find((item) => item.server === "web_search" && item.tool === "web_search")`
|
||||||
|
- No web search tool call captured.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:550`
|
||||||
|
- `stringifyUnknown(webSearchCall?.input)` did not include `OpenAI Codex MCP`.
|
||||||
|
|
||||||
|
- `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:551`
|
||||||
|
- `webSearchCall?.output` was undefined; web search results not captured.
|
||||||
|
|
||||||
|
## Hypotheses
|
||||||
|
|
||||||
|
- The Codex MCP provider may not be emitting `tool_call` timeline items for `read_file`, MCP tool calls, and `web_search` in this scenario, or the model is not executing the requested tools despite explicit prompts.
|
||||||
|
- File change timeline output appears to omit file metadata (e.g., `{ files: [{ path, kind }] }`), so the apply_patch completion output is not surfaced in timeline items.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
@@ -150,6 +150,35 @@ function commandTextFromInput(input: unknown): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commandOutputText(output: unknown): string | null {
|
||||||
|
if (typeof output === "string") {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
if (!output || typeof output !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const record = output as Record<string, unknown>;
|
||||||
|
const outputText = record.output;
|
||||||
|
if (typeof outputText === "string") {
|
||||||
|
return outputText;
|
||||||
|
}
|
||||||
|
if (typeof record.stdout === "string") {
|
||||||
|
return record.stdout;
|
||||||
|
}
|
||||||
|
if (typeof record.stderr === "string") {
|
||||||
|
return record.stderr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyUnknown(value: unknown): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value) ?? "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isSleepCommandToolCall(item: ToolCallItem): boolean {
|
function isSleepCommandToolCall(item: ToolCallItem): boolean {
|
||||||
const display = typeof item.displayName === "string" ? item.displayName.toLowerCase() : "";
|
const display = typeof item.displayName === "string" ? item.displayName.toLowerCase() : "";
|
||||||
if (display.includes("sleep 60")) {
|
if (display.includes("sleep 60")) {
|
||||||
@@ -258,9 +287,13 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
expect((toolCall.callId ?? "").trim().length).toBeGreaterThan(0);
|
expect((toolCall.callId ?? "").trim().length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandToolCall = toolCalls.find(
|
const commandToolCall = toolCalls
|
||||||
(item) => item.server === "command"
|
.slice()
|
||||||
);
|
.reverse()
|
||||||
|
.find(
|
||||||
|
(item) =>
|
||||||
|
item.server === "command" && item.status !== "running"
|
||||||
|
);
|
||||||
expect(commandToolCall).toBeTruthy();
|
expect(commandToolCall).toBeTruthy();
|
||||||
|
|
||||||
const exitCode = extractExitCode(commandToolCall?.output);
|
const exitCode = extractExitCode(commandToolCall?.output);
|
||||||
@@ -393,6 +426,168 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
180_000
|
180_000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"captures tool call inputs/outputs for commands, file changes, file reads, MCP tools, and web search",
|
||||||
|
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: "auto",
|
||||||
|
approvalPolicy: "on-request",
|
||||||
|
networkAccess: true,
|
||||||
|
extra: {
|
||||||
|
codex: {
|
||||||
|
mcp_servers: {
|
||||||
|
test: {
|
||||||
|
command: process.execPath,
|
||||||
|
args: [mcpServerScript],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as AgentSessionConfig;
|
||||||
|
|
||||||
|
let session: AgentSession | null = null;
|
||||||
|
let permissionRequest: AgentPermissionRequest | null = null;
|
||||||
|
let permissionResolved = false;
|
||||||
|
const toolCalls: ToolCallItem[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
session = await client.createSession(config);
|
||||||
|
|
||||||
|
const prompt = [
|
||||||
|
"1. Run the command `bash -lc \"printf 'stdout-marker'\"` using your shell tool.",
|
||||||
|
"2. Run the command `bash -lc \"printf 'stderr-marker' 1>&2\"` using your shell tool.",
|
||||||
|
"3. Use apply_patch (not the shell) to create a new file named tool-create.txt containing only the line 'alpha'.",
|
||||||
|
"4. Use apply_patch (not the shell) to edit tool-create.txt, replacing 'alpha' with 'beta'.",
|
||||||
|
"5. Use the read_file tool (not the shell) to read tool-create.txt.",
|
||||||
|
"6. Call the MCP tool test.echo with input {\"text\":\"mcp-ok\"}.",
|
||||||
|
"7. Use the web_search tool to search for \"OpenAI Codex MCP\".",
|
||||||
|
"8. Request approval to run the command `printf \"permit\" > tool-permission.txt`, then run it.",
|
||||||
|
"9. After all tools finish, reply DONE and stop.",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
for await (const event of session.stream(prompt)) {
|
||||||
|
if (event.type === "permission_requested" && !permissionRequest) {
|
||||||
|
permissionRequest = event.request;
|
||||||
|
await session.respondToPermission(permissionRequest.id, { behavior: "allow" });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.type === "permission_resolved" &&
|
||||||
|
permissionRequest &&
|
||||||
|
event.requestId === permissionRequest.id &&
|
||||||
|
event.resolution.behavior === "allow"
|
||||||
|
) {
|
||||||
|
permissionResolved = true;
|
||||||
|
}
|
||||||
|
if (event.type === "timeline" && providerFromEvent(event) === "codex-mcp") {
|
||||||
|
if (event.item.type === "tool_call" && event.item.server !== "permission") {
|
||||||
|
toolCalls.push(event.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.type === "turn_completed" || event.type === "turn_failed") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.soft(permissionRequest).not.toBeNull();
|
||||||
|
expect.soft(permissionResolved).toBe(true);
|
||||||
|
|
||||||
|
const commandCalls = toolCalls.filter(
|
||||||
|
(item) => item.server === "command" && item.status === "completed"
|
||||||
|
);
|
||||||
|
expect.soft(commandCalls.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
const stdoutCall = commandCalls.find((item) =>
|
||||||
|
(commandOutputText(item.output) ?? "").includes("stdout-marker")
|
||||||
|
);
|
||||||
|
const stderrCall = commandCalls.find((item) =>
|
||||||
|
(commandOutputText(item.output) ?? "").includes("stderr-marker")
|
||||||
|
);
|
||||||
|
expect.soft(stdoutCall).toBeTruthy();
|
||||||
|
expect.soft(stderrCall).toBeTruthy();
|
||||||
|
expect.soft(extractExitCode(stdoutCall?.output)).toBe(0);
|
||||||
|
expect.soft(extractExitCode(stderrCall?.output)).toBe(0);
|
||||||
|
|
||||||
|
const fileChangeCalls = toolCalls.filter(
|
||||||
|
(item) => item.server === "file_change" && item.tool === "apply_patch"
|
||||||
|
);
|
||||||
|
expect.soft(fileChangeCalls.length).toBeGreaterThanOrEqual(2);
|
||||||
|
expect.soft(
|
||||||
|
fileChangeCalls.some((item) => stringifyUnknown(item.input).includes("tool-create.txt"))
|
||||||
|
).toBe(true);
|
||||||
|
expect.soft(
|
||||||
|
fileChangeCalls.some((item) => stringifyUnknown(item.input).includes("alpha"))
|
||||||
|
).toBe(true);
|
||||||
|
expect.soft(
|
||||||
|
fileChangeCalls.some((item) => stringifyUnknown(item.input).includes("beta"))
|
||||||
|
).toBe(true);
|
||||||
|
expect.soft(
|
||||||
|
fileChangeCalls.some((item) => stringifyUnknown(item.output).includes("tool-create.txt"))
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
const readCall = toolCalls.find((item) => item.tool === "read_file");
|
||||||
|
expect.soft(readCall).toBeTruthy();
|
||||||
|
expect.soft(stringifyUnknown(readCall?.input)).toContain("tool-create.txt");
|
||||||
|
expect.soft(stringifyUnknown(readCall?.output)).toContain("beta");
|
||||||
|
|
||||||
|
const mcpCall = toolCalls.find(
|
||||||
|
(item) => item.server === "test" && item.tool === "echo"
|
||||||
|
);
|
||||||
|
expect.soft(mcpCall).toBeTruthy();
|
||||||
|
expect.soft(stringifyUnknown(mcpCall?.input)).toContain("mcp-ok");
|
||||||
|
expect.soft(stringifyUnknown(mcpCall?.output)).toContain("mcp-ok");
|
||||||
|
|
||||||
|
const webSearchCall = toolCalls.find(
|
||||||
|
(item) => item.server === "web_search" && item.tool === "web_search"
|
||||||
|
);
|
||||||
|
expect.soft(webSearchCall).toBeTruthy();
|
||||||
|
expect.soft(stringifyUnknown(webSearchCall?.input)).toContain("OpenAI Codex MCP");
|
||||||
|
expect.soft(webSearchCall?.output).toBeTruthy();
|
||||||
|
|
||||||
|
const callIdStatuses = new Map<string, Set<string>>();
|
||||||
|
for (const toolCall of toolCalls) {
|
||||||
|
if (!toolCall.callId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!callIdStatuses.has(toolCall.callId)) {
|
||||||
|
callIdStatuses.set(toolCall.callId, new Set());
|
||||||
|
}
|
||||||
|
if (typeof toolCall.status === "string") {
|
||||||
|
callIdStatuses.get(toolCall.callId)!.add(toolCall.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const commandCallIds = toolCalls
|
||||||
|
.filter((item) => item.server === "command" && item.callId)
|
||||||
|
.map((item) => item.callId as string);
|
||||||
|
const fileChangeCallIds = toolCalls
|
||||||
|
.filter((item) => item.server === "file_change" && item.callId)
|
||||||
|
.map((item) => item.callId as string);
|
||||||
|
|
||||||
|
const hasCommandLifecycle = commandCallIds.some((callId) => {
|
||||||
|
const statuses = callIdStatuses.get(callId);
|
||||||
|
return statuses?.has("running") && statuses?.has("completed");
|
||||||
|
});
|
||||||
|
const hasFileChangeLifecycle = fileChangeCallIds.some((callId) => {
|
||||||
|
const statuses = callIdStatuses.get(callId);
|
||||||
|
return statuses?.has("running") && statuses?.has("completed");
|
||||||
|
});
|
||||||
|
expect.soft(hasCommandLifecycle).toBe(true);
|
||||||
|
expect.soft(hasFileChangeLifecycle).toBe(true);
|
||||||
|
} finally {
|
||||||
|
await session?.close();
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
restoreSessionDir();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
240_000
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"emits an error timeline item for failed MCP turns",
|
"emits an error timeline item for failed MCP turns",
|
||||||
async () => {
|
async () => {
|
||||||
@@ -569,9 +764,10 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
const config = {
|
const config = {
|
||||||
provider: "codex-mcp",
|
provider: "codex-mcp",
|
||||||
cwd,
|
cwd,
|
||||||
modeId: "read-only",
|
modeId: "auto",
|
||||||
approvalPolicy: "on-request",
|
approvalPolicy: "on-request",
|
||||||
} as AgentSessionConfig;
|
} as AgentSessionConfig;
|
||||||
|
const filePath = path.join(cwd, "permission.txt");
|
||||||
|
|
||||||
let session: AgentSession | null = null;
|
let session: AgentSession | null = null;
|
||||||
let captured: AgentPermissionRequest | null = null;
|
let captured: AgentPermissionRequest | null = null;
|
||||||
@@ -624,6 +820,8 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
(item) => item.type === "tool_call" && item.server === "command"
|
(item) => item.type === "tool_call" && item.server === "command"
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(existsSync(filePath)).toBe(true);
|
||||||
|
expect(readFileSync(filePath, "utf8")).toContain("ok");
|
||||||
} finally {
|
} finally {
|
||||||
await session?.close();
|
await session?.close();
|
||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
@@ -700,9 +898,10 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
const config = {
|
const config = {
|
||||||
provider: "codex-mcp",
|
provider: "codex-mcp",
|
||||||
cwd,
|
cwd,
|
||||||
modeId: "read-only",
|
modeId: "auto",
|
||||||
approvalPolicy: "on-request",
|
approvalPolicy: "on-request",
|
||||||
} as AgentSessionConfig;
|
} as AgentSessionConfig;
|
||||||
|
const filePath = path.join(cwd, "permission.txt");
|
||||||
|
|
||||||
let session: AgentSession | null = null;
|
let session: AgentSession | null = null;
|
||||||
let captured: AgentPermissionRequest | null = null;
|
let captured: AgentPermissionRequest | null = null;
|
||||||
@@ -751,6 +950,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
item.status === "denied"
|
item.status === "denied"
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(existsSync(filePath)).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
await session?.close();
|
await session?.close();
|
||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
@@ -770,9 +970,10 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
const config = {
|
const config = {
|
||||||
provider: "codex-mcp",
|
provider: "codex-mcp",
|
||||||
cwd,
|
cwd,
|
||||||
modeId: "read-only",
|
modeId: "auto",
|
||||||
approvalPolicy: "on-request",
|
approvalPolicy: "on-request",
|
||||||
} as AgentSessionConfig;
|
} as AgentSessionConfig;
|
||||||
|
const filePath = path.join(cwd, "permission.txt");
|
||||||
|
|
||||||
let session: AgentSession | null = null;
|
let session: AgentSession | null = null;
|
||||||
let captured: AgentPermissionRequest | null = null;
|
let captured: AgentPermissionRequest | null = null;
|
||||||
@@ -832,6 +1033,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(sawTurnFailed).toBe(true);
|
expect(sawTurnFailed).toBe(true);
|
||||||
expect(failureMessage ?? "").toMatch(/aborted|interrupted/i);
|
expect(failureMessage ?? "").toMatch(/aborted|interrupted/i);
|
||||||
|
expect(existsSync(filePath)).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
await session?.close();
|
await session?.close();
|
||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
@@ -860,7 +1062,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
let durationMs = 0;
|
let durationMs = 0;
|
||||||
let sawSleepCommand = false;
|
let sawSleepCommand = false;
|
||||||
let interruptIssued = false;
|
let interruptIssued = false;
|
||||||
let interruptTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session = await client.createSession(config);
|
session = await client.createSession(config);
|
||||||
@@ -871,12 +1072,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
|
|
||||||
runStartedAt = Date.now();
|
runStartedAt = Date.now();
|
||||||
const stream = session.stream(prompt);
|
const stream = session.stream(prompt);
|
||||||
interruptTimer = setTimeout(() => {
|
|
||||||
if (!interruptIssued) {
|
|
||||||
interruptIssued = true;
|
|
||||||
void session?.interrupt();
|
|
||||||
}
|
|
||||||
}, 10_000);
|
|
||||||
|
|
||||||
for await (const event of stream) {
|
for await (const event of stream) {
|
||||||
if (event.type === "permission_requested" && session) {
|
if (event.type === "permission_requested" && session) {
|
||||||
@@ -893,10 +1088,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
sawSleepCommand = true;
|
sawSleepCommand = true;
|
||||||
if (!interruptIssued) {
|
if (!interruptIssued) {
|
||||||
interruptIssued = true;
|
interruptIssued = true;
|
||||||
if (interruptTimer) {
|
|
||||||
clearTimeout(interruptTimer);
|
|
||||||
interruptTimer = null;
|
|
||||||
}
|
|
||||||
await session.interrupt();
|
await session.interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -914,10 +1105,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
if (durationMs === 0 && runStartedAt !== null) {
|
if (durationMs === 0 && runStartedAt !== null) {
|
||||||
durationMs = Date.now() - runStartedAt;
|
durationMs = Date.now() - runStartedAt;
|
||||||
}
|
}
|
||||||
if (interruptTimer) {
|
|
||||||
clearTimeout(interruptTimer);
|
|
||||||
interruptTimer = null;
|
|
||||||
}
|
|
||||||
await session?.close();
|
await session?.close();
|
||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
restoreSessionDir();
|
restoreSessionDir();
|
||||||
@@ -950,9 +1137,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
let sawCommand = false;
|
let sawCommand = false;
|
||||||
let interruptAt: number | null = null;
|
let interruptAt: number | null = null;
|
||||||
let stoppedAt: number | null = null;
|
let stoppedAt: number | null = null;
|
||||||
let interruptReason: "tool-call" | "timeout" | null = null;
|
|
||||||
const marker = `codex-mcp-abort-${randomUUID()}`;
|
const marker = `codex-mcp-abort-${randomUUID()}`;
|
||||||
let interruptTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session = await client.createSession(config);
|
session = await client.createSession(config);
|
||||||
@@ -962,13 +1147,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
].join(" ");
|
].join(" ");
|
||||||
|
|
||||||
const stream = session.stream(prompt);
|
const stream = session.stream(prompt);
|
||||||
interruptTimer = setTimeout(() => {
|
|
||||||
if (!interruptAt) {
|
|
||||||
interruptReason = "timeout";
|
|
||||||
interruptAt = Date.now();
|
|
||||||
void session?.interrupt();
|
|
||||||
}
|
|
||||||
}, 15_000);
|
|
||||||
|
|
||||||
for await (const event of stream) {
|
for await (const event of stream) {
|
||||||
if (event.type === "permission_requested" && session) {
|
if (event.type === "permission_requested" && session) {
|
||||||
@@ -985,12 +1163,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
if (commandText.includes(marker)) {
|
if (commandText.includes(marker)) {
|
||||||
sawCommand = true;
|
sawCommand = true;
|
||||||
if (!interruptAt) {
|
if (!interruptAt) {
|
||||||
interruptReason = "tool-call";
|
|
||||||
interruptAt = Date.now();
|
interruptAt = Date.now();
|
||||||
if (interruptTimer) {
|
|
||||||
clearTimeout(interruptTimer);
|
|
||||||
interruptTimer = null;
|
|
||||||
}
|
|
||||||
await session.interrupt();
|
await session.interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1011,7 +1184,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
|
|
||||||
const latencyMs = stoppedAt - interruptAt;
|
const latencyMs = stoppedAt - interruptAt;
|
||||||
expect(sawCommand).toBe(true);
|
expect(sawCommand).toBe(true);
|
||||||
expect(interruptReason).toBe("tool-call");
|
|
||||||
expect(latencyMs).toBeGreaterThanOrEqual(0);
|
expect(latencyMs).toBeGreaterThanOrEqual(0);
|
||||||
expect(latencyMs).toBeLessThan(1_000);
|
expect(latencyMs).toBeLessThan(1_000);
|
||||||
|
|
||||||
@@ -1025,9 +1197,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
const followup = await followupSession.run("Reply OK and stop.");
|
const followup = await followupSession.run("Reply OK and stop.");
|
||||||
expect(followup.finalText.toLowerCase()).toContain("ok");
|
expect(followup.finalText.toLowerCase()).toContain("ok");
|
||||||
} finally {
|
} finally {
|
||||||
if (interruptTimer) {
|
|
||||||
clearTimeout(interruptTimer);
|
|
||||||
}
|
|
||||||
await session?.close();
|
await session?.close();
|
||||||
await followupSession?.close();
|
await followupSession?.close();
|
||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
|
|||||||
- Mode switching
|
- Mode switching
|
||||||
- **Done (2025-12-24 23:35)**: WHAT: authored parity audit in `REPORT-codex-mcp-claude-parity.md:1` covering capabilities, gaps, and evidence lines; updated `plan.md:200` status. RESULT: Codex MCP vs Claude feature parity checklist documented with concrete gaps (modes, persistence, MCP servers, permissions). EVIDENCE: `REPORT-codex-mcp-claude-parity.md` contents summarizing code references.
|
- **Done (2025-12-24 23:35)**: WHAT: authored parity audit in `REPORT-codex-mcp-claude-parity.md:1` covering capabilities, gaps, and evidence lines; updated `plan.md:200` status. RESULT: Codex MCP vs Claude feature parity checklist documented with concrete gaps (modes, persistence, MCP servers, permissions). EVIDENCE: `REPORT-codex-mcp-claude-parity.md` contents summarizing code references.
|
||||||
|
|
||||||
- [ ] **Test (E2E)**: Comprehensive tool call coverage for Codex MCP provider.
|
- [x] **Test (E2E)**: Comprehensive tool call coverage for Codex MCP provider.
|
||||||
|
|
||||||
- All tool call types must be tested and emit proper timeline events:
|
- All tool call types must be tested and emit proper timeline events:
|
||||||
- **Command runs**: `shell_command` / `exec_command` → exit code, stdout, stderr
|
- **Command runs**: `shell_command` / `exec_command` → exit code, stdout, stderr
|
||||||
@@ -400,6 +400,25 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
|
|||||||
- Tool `input` and `output` are captured
|
- Tool `input` and `output` are captured
|
||||||
- `callId` is consistent across events
|
- `callId` is consistent across events
|
||||||
- Permission flow triggers when expected (for unsafe operations)
|
- Permission flow triggers when expected (for unsafe operations)
|
||||||
|
- **Done (2025-12-24 23:44)**: WHAT: added tool-call coverage helpers and E2E test in `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:153` and `packages/server/src/server/agent/providers/codex-mcp-agent.test.ts:430`, plus documented failures in `REPORT-codex-mcp-tool-call-coverage.md:1`. RESULT: test fails because file_change outputs omit file metadata and no read_file/mcp_tool_call/web_search timeline items or outputs are captured. EVIDENCE: `npm run test --workspace=@paseo/server -- codex-mcp-agent.test.ts -t "captures tool call inputs/outputs"` (fails with missing file_change output at `codex-mcp-agent.test.ts:532`, missing read_file at `codex-mcp-agent.test.ts:535`, missing mcp tool call at `codex-mcp-agent.test.ts:542`, missing web_search at `codex-mcp-agent.test.ts:549`).
|
||||||
|
|
||||||
|
- [ ] **Fix**: Codex MCP apply_patch tool_call output should include file metadata (path/kind) and before/after content.
|
||||||
|
|
||||||
|
- Ensure `patch_apply_end` timeline output includes file path info and the patch content needed for before/after validation.
|
||||||
|
- Align with `REPORT-codex-mcp-tool-call-coverage.md`.
|
||||||
|
|
||||||
|
- [ ] **Fix**: Codex MCP should emit read_file tool_call timeline items with input/output content.
|
||||||
|
|
||||||
|
- Map Codex MCP read_file events into timeline items (tool name, file path, content snippet).
|
||||||
|
- Ensure the E2E coverage test can find `tool: "read_file"` with content.
|
||||||
|
|
||||||
|
- [ ] **Fix**: Codex MCP should emit external MCP tool calls (mcp_tool_call) with input/output in timeline items.
|
||||||
|
|
||||||
|
- Ensure MCP server tool calls surface `server`, `tool`, `input`, and `output` fields.
|
||||||
|
|
||||||
|
- [ ] **Fix**: Codex MCP web_search timeline items should include query input and results output.
|
||||||
|
|
||||||
|
- Ensure web_search tool calls emit a timeline item with query and results.
|
||||||
|
|
||||||
- [ ] **CRITICAL REFACTOR**: Eliminate ALL type casting and defensive coding in Codex MCP provider.
|
- [ ] **CRITICAL REFACTOR**: Eliminate ALL type casting and defensive coding in Codex MCP provider.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user