1- import { describe , expect , it } from "bun:test"
1+ import { afterEach , describe , expect , it } from "bun:test"
22import type {
33 AgentSideConnection ,
44 RequestPermissionRequest ,
55 RequestPermissionResponse ,
66 SessionUpdate ,
77} from "@agentclientprotocol/sdk"
88import type { Event , OpencodeClient } from "@opencode-ai/sdk/v2"
9+ import { createTwoFilesPatch } from "diff"
910import { Effect , ManagedRuntime } from "effect"
11+ import { mkdtemp , rm } from "node:fs/promises"
12+ import { tmpdir } from "node:os"
13+ import path from "node:path"
1014import { ACPEvent } from "@/acp/event"
1115import { ACPSession } from "@/acp/session"
1216
1317type PermissionEvent = Extract < Event , { type : "permission.asked" } >
1418type PermissionReplyParams = Parameters < OpencodeClient [ "permission" ] [ "reply" ] > [ 0 ]
1519type SessionUpdateParams = Parameters < AgentSideConnection [ "sessionUpdate" ] > [ 0 ]
20+ const cleanupDirs : string [ ] = [ ]
21+
22+ afterEach ( async ( ) => {
23+ await Promise . all ( cleanupDirs . splice ( 0 ) . map ( ( dir ) => rm ( dir , { recursive : true , force : true } ) ) )
24+ } )
1625
1726const pollUntil = async (
1827 check : ( ) => boolean | Promise < boolean > ,
@@ -137,6 +146,14 @@ function textFromUpdates(updates: SessionUpdateParams[], sessionId: string) {
137146 . join ( "" )
138147}
139148
149+ async function tempFile ( name : string , content : string ) {
150+ const dir = await mkdtemp ( path . join ( tmpdir ( ) , "opencode-acp-permission-" ) )
151+ cleanupDirs . push ( dir )
152+ const file = path . join ( dir , name )
153+ await Bun . write ( file , content )
154+ return file
155+ }
156+
140157describe ( "acp permissions" , ( ) => {
141158 it ( "sends requestPermission and replies with the selected outcome" , async ( ) => {
142159 const harness = createHarness ( )
@@ -151,7 +168,7 @@ describe("acp permissions", () => {
151168 toolCall : {
152169 toolCallId : "call_1" ,
153170 status : "pending" ,
154- title : "bash " ,
171+ title : "printf hello " ,
155172 rawInput : { command : "printf hello" } ,
156173 kind : "execute" ,
157174 locations : [ ] ,
@@ -165,6 +182,116 @@ describe("acp permissions", () => {
165182 expect ( harness . replies ) . toEqual ( [ { requestID : "perm_1" , reply : "once" , directory : "/workspace" } ] )
166183 } )
167184
185+ it ( "uses permission metadata for non-shell titles" , async ( ) => {
186+ const harness = createHarness ( )
187+ await createSession ( harness . session , "ses_a" )
188+
189+ harness . subscription . handle (
190+ permissionAsked ( "ses_a" , "perm_fetch" , {
191+ permission : "webfetch" ,
192+ metadata : {
193+ url : "https://example.com/docs" ,
194+ format : "markdown" ,
195+ } ,
196+ tool : { messageID : "msg_1" , callID : "call_1" } ,
197+ } ) ,
198+ )
199+
200+ await pollUntil ( ( ) => harness . replies . length === 1 , "webfetch permission was never replied" )
201+
202+ expect ( harness . requests [ 0 ] ?. toolCall ) . toMatchObject ( {
203+ toolCallId : "call_1" ,
204+ title : "https://example.com/docs" ,
205+ kind : "fetch" ,
206+ rawInput : { url : "https://example.com/docs" , format : "markdown" } ,
207+ } )
208+ } )
209+
210+ it ( "includes a diff content block for edit permission metadata" , async ( ) => {
211+ const filepath = await tempFile ( "file.ts" , "before\n" )
212+ const harness = createHarness ( )
213+ await createSession ( harness . session , "ses_a" )
214+
215+ harness . subscription . handle (
216+ permissionAsked ( "ses_a" , "perm_edit" , {
217+ permission : "edit" ,
218+ metadata : {
219+ filepath,
220+ diff : createTwoFilesPatch ( filepath , filepath , "before\n" , "after\n" ) ,
221+ } ,
222+ tool : { messageID : "msg_1" , callID : "call_1" } ,
223+ } ) ,
224+ )
225+
226+ await pollUntil ( ( ) => harness . replies . length === 1 , "edit permission was never replied" )
227+
228+ expect ( harness . requests [ 0 ] ?. toolCall ) . toMatchObject ( {
229+ toolCallId : "call_1" ,
230+ title : filepath ,
231+ kind : "edit" ,
232+ locations : [ { path : filepath } ] ,
233+ content : [
234+ {
235+ type : "diff" ,
236+ path : filepath ,
237+ oldText : "before\n" ,
238+ newText : "after\n" ,
239+ } ,
240+ ] ,
241+ } )
242+ } )
243+
244+ it ( "includes per-file diff blocks and locations for apply_patch permission metadata" , async ( ) => {
245+ const first = await tempFile ( "first.ts" , "one\n" )
246+ const second = await tempFile ( "second.ts" , "alpha\n" )
247+ const harness = createHarness ( )
248+ await createSession ( harness . session , "ses_a" )
249+
250+ harness . subscription . handle (
251+ permissionAsked ( "ses_a" , "perm_patch" , {
252+ permission : "edit" ,
253+ metadata : {
254+ filepath : "first.ts, second.ts" ,
255+ files : [
256+ {
257+ filePath : first ,
258+ relativePath : "first.ts" ,
259+ patch : createTwoFilesPatch ( first , first , "one\n" , "two\n" ) ,
260+ } ,
261+ {
262+ filePath : second ,
263+ relativePath : "second.ts" ,
264+ patch : createTwoFilesPatch ( second , second , "alpha\n" , "beta\n" ) ,
265+ } ,
266+ ] ,
267+ } ,
268+ tool : { messageID : "msg_1" , callID : "call_1" } ,
269+ } ) ,
270+ )
271+
272+ await pollUntil ( ( ) => harness . replies . length === 1 , "apply_patch permission was never replied" )
273+
274+ expect ( harness . requests [ 0 ] ?. toolCall ) . toMatchObject ( {
275+ toolCallId : "call_1" ,
276+ title : "2 files" ,
277+ locations : [ { path : first } , { path : second } ] ,
278+ content : [
279+ {
280+ type : "diff" ,
281+ path : first ,
282+ oldText : "one\n" ,
283+ newText : "two\n" ,
284+ } ,
285+ {
286+ type : "diff" ,
287+ path : second ,
288+ oldText : "alpha\n" ,
289+ newText : "beta\n" ,
290+ } ,
291+ ] ,
292+ } )
293+ } )
294+
168295 it ( "forwards external_directory metadata and locations to requestPermission" , async ( ) => {
169296 const harness = createHarness ( )
170297 await createSession ( harness . session , "ses_a" )
@@ -189,7 +316,7 @@ describe("acp permissions", () => {
189316 toolCall : {
190317 toolCallId : "call_1" ,
191318 status : "pending" ,
192- title : "external_directory " ,
319+ title : "Create external directory " ,
193320 rawInput : {
194321 command : "mkdir -p /tmp/outside" ,
195322 description : "Create external directory" ,
0 commit comments