Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/instructions/dotnet-e2e.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
applyTo: "dotnet/test/E2E/**/*.cs"
---

# .NET E2E test instructions

- Create and resume sessions through `E2ETestContext` using `Ctx.CreateSessionAsync` and `Ctx.ResumeSessionAsync`. Do not call these methods directly on `CopilotClient`; the context applies the backend selected by the E2E matrix while preserving providers explicitly configured by the test.
- Create clients with `Ctx.CreateClient` so they receive the harness environment, CLI path, authentication defaults, transport handling, and lifecycle tracking.
- Instantiate `CopilotClient` directly only when client construction, startup, shutdown, or disposal is the behavior under test. Keep cleanup explicit in those tests, and still create any sessions through `Ctx` so they participate in backend coverage.
27 changes: 24 additions & 3 deletions .github/workflows/dotnet-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,35 @@ permissions:

jobs:
test:
name: ".NET SDK Tests (${{ matrix.os }}, ${{ matrix.transport }})"
name: ".NET SDK Tests (${{ matrix.os }}, ${{ matrix.transport }}, ${{ matrix.backend }})"
if: github.event.repository.fork == false
env:
POWERSHELL_UPDATECHECK: Off
COPILOT_SDK_E2E_BACKEND: ${{ matrix.backend }}
DOTNET_TEST_FILTER: ${{ matrix.test-filter }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
transport: ["default", "inprocess"]
# TODO: Re-enable after fixing in-process sqlite file locking on shutdown on Windows
backend: [capi]
# TODO: Re-enable after fixing in-process sqlite file locking on shutdown on Windows.
exclude:
- os: windows-latest
transport: "inprocess"
include:
- os: ubuntu-latest
transport: inprocess
backend: anthropic-messages
test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly"
- os: ubuntu-latest
transport: inprocess
backend: openai-responses
test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly"
- os: ubuntu-latest
transport: inprocess
backend: openai-completions
test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly"
runs-on: ${{ matrix.os }}
defaults:
run:
Expand Down Expand Up @@ -92,4 +108,9 @@ jobs:
- name: Run .NET SDK tests
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: dotnet test --no-build -v n
run: |
args=(--no-build -v n)
if [[ -n "$DOTNET_TEST_FILTER" ]]; then
args+=(--filter "$DOTNET_TEST_FILTER")
fi
dotnet test "${args[@]}"
2 changes: 1 addition & 1 deletion dotnet/test/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Xunit;
using GitHub.Copilot.Test.Harness;

// Each E2E test class fixture spins up its own Copilot CLI subprocess plus a CapiProxy
// Each E2E test class fixture spins up its own Copilot CLI subprocess plus a ReplayProxy
// (replaying HTTP proxy) Node.js subprocess. With ~25 test classes, running them in parallel
// would launch ~50 long-lived Node.js processes simultaneously and exhaust both file
// descriptors and memory on developer machines and CI runners (especially Windows). Tests
Expand Down
1 change: 1 addition & 0 deletions dotnet/test/E2E/ByokBearerTokenProviderE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ namespace GitHub.Copilot.Test.E2E;
/// and the resulting token reaches that provider's endpoint.</item>
/// </list>
/// </remarks>
[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)]
public class ByokBearerTokenProviderE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
: E2ETestBase(fixture, "byok_bearer_token_provider", output)
{
Expand Down
16 changes: 9 additions & 7 deletions dotnet/test/E2E/ClientE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ namespace GitHub.Copilot.Test.E2E;

// These tests bypass E2ETestBase because they are about how the CLI subprocess is started
// Other test classes should instead inherit from E2ETestBase
public class ClientE2ETests
public class ClientE2ETests(E2ETestFixture fixture) : IClassFixture<E2ETestFixture>
{
private E2ETestContext Ctx => fixture.Ctx;

[Theory]
[InlineData(true)] // stdio transport
[InlineData(false)] // TCP transport
Expand Down Expand Up @@ -66,7 +68,7 @@ public async Task Should_Force_Stop_Without_Cleanup(bool useStdio)
{
using var client = new CopilotClient(new CopilotClientOptions { Connection = useStdio ? RuntimeConnection.ForStdio() : RuntimeConnection.ForTcp() });

await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
await client.ForceStopAsync();
}

Expand Down Expand Up @@ -165,7 +167,7 @@ public async Task Should_List_Models_When_Authenticated(bool useStdio)
public async Task Should_Not_Throw_When_Disposing_Session_After_Stopping_Client(bool useStdio)
{
await using var client = new CopilotClient(new CopilotClientOptions { Connection = useStdio ? RuntimeConnection.ForStdio() : RuntimeConnection.ForTcp() });
await using var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });

await client.StopAsync();
}
Expand Down Expand Up @@ -201,7 +203,7 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start(bool u
// Verify subsequent calls also fail (don't hang)
var ex2 = await Assert.ThrowsAnyAsync<Exception>(async () =>
{
var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
await session.SendAsync(new MessageOptions { Prompt = "test" });
});
Assert.True(
Expand All @@ -219,7 +221,7 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start(bool u
public async Task Should_Allow_CreateSession_Called_Without_PermissionHandler(bool useStdio)
{
await using var client = new CopilotClient(new CopilotClientOptions { Connection = useStdio ? RuntimeConnection.ForStdio() : RuntimeConnection.ForTcp() });
await using var session = await client.CreateSessionAsync(new SessionConfig());
await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig());

Assert.NotNull(session.SessionId);
}
Expand All @@ -234,7 +236,7 @@ public async Task Should_Allow_ResumeSession_Called_Without_PermissionHandler()
{
Connection = RuntimeConnection.ForTcp(connectionToken: connectionToken),
});
await using var originalSession = await client.CreateSessionAsync(new SessionConfig());
await using var originalSession = await ctx.CreateSessionAsync(client, new SessionConfig());

var port = client.RuntimePort
?? throw new InvalidOperationException("Client must be using TCP transport to support multi-client resume.");
Expand All @@ -243,7 +245,7 @@ public async Task Should_Allow_ResumeSession_Called_Without_PermissionHandler()
{
Connection = RuntimeConnection.ForUri($"localhost:{port}", connectionToken: connectionToken),
});
await using var resumedSession = await resumeClient.ResumeSessionAsync(originalSession.SessionId, new());
await using var resumedSession = await ctx.ResumeSessionAsync(resumeClient, originalSession.SessionId, new());

