11import { test , expect } from "../fixtures"
2- import { clearSessionDockSeed , seedSessionPermission , seedSessionQuestion , seedSessionTodos } from "../actions"
2+ import { clearSessionDockSeed , seedSessionQuestion , seedSessionTodos } from "../actions"
33import {
44 permissionDockSelector ,
55 promptSelector ,
@@ -11,11 +11,23 @@ import {
1111} from "../selectors"
1212
1313type Sdk = Parameters < typeof clearSessionDockSeed > [ 0 ]
14-
15- async function withDockSession < T > ( sdk : Sdk , title : string , fn : ( session : { id : string ; title : string } ) => Promise < T > ) {
16- const session = await sdk . session . create ( { title } ) . then ( ( r ) => r . data )
14+ type PermissionRule = { permission : string ; pattern : string ; action : "allow" | "deny" | "ask" }
15+
16+ async function withDockSession < T > (
17+ sdk : Sdk ,
18+ title : string ,
19+ fn : ( session : { id : string ; title : string } ) => Promise < T > ,
20+ opts ?: { permission ?: PermissionRule [ ] } ,
21+ ) {
22+ const session = await sdk . session
23+ . create ( opts ?. permission ? { title, permission : opts . permission } : { title } )
24+ . then ( ( r ) => r . data )
1725 if ( ! session ?. id ) throw new Error ( "Session create did not return an id" )
18- return fn ( session )
26+ try {
27+ return await fn ( session )
28+ } finally {
29+ await sdk . session . delete ( { sessionID : session . id } ) . catch ( ( ) => undefined )
30+ }
1931}
2032
2133test . setTimeout ( 120_000 )
@@ -28,6 +40,85 @@ async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>
2840 }
2941}
3042
43+ async function clearPermissionDock ( page : any , label : RegExp ) {
44+ const dock = page . locator ( permissionDockSelector )
45+ for ( let i = 0 ; i < 3 ; i ++ ) {
46+ const count = await dock . count ( )
47+ if ( count === 0 ) return
48+ await dock . getByRole ( "button" , { name : label } ) . click ( )
49+ await page . waitForTimeout ( 150 )
50+ }
51+ }
52+
53+ async function withMockPermission < T > (
54+ page : any ,
55+ request : {
56+ id : string
57+ sessionID : string
58+ permission : string
59+ patterns : string [ ]
60+ metadata ?: Record < string , unknown >
61+ always ?: string [ ]
62+ } ,
63+ opts : { child ?: any } | undefined ,
64+ fn : ( ) => Promise < T > ,
65+ ) {
66+ let pending = [
67+ {
68+ ...request ,
69+ always : request . always ?? [ "*" ] ,
70+ metadata : request . metadata ?? { } ,
71+ } ,
72+ ]
73+
74+ const list = async ( route : any ) => {
75+ await route . fulfill ( {
76+ status : 200 ,
77+ contentType : "application/json" ,
78+ body : JSON . stringify ( pending ) ,
79+ } )
80+ }
81+
82+ const reply = async ( route : any ) => {
83+ const url = new URL ( route . request ( ) . url ( ) )
84+ const id = url . pathname . split ( "/" ) . pop ( )
85+ pending = pending . filter ( ( item ) => item . id !== id )
86+ await route . fulfill ( {
87+ status : 200 ,
88+ contentType : "application/json" ,
89+ body : JSON . stringify ( true ) ,
90+ } )
91+ }
92+
93+ await page . route ( "**/permission" , list )
94+ await page . route ( "**/session/*/permissions/*" , reply )
95+
96+ const sessionList = opts ?. child
97+ ? async ( route : any ) => {
98+ const res = await route . fetch ( )
99+ const json = await res . json ( )
100+ const list = Array . isArray ( json ) ? json : Array . isArray ( json ?. data ) ? json . data : undefined
101+ if ( Array . isArray ( list ) && ! list . some ( ( item ) => item ?. id === opts . child ?. id ) ) list . push ( opts . child )
102+ await route . fulfill ( {
103+ status : res . status ( ) ,
104+ headers : res . headers ( ) ,
105+ contentType : "application/json" ,
106+ body : JSON . stringify ( json ) ,
107+ } )
108+ }
109+ : undefined
110+
111+ if ( sessionList ) await page . route ( "**/session?*" , sessionList )
112+
113+ try {
114+ return await fn ( )
115+ } finally {
116+ await page . unroute ( "**/permission" , list )
117+ await page . unroute ( "**/session/*/permissions/*" , reply )
118+ if ( sessionList ) await page . unroute ( "**/session?*" , sessionList )
119+ }
120+ }
121+
31122test ( "default dock shows prompt input" , async ( { page, sdk, gotoSession } ) => {
32123 await withDockSession ( sdk , "e2e composer dock default" , async ( session ) => {
33124 await gotoSession ( session . id )
@@ -76,72 +167,175 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
76167
77168test ( "blocked permission flow supports allow once" , async ( { page, sdk, gotoSession } ) => {
78169 await withDockSession ( sdk , "e2e composer dock permission once" , async ( session ) => {
79- await withDockSeed ( sdk , session . id , async ( ) => {
80- await gotoSession ( session . id )
81-
82- await seedSessionPermission ( sdk , {
170+ await gotoSession ( session . id )
171+ await withMockPermission (
172+ page ,
173+ {
174+ id : "per_e2e_once" ,
83175 sessionID : session . id ,
84176 permission : "bash" ,
85- patterns : [ "README.md" ] ,
86- description : "Need permission for command" ,
87- } )
88-
89- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
90- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
91-
92- await page
93- . locator ( permissionDockSelector )
94- . getByRole ( "button" , { name : / a l l o w o n c e / i } )
95- . click ( )
96- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
97- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
98- } )
177+ patterns : [ "/tmp/opencode-e2e-perm-once" ] ,
178+ metadata : { description : "Need permission for command" } ,
179+ } ,
180+ undefined ,
181+ async ( ) => {
182+ await page . goto ( page . url ( ) )
183+ await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
184+ await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
185+
186+ await clearPermissionDock ( page , / a l l o w o n c e / i)
187+ await page . goto ( page . url ( ) )
188+ await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
189+ await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
190+ } ,
191+ )
99192 } )
100193} )
101194
102195test ( "blocked permission flow supports reject" , async ( { page, sdk, gotoSession } ) => {
103196 await withDockSession ( sdk , "e2e composer dock permission reject" , async ( session ) => {
104- await withDockSeed ( sdk , session . id , async ( ) => {
105- await gotoSession ( session . id )
106-
107- await seedSessionPermission ( sdk , {
197+ await gotoSession ( session . id )
198+ await withMockPermission (
199+ page ,
200+ {
201+ id : "per_e2e_reject" ,
108202 sessionID : session . id ,
109203 permission : "bash" ,
110- patterns : [ "REJECT.md" ] ,
111- } )
112-
113- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
114- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
115-
116- await page . locator ( permissionDockSelector ) . getByRole ( "button" , { name : / d e n y / i } ) . click ( )
117- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
118- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
119- } )
204+ patterns : [ "/tmp/opencode-e2e-perm-reject" ] ,
205+ } ,
206+ undefined ,
207+ async ( ) => {
208+ await page . goto ( page . url ( ) )
209+ await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
210+ await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
211+
212+ await clearPermissionDock ( page , / d e n y / i)
213+ await page . goto ( page . url ( ) )
214+ await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
215+ await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
216+ } ,
217+ )
120218 } )
121219} )
122220
123221test ( "blocked permission flow supports allow always" , async ( { page, sdk, gotoSession } ) => {
124222 await withDockSession ( sdk , "e2e composer dock permission always" , async ( session ) => {
125- await withDockSeed ( sdk , session . id , async ( ) => {
126- await gotoSession ( session . id )
127-
128- await seedSessionPermission ( sdk , {
223+ await gotoSession ( session . id )
224+ await withMockPermission (
225+ page ,
226+ {
227+ id : "per_e2e_always" ,
129228 sessionID : session . id ,
130229 permission : "bash" ,
131- patterns : [ "README.md" ] ,
132- description : "Need permission for command" ,
230+ patterns : [ "/tmp/opencode-e2e-perm-always" ] ,
231+ metadata : { description : "Need permission for command" } ,
232+ } ,
233+ undefined ,
234+ async ( ) => {
235+ await page . goto ( page . url ( ) )
236+ await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
237+ await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
238+
239+ await clearPermissionDock ( page , / a l l o w a l w a y s / i)
240+ await page . goto ( page . url ( ) )
241+ await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
242+ await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
243+ } ,
244+ )
245+ } )
246+ } )
247+
248+ test ( "child session question request blocks parent dock and unblocks after submit" , async ( {
249+ page,
250+ sdk,
251+ gotoSession,
252+ } ) => {
253+ await withDockSession ( sdk , "e2e composer dock child question parent" , async ( session ) => {
254+ await gotoSession ( session . id )
255+
256+ const child = await sdk . session
257+ . create ( {
258+ title : "e2e composer dock child question" ,
259+ parentID : session . id ,
260+ } )
261+ . then ( ( r ) => r . data )
262+ if ( ! child ?. id ) throw new Error ( "Child session create did not return an id" )
263+
264+ try {
265+ await withDockSeed ( sdk , child . id , async ( ) => {
266+ await seedSessionQuestion ( sdk , {
267+ sessionID : child . id ,
268+ questions : [
269+ {
270+ header : "Child input" ,
271+ question : "Pick one child option" ,
272+ options : [
273+ { label : "Continue" , description : "Continue child" } ,
274+ { label : "Stop" , description : "Stop child" } ,
275+ ] ,
276+ } ,
277+ ] ,
278+ } )
279+
280+ const dock = page . locator ( questionDockSelector )
281+ await expect . poll ( ( ) => dock . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
282+ await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
283+
284+ await dock . locator ( '[data-slot="question-option"]' ) . first ( ) . click ( )
285+ await dock . getByRole ( "button" , { name : / s u b m i t / i } ) . click ( )
286+
287+ await expect . poll ( ( ) => page . locator ( questionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
288+ await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
133289 } )
290+ } finally {
291+ await sdk . session . delete ( { sessionID : child . id } ) . catch ( ( ) => undefined )
292+ }
293+ } )
294+ } )
134295
135- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
136- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
296+ test ( "child session permission request blocks parent dock and supports allow once" , async ( {
297+ page,
298+ sdk,
299+ gotoSession,
300+ } ) => {
301+ await withDockSession ( sdk , "e2e composer dock child permission parent" , async ( session ) => {
302+ await gotoSession ( session . id )
137303
138- await page
139- . locator ( permissionDockSelector )
140- . getByRole ( "button" , { name : / a l l o w a l w a y s / i } )
141- . click ( )
142- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
143- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
144- } )
304+ const child = await sdk . session
305+ . create ( {
306+ title : "e2e composer dock child permission" ,
307+ parentID : session . id ,
308+ } )
309+ . then ( ( r ) => r . data )
310+ if ( ! child ?. id ) throw new Error ( "Child session create did not return an id" )
311+
312+ try {
313+ await withMockPermission (
314+ page ,
315+ {
316+ id : "per_e2e_child" ,
317+ sessionID : child . id ,
318+ permission : "bash" ,
319+ patterns : [ "/tmp/opencode-e2e-perm-child" ] ,
320+ metadata : { description : "Need child permission" } ,
321+ } ,
322+ { child } ,
323+ async ( ) => {
324+ await page . goto ( page . url ( ) )
325+ const dock = page . locator ( permissionDockSelector )
326+ await expect . poll ( ( ) => dock . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
327+ await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
328+
329+ await clearPermissionDock ( page , / a l l o w o n c e / i)
330+ await page . goto ( page . url ( ) )
331+
332+ await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
333+ await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
334+ } ,
335+ )
336+ } finally {
337+ await sdk . session . delete ( { sessionID : child . id } ) . catch ( ( ) => undefined )
338+ }
145339 } )
146340} )
147341
0 commit comments