11import type * as acp from "@agentclientprotocol/sdk" ;
22import path from "node:path" ;
3- import { afterEach , beforeEach , expect , it } from "vitest" ;
3+ import { afterEach , beforeEach , describe , expect , it } from "vitest" ;
44import { ApprovalOptionId } from "../../../ApprovalOptionId" ;
55import {
66 createAuthenticatedFixture ,
@@ -13,6 +13,7 @@ import {
1313
1414const MCP_SERVER_NAME = "integration-mcp" ;
1515const MCP_ECHO_MESSAGE = "mcp approval e2e" ;
16+ const MCP_ECHO_PROMPT = `Use the ${ MCP_SERVER_NAME } MCP echo tool with message "${ MCP_ECHO_MESSAGE } ". Reply with exactly the tool result and no extra text.` ;
1617
1718function createMcpServer ( ) : acp . McpServerStdio {
1819 return {
@@ -31,6 +32,12 @@ function createMcpPermissionResponder(optionId: ApprovalOptionId): PermissionRes
3132 return ( request ) => createPermissionResponse ( isMcpPermissionRequest ( request ) ? optionId : null ) ;
3233}
3334
35+ function failingPermissionResponder ( label : string ) : PermissionResponder {
36+ return ( request ) => {
37+ throw new Error ( `${ label } : unexpected permission request (kind=${ request . toolCall . kind } )` ) ;
38+ } ;
39+ }
40+
3441describeE2E ( "E2E MCP approval tests" , ( ) => {
3542 let fixture : SpawnedAgentFixture ;
3643 let sessionId : string ;
@@ -55,7 +62,7 @@ describeE2E("E2E MCP approval tests", () => {
5562
5663 await fixture . expectPromptText (
5764 sessionId ,
58- `Use the ${ MCP_SERVER_NAME } MCP echo tool with message " ${ MCP_ECHO_MESSAGE } ". Reply with exactly the tool result and no extra text.` ,
65+ MCP_ECHO_PROMPT ,
5966 ( text ) => expect ( text ) . toContain ( `You said: ${ MCP_ECHO_MESSAGE } ` ) ,
6067 ) ;
6168 expectMcpToolPermissionRequest ( ) ;
@@ -73,4 +80,97 @@ describeE2E("E2E MCP approval tests", () => {
7380 } ) ) ;
7481 expectMcpToolPermissionRequest ( ) ;
7582 } ) ;
83+
84+ describe ( "persisted approvals" , ( ) => {
85+ let beforeRestartFixture : SpawnedAgentFixture | null = null ;
86+ let afterRestartFixture : SpawnedAgentFixture | null = null ;
87+
88+ beforeEach ( async ( ) => {
89+ // The outer beforeEach already created `fixture` without a config-backed MCP server.
90+ // Persistence tests need the server in config.toml so Codex offers "Always allow",
91+ // so dispose that fixture and replace it with a config-backed one.
92+ await fixture . dispose ( ) ;
93+ beforeRestartFixture = await createAuthenticatedFixture ( {
94+ configBackedMcpServers : [ createMcpServer ( ) ] ,
95+ } ) ;
96+ fixture = beforeRestartFixture ;
97+ sessionId = ( await fixture . createSession ( ) ) . sessionId ;
98+ } ) ;
99+
100+ afterEach ( async ( ) => {
101+ await afterRestartFixture ?. dispose ( ) ;
102+ afterRestartFixture = null ;
103+ beforeRestartFixture = null ;
104+ } ) ;
105+
106+ it ( "does not re-prompt across agent restart when user picks Always allow" , async ( ) => {
107+ fixture . setPermissionResponder ( createMcpPermissionResponder ( ApprovalOptionId . AllowPersist ) ) ;
108+
109+ await fixture . expectPromptText (
110+ sessionId ,
111+ MCP_ECHO_PROMPT ,
112+ ( text ) => expect ( text ) . toContain ( `You said: ${ MCP_ECHO_MESSAGE } ` ) ,
113+ ) ;
114+
115+ const requests = fixture . readPermissionRequests ( sessionId , "execute" ) ;
116+ expect ( requests . length ) . toBe ( 1 ) ;
117+ expect ( isMcpPermissionRequest ( requests [ 0 ] ! ) ) . toBe ( true ) ;
118+ const optionIds = requests [ 0 ] ! . options . map ( ( option ) => option . optionId ) ;
119+ expect ( optionIds ) . toContain ( ApprovalOptionId . AllowPersist ) ;
120+
121+ afterRestartFixture = await fixture . restart ( ) ;
122+ // `fixture` is now stopped; route all subsequent calls through afterRestartFixture.
123+ fixture = afterRestartFixture ;
124+ afterRestartFixture . setPermissionResponder ( failingPermissionResponder ( "after restart" ) ) ;
125+ const resumedSessionId = ( await afterRestartFixture . createSession ( ) ) . sessionId ;
126+
127+ await afterRestartFixture . expectPromptText (
128+ resumedSessionId ,
129+ MCP_ECHO_PROMPT ,
130+ ( text ) => expect ( text ) . toContain ( `You said: ${ MCP_ECHO_MESSAGE } ` ) ,
131+ ) ;
132+ expect ( afterRestartFixture . readPermissionRequests ( resumedSessionId , "execute" ) . length ) . toBe ( 0 ) ;
133+ } ) ;
134+
135+ it ( "does not re-prompt within a session when user picks Allow for session, but re-prompts after restart" , async ( ) => {
136+ let approvalsGranted = 0 ;
137+ fixture . setPermissionResponder ( ( request ) => {
138+ if ( ! isMcpPermissionRequest ( request ) ) {
139+ return createPermissionResponse ( null ) ;
140+ }
141+ approvalsGranted += 1 ;
142+ if ( approvalsGranted > 1 ) {
143+ throw new Error ( "Allow-for-session approval should be reused within the same session" ) ;
144+ }
145+ return createPermissionResponse ( ApprovalOptionId . AllowForSession ) ;
146+ } ) ;
147+
148+ await fixture . expectPromptText (
149+ sessionId ,
150+ MCP_ECHO_PROMPT ,
151+ ( text ) => expect ( text ) . toContain ( `You said: ${ MCP_ECHO_MESSAGE } ` ) ,
152+ ) ;
153+ expect ( fixture . readPermissionRequests ( sessionId , "execute" ) . length ) . toBe ( 1 ) ;
154+
155+ await fixture . expectPromptText (
156+ sessionId ,
157+ MCP_ECHO_PROMPT ,
158+ ( text ) => expect ( text ) . toContain ( `You said: ${ MCP_ECHO_MESSAGE } ` ) ,
159+ ) ;
160+ // Still just the one approval recorded - the second call reused the session-scoped grant.
161+ expect ( fixture . readPermissionRequests ( sessionId , "execute" ) . length ) . toBe ( 1 ) ;
162+
163+ afterRestartFixture = await fixture . restart ( ) ;
164+ fixture = afterRestartFixture ;
165+ afterRestartFixture . setPermissionResponder ( createMcpPermissionResponder ( ApprovalOptionId . AllowOnce ) ) ;
166+ const newSessionId = ( await afterRestartFixture . createSession ( ) ) . sessionId ;
167+
168+ await afterRestartFixture . expectPromptText (
169+ newSessionId ,
170+ MCP_ECHO_PROMPT ,
171+ ( text ) => expect ( text ) . toContain ( `You said: ${ MCP_ECHO_MESSAGE } ` ) ,
172+ ) ;
173+ expect ( afterRestartFixture . readPermissionRequests ( newSessionId , "execute" ) . length ) . toBe ( 1 ) ;
174+ } ) ;
175+ } ) ;
76176} ) ;
0 commit comments