mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
test: cover permission interrupt + read-only gating
This commit is contained in:
@@ -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
16
plan.md
@@ -32,18 +32,28 @@ Build a new Codex MCP provider side‑by‑side 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
|
||||
|
||||
Reference in New Issue
Block a user