Assert.Equal(originalSession.SessionId, resumedSession.SessionId);
}
Expand Down
29 changes: 16 additions & 13 deletions dotnet/test/E2E/ClientOptionsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using GitHub.Copilot.Test.Harness;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -41,7 +42,7 @@ public async Task Should_Use_Client_Cwd_For_Default_WorkingDirectory()
WorkingDirectory = clientCwd,
});

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand Down Expand Up @@ -110,7 +111,7 @@ public async Task Should_Propagate_Process_Options_To_Spawned_Cli()
Assert.Equal("dotnet-sdk-e2e", capturedEnv.GetProperty("COPILOT_OTEL_SOURCE_NAME").GetString());
Assert.Equal("true", capturedEnv.GetProperty("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT").GetString());

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
EnableConfigDiscovery = true,
IncludeSubAgentStreamingEvents = false,
Expand Down Expand Up @@ -139,7 +140,7 @@ public async Task Should_Forward_EnableSessionTelemetry_In_Wire_Request()
await client.StartAsync();

// When explicitly set to false, it should appear in the wire request
var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
EnableSessionTelemetry = false,
OnPermissionRequest = PermissionHandler.ApproveAll,
Expand All @@ -166,7 +167,7 @@ public async Task Should_Omit_EnableSessionTelemetry_When_Not_Set()
await client.StartAsync();

// When omitted (null/default), the field should not be present in the wire request
var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand All @@ -191,7 +192,7 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Create_Wire_Req

await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
SkipEmbeddingRetrieval = false,
OrganizationCustomInstructions = "Follow org policy.",
Expand Down Expand Up @@ -219,6 +220,7 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Create_Wire_Req
}

[Fact]
[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)]
public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
Expand All @@ -232,7 +234,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request

await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
ClientName = "advanced-create-client",
Model = "claude-sonnet-4.5",
Expand Down Expand Up @@ -366,6 +368,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request
}

[Fact]
[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)]
public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
Expand All @@ -378,7 +381,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques

await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
Model = "claude-sonnet-4.5",
Provider = new ProviderConfig
Expand Down Expand Up @@ -432,7 +435,7 @@ public async Task Should_Apply_Empty_Mode_Defaults_To_CreateSession_Wire_Request

