-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand SDK E2E runtime coverage #1197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+9,453
−969
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
890077e
Expand C# E2E runtime coverage
stephentoub 7def24d
Port E2E coverage across SDKs
stephentoub 71fb19f
Address PR review feedback
stephentoub abed43b
Address path composition review feedback
stephentoub 5bc60bb
Fix CI formatting checks
stephentoub b881d76
Fix cross-platform E2E CI failures
stephentoub 8d5ce43
Fix Go session event race
stephentoub e63a9d9
Fix remaining E2E CI failures
stephentoub 7f269f1
Align legacy connect fallback across SDKs
stephentoub 34c57a1
Harden Go pending-work resume false-path test
stephentoub 3d60893
Harden denied tool-result E2E completion
stephentoub 3d4b7f8
Harden streaming abort recovery test
stephentoub 669b4aa
Harden Go streaming abort recovery test
stephentoub 549be42
Harden pending resume and Node E2E tests
stephentoub 08d6167
Restore shared client API snapshot prompt
stephentoub e502b13
Add Node and Go RPC parity E2E coverage
stephentoub File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| using System.ComponentModel; | ||
| using GitHub.Copilot.SDK.Test.Harness; | ||
| using Microsoft.Extensions.AI; | ||
| using Xunit; | ||
| using Xunit.Abstractions; | ||
|
|
||
| namespace GitHub.Copilot.SDK.Test.E2E; | ||
|
|
||
| /// <summary> | ||
| /// Verifies that <see cref="CopilotSession.AbortAsync"/> cleanly interrupts an active | ||
| /// turn — both during streaming and during tool execution — without leaving dangling | ||
| /// state or causing exceptions in the event delivery pipeline. | ||
| /// </summary> | ||
| public class AbortE2ETests(E2ETestFixture fixture, ITestOutputHelper output) | ||
| : E2ETestBase(fixture, "abort", output) | ||
| { | ||
| [Fact] | ||
| public async Task Should_Abort_During_Active_Streaming() | ||
| { | ||
| var session = await CreateSessionAsync(new SessionConfig { Streaming = true }); | ||
|
|
||
| var firstDeltaReceived = new TaskCompletionSource<AssistantMessageDeltaEvent>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
| var allEvents = new List<SessionEvent>(); | ||
|
|
||
| session.On(evt => | ||
| { | ||
| lock (allEvents) { allEvents.Add(evt); } | ||
| if (evt is AssistantMessageDeltaEvent delta) | ||
| { | ||
| firstDeltaReceived.TrySetResult(delta); | ||
| } | ||
| }); | ||
|
|
||
| // Fire-and-forget — we'll abort before it finishes | ||
| _ = session.SendAsync(new MessageOptions | ||
| { | ||
| Prompt = "Write a very long essay about the history of computing, covering every decade from the 1940s to the 2020s in great detail.", | ||
| }); | ||
|
|
||
| // Wait for at least one delta to arrive (proves streaming started) | ||
| var delta = await firstDeltaReceived.Task.WaitAsync(TimeSpan.FromSeconds(60)); | ||
| Assert.False(string.IsNullOrEmpty(delta.Data.DeltaContent)); | ||
|
|
||
| // Now abort mid-stream | ||
| await session.AbortAsync(); | ||
|
|
||
| List<SessionEvent> snapshot; | ||
| lock (allEvents) { snapshot = [.. allEvents]; } | ||
|
|
||
| // No session.idle should have appeared (abort cancels the turn) | ||
| // OR if idle DID appear, it should be after the abort, which is fine | ||
| // The key contract: no exceptions were thrown, and the session is usable afterwards | ||
| var types = snapshot.Select(e => e.Type).ToList(); | ||
| Assert.Contains("assistant.message_delta", types); | ||
|
|
||
| // Session should be usable after abort — verify by listening for the | ||
| // recovery message rather than racing against a late idle from the | ||
| // aborted streaming turn. | ||
| var recoveryReceived = new TaskCompletionSource<AssistantMessageEvent>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
| session.On(evt => | ||
| { | ||
| if (evt is AssistantMessageEvent msg && (msg.Data.Content?.Contains("abort_recovery_ok") == true)) | ||
| { | ||
| recoveryReceived.TrySetResult(msg); | ||
| } | ||
| }); | ||
|
|
||
| await session.SendAsync(new MessageOptions | ||
| { | ||
| Prompt = "Say 'abort_recovery_ok'.", | ||
| }); | ||
|
|
||
| var recoveryMessage = await recoveryReceived.Task.WaitAsync(TimeSpan.FromSeconds(60)); | ||
| Assert.Contains("abort_recovery_ok", recoveryMessage.Data.Content?.ToLowerInvariant() ?? string.Empty); | ||
|
|
||
| await session.DisposeAsync(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Should_Abort_During_Active_Tool_Execution() | ||
| { | ||
| var toolStarted = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
| var releaseTool = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
|
|
||
| var session = await CreateSessionAsync(new SessionConfig | ||
| { | ||
| Tools = [AIFunctionFactory.Create(SlowTool, "slow_analysis")], | ||
| OnPermissionRequest = PermissionHandler.ApproveAll, | ||
| }); | ||
|
|
||
| // Fire-and-forget | ||
| _ = session.SendAsync(new MessageOptions | ||
| { | ||
| Prompt = "Use slow_analysis with value 'test_abort'. Wait for the result.", | ||
| }); | ||
|
|
||
| // Wait for the tool to start executing | ||
| var toolValue = await toolStarted.Task.WaitAsync(TimeSpan.FromSeconds(60)); | ||
| Assert.Equal("test_abort", toolValue); | ||
|
|
||
| // Abort while the tool is running | ||
| await session.AbortAsync(); | ||
|
|
||
| // Release the tool so its task doesn't leak | ||
| releaseTool.TrySetResult("RELEASED_AFTER_ABORT"); | ||
|
|
||
| // Session should be usable after abort — verify by listening for the right event | ||
| var recoveryReceived = new TaskCompletionSource<AssistantMessageEvent>(TaskCreationOptions.RunContinuationsAsynchronously); | ||
| session.On(evt => | ||
| { | ||
| if (evt is AssistantMessageEvent msg && (msg.Data.Content?.Contains("tool_abort_recovery_ok") == true)) | ||
| { | ||
| recoveryReceived.TrySetResult(msg); | ||
| } | ||
| }); | ||
|
|
||
| await session.SendAsync(new MessageOptions | ||
| { | ||
| Prompt = "Say 'tool_abort_recovery_ok'.", | ||
| }); | ||
|
|
||
| var recoveryMessage = await recoveryReceived.Task.WaitAsync(TimeSpan.FromSeconds(60)); | ||
| Assert.Contains("tool_abort_recovery_ok", recoveryMessage.Data.Content?.ToLowerInvariant() ?? string.Empty); | ||
|
|
||
| await session.DisposeAsync(); | ||
|
|
||
| [Description("A slow analysis tool that blocks until released")] | ||
| async Task<string> SlowTool([Description("Value to analyze")] string value) | ||
| { | ||
| toolStarted.TrySetResult(value); | ||
| return await releaseTool.Task; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.