@@ -5,6 +5,8 @@ import os from "node:os";
55import path from "node:path" ;
66import { Readable , Writable } from "node:stream" ;
77import { describe , expect , vi } from "vitest" ;
8+ import { AgentMode } from "../../../AgentMode" ;
9+ import { ApprovalOptionId } from "../../../ApprovalOptionId" ;
810import { removeDirectoryWithRetry , writeCodexHomeConfig } from "../../acp-test-utils" ;
911
1012export const RUN_E2E_TESTS = process . env [ "RUN_E2E_TESTS" ] === "true" ;
@@ -13,11 +15,38 @@ const DEFAULT_E2E_SUITE_TIMEOUT_MS = 60_000;
1315export interface SpawnedSessionFixture {
1416 readonly response : acp . NewSessionResponse ;
1517 expectPromptText ( promptText : string , assertText : ( text : string ) => void , timeoutMs ?: number ) : Promise < void > ;
18+ readPermissionRequests ( toolCallKind : acp . ToolKind ) : acp . RequestPermissionRequest [ ] ;
19+ }
20+
21+ export function expectEndTurn ( response : acp . PromptResponse ) : void {
22+ expect ( response . stopReason ) . toBe ( "end_turn" ) ;
23+ }
24+
25+ export type PermissionResponder = (
26+ params : acp . RequestPermissionRequest ,
27+ ) => acp . RequestPermissionResponse ;
28+
29+ export function createPermissionResponder (
30+ expectedToolCallKind : acp . ToolKind ,
31+ optionId : ApprovalOptionId ,
32+ ) : PermissionResponder {
33+ return ( request ) => createPermissionResponse (
34+ request . toolCall . kind === expectedToolCallKind ? optionId : null
35+ ) ;
36+ }
37+
38+ export function createPermissionResponse ( optionId : ApprovalOptionId | null ) : acp . RequestPermissionResponse {
39+ if ( optionId === null ) {
40+ return { outcome : { outcome : "cancelled" } } ;
41+ }
42+ return { outcome : { outcome : "selected" , optionId} } ;
1643}
1744
1845export interface SpawnedAgentFixture {
1946 readonly connection : acp . ClientSideConnection ;
47+ readonly workspaceDir : string ;
2048 createSession ( ) : Promise < SpawnedSessionFixture > ;
49+ setPermissionResponder ( responder : PermissionResponder ) : void ;
2150 dispose ( ) : Promise < void > ;
2251}
2352
@@ -51,11 +80,23 @@ interface RuntimePaths {
5180
5281class RecordingClient implements acp . Client {
5382 private readonly textBySessionId = new Map < string , string > ( ) ;
83+ private readonly permissionRequestsBySessionId = new Map < string , acp . RequestPermissionRequest [ ] > ( ) ;
84+ private permissionResponder : PermissionResponder = ( ) => ( {
85+ outcome : { outcome : "cancelled" } ,
86+ } ) ;
5487
55- async requestPermission ( _params : acp . RequestPermissionRequest ) : Promise < acp . RequestPermissionResponse > {
56- return {
57- outcome : { outcome : "cancelled" } ,
58- } ;
88+ setPermissionResponder ( responder : PermissionResponder ) : void {
89+ this . permissionResponder = responder ;
90+ }
91+
92+ async requestPermission ( params : acp . RequestPermissionRequest ) : Promise < acp . RequestPermissionResponse > {
93+ let requests = this . permissionRequestsBySessionId . get ( params . sessionId ) ;
94+ if ( ! requests ) {
95+ requests = [ ] ;
96+ this . permissionRequestsBySessionId . set ( params . sessionId , requests ) ;
97+ }
98+ requests . push ( params ) ;
99+ return this . permissionResponder ( params ) ;
59100 }
60101
61102 async sessionUpdate ( params : acp . SessionNotification ) : Promise < void > {
@@ -70,6 +111,14 @@ class RecordingClient implements acp.Client {
70111 readText ( sessionId : string ) : string {
71112 return this . textBySessionId . get ( sessionId ) ?? "" ;
72113 }
114+
115+ readPermissionRequests (
116+ sessionId : string ,
117+ toolCallKind : acp . ToolKind ,
118+ ) : acp . RequestPermissionRequest [ ] {
119+ const requests = this . permissionRequestsBySessionId . get ( sessionId ) ?? [ ] ;
120+ return requests . filter ( ( request ) => request . toolCall . kind === toolCallKind ) ;
121+ }
73122}
74123
75124export async function createFixtureWithSkill ( skill : TestSkill ) : Promise < SpawnedAgentFixture > {
@@ -104,6 +153,10 @@ export async function createAuthenticatedFixture(
104153 } , runtimePaths , extraEnv ) ;
105154}
106155
156+ export async function createReadOnlyFixture ( ) : Promise < SpawnedAgentFixture > {
157+ return await createAuthenticatedFixture ( undefined , { INITIAL_AGENT_MODE : AgentMode . ReadOnly . id } ) ;
158+ }
159+
107160export interface GatewayFixtureOptions {
108161 readonly baseUrl : string ;
109162 readonly headers ?: Record < string , string > ;
@@ -186,12 +239,19 @@ async function createSpawnedFixture(
186239 async expectPromptText ( promptText : string , assertText : ( text : string ) => void , timeoutMs = 30_000 ) : Promise < void > {
187240 await expectPromptTextForSession ( connection , client , newSessionResponse . sessionId , promptText , assertText , timeoutMs ) ;
188241 } ,
242+ readPermissionRequests ( toolCallKind : acp . ToolKind ) : acp . RequestPermissionRequest [ ] {
243+ return client . readPermissionRequests ( newSessionResponse . sessionId , toolCallKind ) ;
244+ } ,
189245 } ;
190246 } ;
191247
192248 return {
193249 connection,
250+ workspaceDir : runtimePaths . workspaceDir ,
194251 createSession,
252+ setPermissionResponder ( responder : PermissionResponder ) : void {
253+ client . setPermissionResponder ( responder ) ;
254+ } ,
195255 async dispose ( ) : Promise < void > {
196256 if ( ! agentProcess . stdin . destroyed && ! agentProcess . stdin . writableEnded ) {
197257 agentProcess . stdin . end ( ) ;
@@ -281,9 +341,7 @@ async function expectPromptTextForSession(
281341 } ] ,
282342 } ) ;
283343
284- if ( promptResponse . stopReason !== "end_turn" ) {
285- throw new Error ( `Unexpected stop reason: ${ promptResponse . stopReason } ` ) ;
286- }
344+ expectEndTurn ( promptResponse ) ;
287345
288346 await vi . waitFor ( ( ) => {
289347 const sessionText = client . readText ( sessionId ) ;
0 commit comments