await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated),
Expand Down Expand Up @@ -471,7 +474,7 @@ public async Task Should_Propagate_Activity_TraceContext_To_Session_Create_And_S
activity.TraceStateString = "vendor=create-send";
activity.Start();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand Down Expand Up @@ -555,7 +558,7 @@ public async Task Should_Propagate_Activity_TraceContext_To_Session_Resume()
activity.TraceStateString = "vendor=resume";
activity.Start();

var session = await client.ResumeSessionAsync("trace-resume-session", new ResumeSessionConfig
var session = await Ctx.ResumeSessionAsync(client, "trace-resume-session", new ResumeSessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand All @@ -582,7 +585,7 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Resume_Wire_Req

await client.StartAsync();

var session = await client.ResumeSessionAsync("resume-session", new ResumeSessionConfig
var session = await Ctx.ResumeSessionAsync(client, "resume-session", new ResumeSessionConfig
{
SkipEmbeddingRetrieval = false,
OrganizationCustomInstructions = "Resume org policy.",
Expand Down Expand Up @@ -624,7 +627,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Resume_Wire_Request

await client.StartAsync();

var session = await client.ResumeSessionAsync("advanced-resume-session", new ResumeSessionConfig
var session = await Ctx.ResumeSessionAsync(client, "advanced-resume-session", new ResumeSessionConfig
{
ClientName = "advanced-resume-client",
Model = "claude-haiku-4.5",
Expand Down Expand Up @@ -706,7 +709,7 @@ public async Task Should_Apply_Empty_Mode_Defaults_To_ResumeSession_Wire_Request

await client.StartAsync();

var session = await client.ResumeSessionAsync("resume-empty-session", new ResumeSessionConfig
var session = await Ctx.ResumeSessionAsync(client, "resume-empty-session", new ResumeSessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated),
Expand Down
4 changes: 2 additions & 2 deletions dotnet/test/E2E/CopilotRequestCancelErrorE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public async Task Reports_A_Thrown_Callback_Error_Instead_Of_Hanging()
await using var client = CreateClientWith(handler);
await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand All @@ -81,7 +81,7 @@ public async Task Observes_Runtime_Cancellation_Of_An_In_Flight_Inference_Reques
await using var client = CreateClientWith(handler);
await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand Down
6 changes: 4 additions & 2 deletions dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ private CopilotClient CreateClientWith(RecordingRequestHandler provider) =>
});

[Fact]
[Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)]
public async Task Threads_The_Session_Id_Into_A_Capi_Session_Inference_Request()
{
var provider = new RecordingRequestHandler();
await using var client = CreateClientWith(provider);
await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand Down Expand Up @@ -64,13 +65,14 @@ public async Task Threads_The_Session_Id_Into_A_Capi_Session_Inference_Request()
}

[Fact]
[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)]
public async Task Threads_The_Session_Id_Into_A_Byok_Session_Inference_Request()
{
var provider = new RecordingRequestHandler();
await using var client = CreateClientWith(provider);
await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
// BYOK providers require an explicit model id.
Expand Down
3 changes: 2 additions & 1 deletion dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace GitHub.Copilot.Test.E2E;
/// message. Without the eager start the turn never completes and this test
/// times out.
/// </remarks>
[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)]
public class CopilotRequestWebSocketE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
: E2ETestBase(fixture, "copilot_request_websocket", output)
{
Expand All @@ -54,7 +55,7 @@ public async Task Services_A_WebSocket_Turn_End_To_End_Via_The_Request_Handler()
}, environment: env);
await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand Down
5 changes: 4 additions & 1 deletion dotnet/test/E2E/GitHubTelemetryForwardingE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ namespace GitHub.Copilot.Test.E2E;

#pragma warning disable GHCP001 // GitHub telemetry forwarding is experimental.

// TODO(BYOK): Anthropic Messages produced no GitHub telemetry notification. Determine whether
// provider-backed sessions should forward the same telemetry before keeping this CAPI-only.
[Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)]
public class GitHubTelemetryForwardingE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
: E2ETestBase(fixture, "github_telemetry", output)
{
Expand All @@ -32,7 +35,7 @@ public async Task Should_Forward_GitHub_Telemetry_For_A_Live_Session()
CopilotSession? session = null;
try
{
session = await client.CreateSessionAsync(new SessionConfig
session = await Ctx.CreateSessionAsync(client, new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
Expand Down
Loading
Loading