Skip to content

Commit 2f5b630

Browse files
committed
Add more e2e mcp approval tests
1 parent cf87c76 commit 2f5b630

1 file changed

Lines changed: 59 additions & 16 deletions

File tree

src/__tests__/CodexACPAgent/e2e/acp-e2e-mcp-approval.test.ts

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ function createMcpPermissionResponse(optionId: McpApprovalOptionIdValue | null):
3737
return {outcome: {outcome: "selected", optionId}};
3838
}
3939

40-
function createMcpPermissionResponder(optionId: McpApprovalOptionIdValue): PermissionResponder {
41-
return (request) => createMcpPermissionResponse(isMcpPermissionRequest(request) ? optionId : null);
40+
function createMcpPermissionResponder(...optionIds: McpApprovalOptionIdValue[]): PermissionResponder {
41+
const queue = [...optionIds];
42+
return (request) => createMcpPermissionResponse(
43+
isMcpPermissionRequest(request)
44+
? queue.shift() ?? McpApprovalOptionId.Decline
45+
: null,
46+
);
4247
}
4348

4449
describeE2E("E2E MCP approval tests", () => {
@@ -52,30 +57,40 @@ describeE2E("E2E MCP approval tests", () => {
5257
await fixture.dispose();
5358
});
5459

55-
function expectMcpToolPermissionRequest(sessionId: string): void {
56-
const requests = fixture.readPermissionRequests(sessionId, "execute");
57-
expect(requests.length).toBe(1);
58-
expect(isMcpPermissionRequest(requests[0]!)).toBe(true);
59-
}
60-
61-
it("executes an approved MCP tool call", async () => {
62-
fixture.setPermissionResponder(createMcpPermissionResponder(McpApprovalOptionId.AllowOnce));
60+
async function createMcpSession(): Promise<{ sessionId: string; invocationMarkerPath: string }> {
6361
const invocationMarkerPath = path.join(fixture.workspaceDir, `mcp-tool-invocation-${crypto.randomUUID()}.txt`);
6462
const sessionId = (await fixture.createSession([createMcpServer(invocationMarkerPath)])).sessionId;
63+
return {sessionId, invocationMarkerPath};
64+
}
6565

66+
async function expectEchoToolReply(sessionId: string, message: string): Promise<void> {
6667
await fixture.expectPromptText(
6768
sessionId,
68-
`Use the ${MCP_SERVER_NAME} MCP echo tool with message "${MCP_ECHO_MESSAGE}". Reply with exactly the tool result and no extra text.`,
69-
(text) => expect(text).toContain(`You said: ${MCP_ECHO_MESSAGE}`),
69+
`Use the ${MCP_SERVER_NAME} MCP echo tool with message "${message}". Reply with exactly the tool result and no extra text.`,
70+
(text) => expect(text).toContain(`You said: ${message}`),
7071
);
72+
}
73+
74+
function expectMcpPermissionRequestCount(sessionId: string, count: number): void {
75+
const requests = fixture.readPermissionRequests(sessionId, "execute");
76+
expect(requests.length).toBe(count);
77+
for (const request of requests) {
78+
expect(isMcpPermissionRequest(request)).toBe(true);
79+
}
80+
}
81+
82+
it("executes an approved MCP tool call", async () => {
83+
fixture.setPermissionResponder(createMcpPermissionResponder(McpApprovalOptionId.AllowOnce));
84+
const {sessionId, invocationMarkerPath} = await createMcpSession();
85+
86+
await expectEchoToolReply(sessionId, MCP_ECHO_MESSAGE);
7187
expect(fs.readFileSync(invocationMarkerPath, "utf8")).toBe(MCP_ECHO_MESSAGE);
72-
expectMcpToolPermissionRequest(sessionId);
88+
expectMcpPermissionRequestCount(sessionId, 1);
7389
});
7490

7591
it("ends turn when MCP tool call is rejected", async () => {
7692
fixture.setPermissionResponder(createMcpPermissionResponder(McpApprovalOptionId.Decline));
77-
const invocationMarkerPath = path.join(fixture.workspaceDir, `mcp-tool-invocation-${crypto.randomUUID()}.txt`);
78-
const sessionId = (await fixture.createSession([createMcpServer(invocationMarkerPath)])).sessionId;
93+
const {sessionId, invocationMarkerPath} = await createMcpSession();
7994

8095
expectEndTurn(await fixture.connection.prompt({
8196
sessionId,
@@ -85,6 +100,34 @@ describeE2E("E2E MCP approval tests", () => {
85100
}],
86101
}));
87102
expect(fs.existsSync(invocationMarkerPath)).toBe(false);
88-
expectMcpToolPermissionRequest(sessionId);
103+
expectMcpPermissionRequestCount(sessionId, 1);
104+
});
105+
106+
it("skips subsequent approvals in the same session when allow_session is selected", async () => {
107+
fixture.setPermissionResponder(
108+
createMcpPermissionResponder(McpApprovalOptionId.AllowSession, McpApprovalOptionId.Decline),
109+
);
110+
const {sessionId, invocationMarkerPath} = await createMcpSession();
111+
112+
await expectEchoToolReply(sessionId, "session approval first");
113+
await expectEchoToolReply(sessionId, "session approval second");
114+
115+
expect(fs.readFileSync(invocationMarkerPath, "utf8")).toBe("session approval second");
116+
expectMcpPermissionRequestCount(sessionId, 1);
117+
});
118+
119+
// This case only asserts observed same-session approval.
120+
// codex-acp adapter produces persist metadata for MCP tool approvals, but its persistence behavior depends on upstream.
121+
it("skips subsequent approvals in the same session when allow_always is selected", async () => {
122+
fixture.setPermissionResponder(
123+
createMcpPermissionResponder(McpApprovalOptionId.AllowAlways, McpApprovalOptionId.Decline),
124+
);
125+
const {sessionId, invocationMarkerPath} = await createMcpSession();
126+
127+
await expectEchoToolReply(sessionId, "always approval first");
128+
await expectEchoToolReply(sessionId, "always approval second");
129+
130+
expect(fs.readFileSync(invocationMarkerPath, "utf8")).toBe("always approval second");
131+
expectMcpPermissionRequestCount(sessionId, 1);
89132
});
90133
});

0 commit comments

Comments
 (0)