test: cover permission interrupt + read-only gating

This commit is contained in:
Mohamed Boudra
2025-12-24 18:31:05 +07:00
parent f65a134ac0
commit 16ccce4291
2 changed files with 160 additions and 3 deletions

View File

@@ -604,6 +604,67 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
180_000
);
test(
"requires permission before commands in read-only (untrusted) mode",
async () => {
const cwd = tmpCwd();
const restoreSessionDir = useTempCodexSessionDir();
const { CodexMcpAgentClient } = await loadCodexMcpAgentClient();
const client = new CodexMcpAgentClient();
const config = {
provider: "codex-mcp",
cwd,
modeId: "read-only",
approvalPolicy: "untrusted",
} as AgentSessionConfig;
let session: AgentSession | null = null;
let captured: AgentPermissionRequest | null = null;
const timelineItems: AgentTimelineItem[] = [];
try {
session = await client.createSession(config);
const prompt = [
"Request approval to run the command `pwd`.",
"After approval, run it and reply DONE.",
].join(" ");
for await (const event of session.stream(prompt)) {
if (event.type === "permission_requested" && !captured) {
captured = event.request;
await session.respondToPermission(captured.id, { behavior: "allow" });
}
if (event.type === "timeline" && providerFromEvent(event) === "codex-mcp") {
timelineItems.push(event.item);
}
if (event.type === "turn_completed" || event.type === "turn_failed") {
break;
}
}
const permissionRequestIndex = timelineItems.findIndex(
(item) =>
item.type === "tool_call" &&
item.server === "permission" &&
item.status === "requested"
);
const commandIndex = timelineItems.findIndex(
(item) => item.type === "tool_call" && item.server === "command"
);
expect(captured).not.toBeNull();
expect(permissionRequestIndex).toBeGreaterThanOrEqual(0);
expect(commandIndex).toBeGreaterThan(permissionRequestIndex);
} finally {
await session?.close();
rmSync(cwd, { recursive: true, force: true });
restoreSessionDir();
}
},
180_000
);
test(
"denies permission requests and reports resolution",
async () => {
@@ -679,6 +740,92 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
180_000
);
test(
"aborts when permission responses request an interrupt",
async () => {
const cwd = tmpCwd();
const restoreSessionDir = useTempCodexSessionDir();
const { CodexMcpAgentClient } = await loadCodexMcpAgentClient();
const client = new CodexMcpAgentClient();
const config = {
provider: "codex-mcp",
cwd,
modeId: "full-access",
approvalPolicy: "on-request",
} as AgentSessionConfig;
let session: AgentSession | null = null;
let captured: AgentPermissionRequest | null = null;
let sawPermissionResolved = false;
let sawTurnFailed = false;
let failureMessage: string | null = null;
const timelineItems: AgentTimelineItem[] = [];
try {
session = await client.createSession(config);
const prompt = [
"Request approval to run the command `pwd`.",
"If approval is denied, stop immediately.",
].join(" ");
for await (const event of session.stream(prompt)) {
if (event.type === "permission_requested" && !captured) {
captured = event.request;
await session.respondToPermission(captured.id, {
behavior: "deny",
message: "Stop now.",
interrupt: true,
});
}
if (
event.type === "permission_resolved" &&
captured &&
event.requestId === captured.id &&
event.resolution.behavior === "deny" &&
event.resolution.interrupt
) {
sawPermissionResolved = true;
}
if (event.type === "timeline" && providerFromEvent(event) === "codex-mcp") {
timelineItems.push(event.item);
}
if (event.type === "turn_failed") {
sawTurnFailed = true;
failureMessage = event.error;
break;
}
if (event.type === "turn_completed") {
break;
}
}
expect(captured).not.toBeNull();
expect(sawPermissionResolved).toBe(true);
expect(
timelineItems.some(
(item) =>
item.type === "tool_call" &&
item.server === "permission" &&
item.status === "denied"
)
).toBe(true);
expect(
timelineItems.some(
(item) => item.type === "tool_call" && item.server === "command"
)
).toBe(false);
expect(sawTurnFailed).toBe(true);
expect(failureMessage ?? "").toMatch(/aborted|interrupted/i);
} finally {
await session?.close();
rmSync(cwd, { recursive: true, force: true });
restoreSessionDir();
}
},
180_000
);
test(
"interrupts a long-running command via abort",
async () => {

16
plan.md
View File

@@ -32,18 +32,28 @@ 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.
- [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.
- [x] **Test (E2E)**: Add coverage for permission abort path (respondToPermission interrupt) and clarify read-only/untrusted gating behavior for Codex MCP.
- **Done (2025-12-24 18:30)**: Added Codex MCP E2E tests for read-only/untrusted permission gating and interrupt-based permission abort handling; Vitest failed locally due to missing tool version for `vitest` in `.tool-versions`.
- [ ] **Docs**: Document Codex MCP E2E environment requirements (codex CLI version/availability, credentials, CODEX_HOME/CODEX_SESSION_DIR isolation).
- [ ] **Test / Review (E2E + Typecheck)**: Document Codex CLI/model/permission mismatches and run typecheck.
- [ ] **Review**: Re-audit Codex MCP E2E coverage and environment requirements after follow-ups.
- Capture exact failing scenarios, including CLI flags, sandbox/approval policy, and observed vs expected behavior.
- Review code, really bad typing and code is generally hortible quality, write a report for follow up tasks, reduce casting to a minimum, strongly typing everywhere
- Paste key error output snippets and summarize root causes.
- Run server typecheck and record failures; add fix tasks for each category.
- [ ] **Review**: Check implementation + edge cases.
- If issues: add fix tasks + re-review.
- [ ] **Review**: Strong type/quality review of provider + tests.
- Identify type safety issues, missing error handling, and brittle assumptions.
- Add fix tasks and re-review.
- [ ] **Test (E2E)**: Final verification (full scenario matrix).
- read-only + on-request