From 90e521413b42881e7e4a0df0fe62b862b5c4e057 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 7 Apr 2026 14:40:23 +0100 Subject: [PATCH] fix: resolve .NET E2E race conditions in cancellation and multi-client tests SessionTests: SendAndWait_Throws_OperationCanceledException_When_Token_Cancelled did not wait for the agent to go idle after cancellation, leaking proxy requests into subsequent tests (causing Customized_SystemMessage_Config to fail). MultiClientTests: One_Client_Approves_Permission_And_Both_See_The_Result asserted PermissionCompletedEvent on client2 immediately after session1 went idle, but event propagation to client2 is async and may not have arrived yet. --- dotnet/test/MultiClientTests.cs | 5 +++++ dotnet/test/SessionTests.cs | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/dotnet/test/MultiClientTests.cs b/dotnet/test/MultiClientTests.cs index bdd264a4a5..0f12a3cec1 100644 --- a/dotnet/test/MultiClientTests.cs +++ b/dotnet/test/MultiClientTests.cs @@ -170,6 +170,9 @@ public async Task One_Client_Approves_Permission_And_Both_See_The_Result() var client1Events = new ConcurrentBag(); var client2Events = new ConcurrentBag(); + // Wait for PermissionCompletedEvent on client2 which may arrive slightly after session1 goes idle + var client2PermissionCompleted = TestHelper.GetNextEventOfTypeAsync(session2); + using var sub1 = session1.On(evt => client1Events.Add(evt)); using var sub2 = session2.On(evt => client2Events.Add(evt)); @@ -181,6 +184,8 @@ public async Task One_Client_Approves_Permission_And_Both_See_The_Result() Assert.NotNull(response); Assert.NotEmpty(client1PermissionRequests); + await client2PermissionCompleted; + Assert.Contains(client1Events, e => e is PermissionRequestedEvent); Assert.Contains(client2Events, e => e is PermissionRequestedEvent); Assert.Contains(client1Events, e => e is PermissionCompletedEvent); diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index d0084c62ee..9bd03f186b 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -432,11 +432,18 @@ public async Task SendAndWait_Throws_On_Timeout() { var session = await CreateSessionAsync(); + var sessionIdleTask = TestHelper.GetNextEventOfTypeAsync(session); + // Use a slow command to ensure timeout triggers before completion var ex = await Assert.ThrowsAsync(() => session.SendAndWaitAsync(new MessageOptions { Prompt = "Run 'sleep 2 && echo done'" }, TimeSpan.FromMilliseconds(100))); Assert.Contains("timed out", ex.Message); + + // The timeout only cancels the client-side wait; abort the agent and wait for idle + // so leftover requests don't leak into subsequent tests. + await session.AbortAsync(); + await sessionIdleTask; } [Fact] @@ -446,6 +453,7 @@ public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Cance // Set up wait for tool execution to start BEFORE sending var toolStartTask = TestHelper.GetNextEventOfTypeAsync(session); + var sessionIdleTask = TestHelper.GetNextEventOfTypeAsync(session); using var cts = new CancellationTokenSource(); @@ -461,6 +469,12 @@ public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Cance cts.Cancel(); await Assert.ThrowsAnyAsync(() => sendTask); + + // Cancelling the token only cancels the client-side wait, not the server-side agent loop. + // Explicitly abort so the agent stops, then wait for idle to ensure we're not still + // running this agent's operations in the context of a subsequent test. + await session.AbortAsync(); + await sessionIdleTask; } [Fact]