From d7bdc300362bf80146df4ce769f2038601cd3d11 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 6 May 2026 23:13:16 -0400 Subject: [PATCH 1/2] Fix E2E event capture race Use a thread-safe collection for SessionE2ETests event type capture so assertions do not enumerate a List while background event delivery appends to it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/E2E/SessionE2ETests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index 50b4dc1f5..15aa3543d 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -5,6 +5,7 @@ using GitHub.Copilot.SDK.Test.Harness; using GitHub.Copilot.SDK.Rpc; using Microsoft.Extensions.AI; +using System.Collections.Concurrent; using System.ComponentModel; using Xunit; using Xunit.Abstractions; @@ -401,9 +402,9 @@ public async Task Send_Returns_Immediately_While_Events_Stream_In_Background() { OnPermissionRequest = PermissionHandler.ApproveAll, }); - var events = new List(); + var events = new ConcurrentQueue(); - session.On(evt => events.Add(evt.Type)); + session.On(evt => events.Enqueue(evt.Type)); // Use a slow command so we can verify SendAsync() returns before completion await session.SendAsync(new MessageOptions { Prompt = "Run 'sleep 2 && echo done'" }); @@ -423,9 +424,9 @@ public async Task Send_Returns_Immediately_While_Events_Stream_In_Background() public async Task SendAndWait_Blocks_Until_Session_Idle_And_Returns_Final_Assistant_Message() { var session = await CreateSessionAsync(); - var events = new List(); + var events = new ConcurrentQueue(); - session.On(evt => events.Add(evt.Type)); + session.On(evt => events.Enqueue(evt.Type)); var response = await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 2+2?" }); From 92eb62fd607120957ddaddaf5af68e57339cdc03 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 6 May 2026 23:29:04 -0400 Subject: [PATCH 2/2] Avoid shell E2E workspace cleanup race Run long-lived shell edge-case commands from the system temp directory so killed or timed-out child processes cannot keep the fixture workspace directory locked on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs b/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs index 13bea7ae4..8b9126b8b 100644 --- a/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs +++ b/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs @@ -35,7 +35,9 @@ public async Task Shell_Exec_With_Timeout_Kills_Long_Running_Command() ? $"echo started>\"{startedPath}\" & for /L %i in (1,1,2147483647) do @rem & echo should-not-exist>\"{markerPath}\"" : $"printf 'started' > '{startedPath}'; sleep 30; printf 'should-not-exist' > '{markerPath}'"; - var result = await session.Rpc.Shell.ExecAsync(command, timeout: TimeSpan.FromMilliseconds(200)); + // On Windows, terminating the shell wrapper can briefly leave children alive. + // Keep this long-running command outside the fixture workspace so cleanup is not blocked by cwd handles. + var result = await session.Rpc.Shell.ExecAsync(command, cwd: Path.GetTempPath(), timeout: TimeSpan.FromMilliseconds(200)); Assert.False(string.IsNullOrWhiteSpace(result.ProcessId)); await TestHelper.WaitForConditionAsync( @@ -120,7 +122,9 @@ public async Task Shell_Kill_Cleans_Up_After_Terminating_Signal(ShellKillSignal ? "powershell -NoLogo -NoProfile -Command \"Start-Sleep -Seconds 60\"" : "sleep 60"; - var execResult = await session.Rpc.Shell.ExecAsync(command); + // On Windows, terminating the shell wrapper can briefly leave grandchildren alive. + // Keep this command outside the fixture workspace so cleanup is not blocked by cwd handles. + var execResult = await session.Rpc.Shell.ExecAsync(command, cwd: Path.GetTempPath()); Assert.False(string.IsNullOrWhiteSpace(execResult.ProcessId)); var killResult = await session.Rpc.Shell.KillAsync(execResult.ProcessId, signal);