@@ -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
4449describeE2E ( "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