@@ -143,6 +143,7 @@ describe('CopilotCLISession', () => {
143143 let instaService : IInstantiationService ;
144144 let sdk : ICopilotCLISDK ;
145145 let requestLogger : IRequestLogger ;
146+ let toolsService : FakeToolsService ;
146147 const delegationService = new class extends mock < IChatDelegationSummaryService > ( ) {
147148 override async summarize ( context : ChatContext , token : CancellationToken ) : Promise < string | undefined > {
148149 return undefined ;
@@ -168,6 +169,7 @@ describe('CopilotCLISession', () => {
168169 workspaceService = createWorkspaceService ( '/workspace' ) ;
169170 sessionOptions = new CopilotCLISessionOptions ( { workspaceInfo : workspaceInfoFor ( workspaceService . getWorkspaceFolders ( ) ! [ 0 ] ) } , logger ) ;
170171 instaService = services . seal ( ) ;
172+ toolsService = new FakeToolsService ( ) ;
171173 } ) ;
172174
173175 afterEach ( ( ) => {
@@ -193,7 +195,7 @@ describe('CopilotCLISession', () => {
193195 delegationService ,
194196 requestLogger ,
195197 new NullICopilotCLIImageSupport ( ) ,
196- new FakeToolsService ( ) ,
198+ toolsService ,
197199 new FakeUserQuestionHandler ( )
198200 ) ) ;
199201 }
@@ -330,6 +332,7 @@ describe('CopilotCLISession', () => {
330332 let result : unknown ;
331333 const nonAttachedFilePath = '/outside-workspace/other-file.ts' ;
332334 const attachedFilePath = '/outside-workspace/attached-file.ts' ;
335+ toolsService . setConfirmationResult ( 'no' ) ;
333336 sdkSession . send = async ( { prompt } : any ) => {
334337 sdkSession . emit ( 'assistant.turn_start' , { } ) ;
335338 sdkSession . emit ( 'assistant.message' , { content : `Echo: ${ prompt } ` } ) ;
@@ -339,11 +342,11 @@ describe('CopilotCLISession', () => {
339342 const session = await createSession ( ) ;
340343 const stream = new MockChatResponseStream ( ) ;
341344 session . attachStream ( stream ) ;
342- disposables . add ( session . attachPermissionHandler ( async ( ) => false ) ) ;
343345
344346 const attachments = [ { type : 'file' as const , path : attachedFilePath , displayName : 'attached-file.ts' } ] ;
345347 await session . handleRequest ( { id : '' , toolInvocationToken : undefined as never } , { prompt : 'Test' } , attachments as any , undefined , authInfo , CancellationToken . None ) ;
346348 expect ( result ) . toEqual ( { kind : 'denied-interactively-by-user' } ) ;
349+ expect ( toolsService . invokeToolCalls ) . toHaveLength ( 1 ) ;
347350 } ) ;
348351
349352 it ( 'auto-approves read permission inside working directory without external handler' , async ( ) => {
@@ -421,7 +424,7 @@ describe('CopilotCLISession', () => {
421424
422425 it ( 'requires read permission outside workspace and working directory' , async ( ) => {
423426 let result : unknown ;
424- let askedForPermission : PermissionRequest | undefined = undefined ;
427+ toolsService . setConfirmationResult ( 'no' ) ;
425428 sdkSession . send = async ( { prompt } : any ) => {
426429 sdkSession . emit ( 'assistant.turn_start' , { } ) ;
427430 sdkSession . emit ( 'assistant.message' , { content : `Echo: ${ prompt } ` } ) ;
@@ -434,25 +437,20 @@ describe('CopilotCLISession', () => {
434437 const stream = new MockChatResponseStream ( ) ;
435438 session . attachStream ( stream ) ;
436439
437- disposables . add ( session . attachPermissionHandler ( ( permission ) => {
438- askedForPermission = permission ;
439- return Promise . resolve ( false ) ;
440- } ) ) ;
441-
442440 // Path must be absolute within workspace, should auto-approve
443441 await session . handleRequest ( { id : '' , toolInvocationToken : undefined as never } , { prompt : 'Test' } , [ ] , undefined , authInfo , CancellationToken . None ) ;
444- const file = path . join ( '/workingDirectory' , 'file.ts' ) ;
445442 expect ( result ) . toEqual ( { kind : 'denied-interactively-by-user' } ) ;
446- expect ( askedForPermission ) . not . toBeUndefined ( ) ;
447- expect ( askedForPermission ! . kind ) . toBe ( 'read' ) ;
448- expect ( ( askedForPermission as unknown as { path : string } ) ! . path ) . toBe ( file ) ;
443+ expect ( toolsService . invokeToolCalls ) . toHaveLength ( 1 ) ;
444+ expect ( toolsService . invokeToolCalls [ 0 ] . input ) . toMatchObject ( {
445+ title : 'Read file(s)' ,
446+ message : 'Read file'
447+ } ) ;
449448 } ) ;
450449
451450 it ( 'approves write permission when handler returns true' , async ( ) => {
452451 let result : unknown ;
453452 const session = await createSession ( ) ;
454- // Register approval handler
455- disposables . add ( session . attachPermissionHandler ( async ( ) => true ) ) ;
453+ toolsService . setConfirmationResult ( 'yes' ) ;
456454 sdkSession . send = async ( { prompt } : any ) => {
457455 sdkSession . emit ( 'assistant.turn_start' , { } ) ;
458456 sdkSession . emit ( 'assistant.message' , { content : `Echo: ${ prompt } ` } ) ;
@@ -471,7 +469,7 @@ describe('CopilotCLISession', () => {
471469 it ( 'denies write permission when handler returns false' , async ( ) => {
472470 let result : unknown ;
473471 const session = await createSession ( ) ;
474- session . attachPermissionHandler ( async ( ) => false ) ;
472+ toolsService . setConfirmationResult ( 'no' ) ;
475473 sdkSession . send = async ( { prompt } : any ) => {
476474 sdkSession . emit ( 'assistant.turn_start' , { } ) ;
477475 sdkSession . emit ( 'assistant.message' , { content : `Echo: ${ prompt } ` } ) ;
@@ -489,7 +487,9 @@ describe('CopilotCLISession', () => {
489487 it ( 'denies write permission when handler throws' , async ( ) => {
490488 let result : unknown ;
491489 const session = await createSession ( ) ;
492- session . attachPermissionHandler ( async ( ) => { throw new Error ( 'oops' ) ; } ) ;
490+ toolsService . invokeTool = vi . fn ( async ( ) => {
491+ throw new Error ( 'oops' ) ;
492+ } ) ;
493493 sdkSession . send = async ( { prompt } : any ) => {
494494 sdkSession . emit ( 'assistant.turn_start' , { } ) ;
495495 sdkSession . emit ( 'assistant.message' , { content : `Echo: ${ prompt } ` } ) ;
@@ -509,7 +509,7 @@ describe('CopilotCLISession', () => {
509509 let resolveSend : ( ) => void ;
510510 sdkSession . send = async ( ) => new Promise < void > ( r => { resolveSend = r ; } ) ;
511511 const session = await createSession ( ) ;
512- session . attachPermissionHandler ( async ( ) => true ) ;
512+ toolsService . setConfirmationResult ( 'yes' ) ;
513513 const stream = new MockChatResponseStream ( ) ;
514514 session . attachStream ( stream ) ;
515515 // Spy on trackEdit to capture ordering (we don't want to depend on externalEdit mechanics here)
@@ -578,7 +578,7 @@ describe('CopilotCLISession', () => {
578578 const pushedParts : unknown [ ] = [ ] ;
579579 const stream = new MockChatResponseStream ( part => pushedParts . push ( part ) ) ;
580580 session . attachStream ( stream ) ;
581- disposables . add ( session . attachPermissionHandler ( async ( ) => true ) ) ;
581+ toolsService . setConfirmationResult ( 'yes' ) ;
582582
583583 const requestPromise = session . handleRequest ( { id : '' , toolInvocationToken : undefined as never } , { prompt : 'Run bash' } , [ ] , undefined , authInfo , CancellationToken . None ) ;
584584 await new Promise ( r => setTimeout ( r , 0 ) ) ;
@@ -664,6 +664,19 @@ describe('CopilotCLISession', () => {
664664 await requestPromise ;
665665 } ) ;
666666
667+ describe ( '/compact command' , ( ) => {
668+ it ( 'compacts the conversation and reports success' , async ( ) => {
669+ const session = await createSession ( ) ;
670+ const stream = new MockChatResponseStream ( ) ;
671+ session . attachStream ( stream ) ;
672+
673+ await session . handleRequest ( { id : '' , toolInvocationToken : undefined as never } , { command : 'compact' } , [ ] , undefined , authInfo , CancellationToken . None ) ;
674+
675+ expect ( sdkSession . currentMode ) . toBe ( 'interactive' ) ;
676+ expect ( stream . output . join ( '\n' ) ) . toContain ( 'Compacted conversation.' ) ;
677+ } ) ;
678+ } ) ;
679+
667680 describe ( '/mcp command' , ( ) => {
668681 it ( 'shows no servers message when no MCP tools are loaded' , async ( ) => {
669682 sdkSession . toolMetadata = [ ] ;
@@ -699,6 +712,54 @@ describe('CopilotCLISession', () => {
699712 } ) ;
700713
701714 describe ( 'steering (sending messages to a busy session)' , ( ) => {
715+ it ( 'allows steering after an earlier failed request' , async ( ) => {
716+ sdkSession . send = async ( ) => {
717+ throw new Error ( 'boom' ) ;
718+ } ;
719+
720+ const session = await createSession ( ) ;
721+ const stream = new MockChatResponseStream ( ) ;
722+ session . attachStream ( stream ) ;
723+
724+ await session . handleRequest (
725+ { id : 'req-1' , toolInvocationToken : undefined as never } ,
726+ { prompt : 'Initial failure' } , [ ] , undefined , authInfo , CancellationToken . None
727+ ) ;
728+ expect ( session . status ) . toBe ( ChatSessionStatus . Failed ) ;
729+
730+ let resolveSecondSend ! : ( ) => void ;
731+ let sendCallCount = 0 ;
732+ sdkSession . send = async ( options : any ) => {
733+ sendCallCount ++ ;
734+ sdkSession . lastSendOptions = options ;
735+ if ( sendCallCount === 1 ) {
736+ await new Promise < void > ( r => { resolveSecondSend = r ; } ) ;
737+ }
738+ sdkSession . emit ( 'assistant.turn_start' , { } ) ;
739+ sdkSession . emit ( 'assistant.message' , { content : `Echo: ${ options . prompt } ` } ) ;
740+ sdkSession . emit ( 'assistant.turn_end' , { } ) ;
741+ } ;
742+
743+ const secondRequest = session . handleRequest (
744+ { id : 'req-2' , toolInvocationToken : undefined as never } ,
745+ { prompt : 'Second request' } , [ ] , undefined , authInfo , CancellationToken . None
746+ ) ;
747+ await new Promise ( r => setTimeout ( r , 10 ) ) ;
748+
749+ const steeringRequest = session . handleRequest (
750+ { id : 'req-3' , toolInvocationToken : undefined as never } ,
751+ { prompt : 'Steer after failure' } , [ ] , undefined , authInfo , CancellationToken . None
752+ ) ;
753+ await new Promise ( r => setTimeout ( r , 10 ) ) ;
754+
755+ expect ( sdkSession . lastSendOptions ?. mode ) . toBe ( 'immediate' ) ;
756+ expect ( sdkSession . lastSendOptions ?. prompt ) . toBe ( 'Steer after failure' ) ;
757+
758+ resolveSecondSend ( ) ;
759+ await Promise . all ( [ secondRequest , steeringRequest ] ) ;
760+ expect ( session . status ) . toBe ( ChatSessionStatus . Completed ) ;
761+ } ) ;
762+
702763 it ( 'routes through steering when session is already InProgress' , async ( ) => {
703764 // Arrange: make `send` block so the first request stays in progress
704765 let resolveFirstSend : ( ) => void = ( ) => { } ;
0 commit comments