@@ -631,6 +631,154 @@ describe('ACP delegate execution production adapter', () => {
631631 await expect ( execution . reserve ( 1 ) ) . resolves . toHaveProperty ( 'slotIds' )
632632 } )
633633
634+ it ( 'settles cancellation when the terminated provider prompt never returns' , async ( ) => {
635+ const prompt = deferred < PromptResponse > ( )
636+ const cleanup : string [ ] = [ ]
637+ let callbacks ! : AcpDelegateExecutionCallbacks
638+ const execution = createAcpDelegateExecution ( {
639+ capacity : 1 ,
640+ prepare : async ( input ) => ( {
641+ executionId : input . attemptId ,
642+ provenance : {
643+ projectId : input . session . projectId ,
644+ sessionId : input . session . sessionId ,
645+ agentFrameId : input . frameId ,
646+ runtimeSegmentId : input . runtimeSegmentId
647+ } ,
648+ workspace : { cwd : '/workspace/terminated-provider' } ,
649+ runtimeHome : '/runtime/terminated-provider' ,
650+ frameworkId : 'certified-test' ,
651+ capability : {
652+ revoke : async ( ) => {
653+ cleanup . push ( 'revoke' )
654+ }
655+ } ,
656+ disposeResources : async ( ) => {
657+ cleanup . push ( 'resources' )
658+ }
659+ } ) ,
660+ assertFrameworkNativeDelegationDisabled : async ( ) => undefined ,
661+ createRuntime : ( _scope , runtimeCallbacks ) => {
662+ callbacks = runtimeCallbacks
663+ return {
664+ createSession : async ( ) => ( { sessionId : 'provider-terminated' } ) ,
665+ sendAppContinuation : ( ) => {
666+ callbacks . onProviderPromptAccepted ( 'provider-terminated' )
667+ return prompt . promise
668+ } ,
669+ cancelPrompt : async ( ) => undefined ,
670+ respondToPermission : async ( ) => undefined ,
671+ setPermissionProfile : async ( ) => undefined ,
672+ deleteSession : async ( ) => {
673+ cleanup . push ( 'delete' )
674+ } ,
675+ shutdownForQuit : async ( ) => {
676+ cleanup . push ( 'shutdown' )
677+ return { reaped : true }
678+ }
679+ }
680+ }
681+ } )
682+ const reservation = await execution . reserve ( 1 )
683+ const running = execution . run ( makeInput ( 'terminated-provider' ) , reservation . slotIds [ 0 ] )
684+ await running . accepted
685+
686+ await expect ( running . cancel ( ) ) . resolves . toBeUndefined ( )
687+ await expect ( running . completion ) . resolves . toEqual ( { status : 'cancelled' } )
688+ await vi . waitFor ( ( ) => expect ( cleanup ) . toEqual ( [ 'revoke' , 'delete' , 'shutdown' , 'resources' ] ) )
689+ await expect ( execution . reserve ( 1 ) ) . resolves . toHaveProperty ( 'slotIds' )
690+ } )
691+
692+ it ( 'does not start a provider prompt when cancellation lands during deferred Turn begin' , async ( ) => {
693+ const { execution, controls } = makeHarness ( 1 )
694+ const begin = deferred < void > ( )
695+ const reservation = await execution . reserve ( 1 )
696+ const running = execution . run (
697+ {
698+ ...makeInput ( 'deferred-begin' ) ,
699+ turn : {
700+ promptMessageId : 'prompt-deferred-begin' ,
701+ messageBranchId : 'branch-deferred-begin' ,
702+ runtimeSegmentId : 'segment-deferred-begin' ,
703+ begin : ( ) => begin . promise
704+ }
705+ } ,
706+ reservation . slotIds [ 0 ]
707+ )
708+ await vi . waitFor ( ( ) => expect ( controls . has ( 'deferred-begin' ) ) . toBe ( true ) )
709+
710+ await running . cancel ( )
711+ begin . resolve ( )
712+
713+ await expect ( running . accepted ) . rejects . toMatchObject ( {
714+ name : 'DelegateMessagePreAcceptanceError'
715+ } )
716+ await expect ( running . completion ) . resolves . toEqual ( { status : 'cancelled' } )
717+ await vi . waitFor ( ( ) => expect ( controls . get ( 'deferred-begin' ) ?. prompts ) . toEqual ( [ ] ) )
718+ } )
719+
720+ it ( 'reports cleanup failure and retries only the unfinished cleanup step' , async ( ) => {
721+ let revokeAttempts = 0
722+ const cleanup : string [ ] = [ ]
723+ let callbacks ! : AcpDelegateExecutionCallbacks
724+ const execution = createAcpDelegateExecution ( {
725+ capacity : 1 ,
726+ prepare : async ( input ) => ( {
727+ executionId : input . attemptId ,
728+ provenance : {
729+ projectId : input . session . projectId ,
730+ sessionId : input . session . sessionId ,
731+ agentFrameId : input . frameId ,
732+ runtimeSegmentId : input . runtimeSegmentId
733+ } ,
734+ workspace : { cwd : '/workspace/retry-cleanup' } ,
735+ runtimeHome : '/runtime/retry-cleanup' ,
736+ frameworkId : 'certified-test' ,
737+ capability : {
738+ async revoke ( ) {
739+ revokeAttempts += 1
740+ if ( revokeAttempts === 1 ) throw new Error ( 'revoke failed once' )
741+ cleanup . push ( 'revoke' )
742+ }
743+ } ,
744+ disposeResources : async ( ) => {
745+ cleanup . push ( 'resources' )
746+ }
747+ } ) ,
748+ assertFrameworkNativeDelegationDisabled : async ( ) => undefined ,
749+ createRuntime : ( _scope , runtimeCallbacks ) => {
750+ callbacks = runtimeCallbacks
751+ return {
752+ createSession : async ( ) => ( { sessionId : 'provider-retry-cleanup' } ) ,
753+ sendAppContinuation : ( ) => {
754+ callbacks . onProviderPromptAccepted ( 'provider-retry-cleanup' )
755+ return new Promise < PromptResponse > ( ( ) => undefined )
756+ } ,
757+ cancelPrompt : async ( ) => undefined ,
758+ respondToPermission : async ( ) => undefined ,
759+ setPermissionProfile : async ( ) => undefined ,
760+ deleteSession : async ( ) => {
761+ cleanup . push ( 'delete' )
762+ } ,
763+ shutdownForQuit : async ( ) => {
764+ cleanup . push ( 'shutdown' )
765+ return { reaped : true }
766+ }
767+ }
768+ }
769+ } )
770+ const reservation = await execution . reserve ( 1 )
771+ const running = execution . run ( makeInput ( 'retry-cleanup' ) , reservation . slotIds [ 0 ] )
772+ await running . accepted
773+
774+ await expect ( running . cancel ( ) ) . rejects . toThrow ( 'revoke failed once' )
775+ await expect ( running . cancel ( ) ) . resolves . toBeUndefined ( )
776+
777+ expect ( revokeAttempts ) . toBe ( 2 )
778+ expect ( cleanup ) . toEqual ( [ 'revoke' , 'delete' , 'shutdown' , 'resources' ] )
779+ await expect ( execution . reserve ( 1 ) ) . resolves . toHaveProperty ( 'slotIds' )
780+ } )
781+
634782 it ( 'reserves an entire batch atomically and releases terminal slots' , async ( ) => {
635783 const { execution, controls } = makeHarness ( 2 )
636784 const reservation = await execution . reserve ( 2 )
0 commit comments