@@ -587,6 +587,83 @@ public async Task SendAndWaitAsync_DroppedIdle_Fallback_Flushes_Event_Enqueued_D
587587 }
588588 }
589589
590+ [ Fact ]
591+ public async Task SendAndWaitAsync_DroppedIdle_Fallback_Serializes_Concurrent_Enqueue_With_Final_Barrier ( )
592+ {
593+ await using var server = await FakeCopilotServer . StartAsync ( ) ;
594+ server . ConfigureEventEnqueueDuringFinalBarrier ( ) ;
595+ await using var client = new CopilotClient ( new CopilotClientOptions { Connection = RuntimeConnection . ForUri ( server . Url ) } ) ;
596+ await using var session = await client . CreateSessionAsync ( new SessionConfig
597+ {
598+ OnPermissionRequest = PermissionHandler . ApproveAll
599+ } ) ;
600+
601+ var dispatchGate = typeof ( CopilotSession ) . GetField ( "_eventDispatchGate" , BindingFlags . Instance | BindingFlags . NonPublic )
602+ ? . GetValue ( session )
603+ ?? throw new InvalidOperationException ( "Event dispatch synchronization gate was not found." ) ;
604+ var enqueueStarted = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
605+ var releaseEnqueue = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
606+ var queuedHandlerStarted = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
607+ var releaseQueuedHandler = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
608+ Task ? enqueueTask = null ;
609+ using var subscription = session . On < AssistantTurnEndEvent > ( evt =>
610+ {
611+ if ( evt . Data . TurnId == "activity-reactivation-barrier" )
612+ {
613+ lock ( dispatchGate )
614+ {
615+ var dispatchAttempted = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
616+ var task = Task . Run ( ( ) =>
617+ {
618+ dispatchAttempted . TrySetResult ( ) ;
619+ DispatchEvent ( session , new AssistantTurnEndEvent
620+ {
621+ Data = new AssistantTurnEndData { TurnId = "queued-after-concurrent-enqueue" }
622+ } ) ;
623+ } ) ;
624+ enqueueTask = task ;
625+ dispatchAttempted . Task . GetAwaiter ( ) . GetResult ( ) ;
626+ enqueueStarted . TrySetResult ( ) ;
627+ releaseEnqueue . Task . GetAwaiter ( ) . GetResult ( ) ;
628+ }
629+ }
630+ else if ( evt . Data . TurnId == "queued-after-concurrent-enqueue" )
631+ {
632+ queuedHandlerStarted . TrySetResult ( ) ;
633+ releaseQueuedHandler . Task . GetAwaiter ( ) . GetResult ( ) ;
634+ }
635+ } ) ;
636+
637+ var completionTask = session . SendAndWaitAsync (
638+ new MessageOptions { Prompt = "serialize a concurrent enqueue with the final barrier" } ,
639+ timeout : TimeSpan . FromSeconds ( 5 ) ) ;
640+
641+ try
642+ {
643+ await enqueueStarted . Task . WaitAsync ( TimeSpan . FromSeconds ( 2 ) ) ;
644+ await Task . Delay ( 200 ) ;
645+ Assert . False ( enqueueTask ! . IsCompleted , "DispatchEvent must be blocked by the shared enqueue/barrier gate." ) ;
646+ Assert . False ( completionTask . IsCompleted , "Completion must not cross an event enqueue that is contending with the final barrier." ) ;
647+
648+ releaseEnqueue . TrySetResult ( ) ;
649+ await enqueueTask ! . WaitAsync ( TimeSpan . FromSeconds ( 2 ) ) ;
650+ var first = await Task . WhenAny ( queuedHandlerStarted . Task , completionTask )
651+ . WaitAsync ( TimeSpan . FromSeconds ( 2 ) ) ;
652+ Assert . Same ( queuedHandlerStarted . Task , first ) ;
653+ Assert . False ( completionTask . IsCompleted , "Completion must wait for the in-flight event's handler to cross the FIFO barrier." ) ;
654+
655+ releaseQueuedHandler . TrySetResult ( ) ;
656+ var response = await completionTask ;
657+ Assert . Equal ( "completed response" , response ? . Data . Content ) ;
658+ }
659+ finally
660+ {
661+ releaseEnqueue . TrySetResult ( ) ;
662+ releaseQueuedHandler . TrySetResult ( ) ;
663+ }
664+ }
665+
666+
590667 [ MethodImpl ( MethodImplOptions . NoInlining ) ]
591668 private static async Task < WeakReference < CopilotSession > > CreateDroppedSessionAsync ( CopilotClient client )
592669 {
@@ -800,6 +877,7 @@ public void ConfigureEventEnqueueDuringFinalBarrier()
800877 _enqueueDuringFinalBarrier = true ;
801878 }
802879
880+
803881 public void CompleteReactivatedActivity ( )
804882 {
805883 Volatile . Write ( ref _hasActiveWork , false ) ;
0 commit comments