Skip to content

Commit ebb0eca

Browse files
stephentoubCopilot
andauthored
Improve E2E coverage across SDKs (#1906)
* test(dotnet): improve C# e2e RPC coverage Add C# E2E coverage for previously unexercised generated RPC methods, advanced session option forwarding, runtime provider registration, account/user settings flows, session metadata helpers, and handler callbacks. Fix C# RPC code generation so root collection and dictionary result types are included in the source-generated JSON context. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: expand e2e parity across SDKs Replicate the newly added C# E2E coverage across Node, Python, Go, Rust, and Java, including RPC option forwarding, pending handler paths, MCP OAuth handling, account/user settings flows, and session state extras. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: address e2e review cleanup Resolve review feedback by removing unused/test-only variables, tightening cleanup exception handling, fixing stale comments, and making the Python MCP OAuth wait explicit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: avoid macro-only account token binding Adjust the Rust account lifecycle E2E assertion so CodeQL can see the matched user binding is used outside an assertion macro. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: finish review comment cleanup Make MCP OAuth waits explicit in Python, rewrap Rust OAuth assertions, and avoid CodeQL macro-only bindings in the Rust account lifecycle test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test(java): add extension option parity Add Java session and resume support for requestExtensions, extensionSdkPath, and ExtensionInfo so create/resume requests match the other SDKs. Extend the client options E2E coverage to assert those options are forwarded. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: cover headers refresh none variant Add Go and Java E2E assertions for the MCP headers refresh none response variant so their missing-pending-handler coverage matches Node and .NET. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: add discovery path e2e parity Cover the server discovery-path, agent discovery, and instruction discovery RPCs in the Node, Python, Go, Rust, and Java E2E suites so they match the .NET server RPC coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test(java): add dedicated rpc server e2e coverage Add a dedicated Java RpcServerE2ETest that exercises server-scoped RPC wrappers in parity with the other SDKs. Update Java RPC codegen so server-scoped session methods keep their required sessionId params while session-scoped wrappers continue to inject sessionId automatically. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test: fix e2e CI regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: sort Python e2e imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(java): avoid extension option public surface Remove Java non-generated extension option APIs from the parity coverage so the PR does not expand Java public surface area outside generated RPC APIs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b14b743 commit ebb0eca

52 files changed

Lines changed: 5995 additions & 100 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dotnet/src/Generated/Rpc.cs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/test/E2E/ClientOptionsE2ETests.cs

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,205 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Create_Wire_Req
219219
await session.DisposeAsync();
220220
}
221221

222+
[Fact]
223+
public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request()
224+
{
225+
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
226+
var outputDirectory = Path.Join(Ctx.WorkDir, "large-output-create");
227+
228+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
229+
{
230+
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
231+
UseLoggedInUser = false,
232+
});
233+
234+
await client.StartAsync();
235+
236+
var session = await client.CreateSessionAsync(new SessionConfig
237+
{
238+
ClientName = "advanced-create-client",
239+
Model = "claude-sonnet-4.5",
240+
ReasoningEffort = "medium",
241+
ReasoningSummary = ReasoningSummary.Detailed,
242+
ContextTier = ContextTier.LongContext,
243+
EnableCitations = true,
244+
Capi = new CapiSessionOptions { EnableWebSocketResponses = false },
245+
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
246+
CustomAgents =
247+
[
248+
new CustomAgentConfig
249+
{
250+
Name = "agent-one",
251+
DisplayName = "Agent One",
252+
Description = "Handles agent-one tasks.",
253+
Prompt = "Be agent one.",
254+
Tools = ["view"],
255+
Infer = true,
256+
Skills = ["create-skill"],
257+
Model = "claude-haiku-4.5",
258+
},
259+
],
260+
DefaultAgent = new DefaultAgentConfig { ExcludedTools = ["edit"] },
261+
Agent = "agent-one",
262+
SkillDirectories = ["skills-create"],
263+
DisabledSkills = ["disabled-create-skill"],
264+
PluginDirectories = ["plugins-create"],
265+
InfiniteSessions = new InfiniteSessionConfig
266+
{
267+
Enabled = false,
268+
BackgroundCompactionThreshold = 0.5,
269+
BufferExhaustionThreshold = 0.9,
270+
},
271+
LargeOutput = new LargeToolOutputConfig
272+
{
273+
Enabled = true,
274+
MaxSizeBytes = 4096,
275+
OutputDirectory = outputDirectory,
276+
},
277+
Memory = new MemoryConfiguration { Enabled = true },
278+
GitHubToken = "session-create-token",
279+
RemoteSession = GitHub.Copilot.Rpc.RemoteSessionMode.Export,
280+
Cloud = new CloudSessionOptions
281+
{
282+
Repository = new CloudSessionRepository
283+
{
284+
Owner = "github",
285+
Name = "copilot-sdk",
286+
Branch = "main",
287+
},
288+
},
289+
EnableMcpApps = true,
290+
RequestCanvasRenderer = true,
291+
RequestExtensions = true,
292+
ExtensionSdkPath = "custom-extension-sdk",
293+
ExtensionInfo = new ExtensionInfo { Source = "dotnet-sdk-tests", Name = "advanced-create-extension" },
294+
Canvases =
295+
[
296+
new CanvasDeclaration
297+
{
298+
Id = "advanced-create-canvas",
299+
DisplayName = "Advanced Create Canvas",
300+
Description = "Covers create-time canvas options.",
301+
},
302+
],
303+
Providers =
304+
[
305+
new NamedProviderConfig
306+
{
307+
Name = "create-provider",
308+
Type = "openai",
309+
WireApi = "responses",
310+
BaseUrl = "https://create-provider.example.test/v1",
311+
ApiKey = "create-provider-key",
312+
Headers = new Dictionary<string, string> { ["X-Create-Provider"] = "yes" },
313+
},
314+
],
315+
Models =
316+
[
317+
new ProviderModelConfig
318+
{
319+
Provider = "create-provider",
320+
Id = "create-model",
321+
Name = "Create Model",
322+
ModelId = "claude-sonnet-4.5",
323+
WireModel = "create-wire-model",
324+
MaxContextWindowTokens = 12_000,
325+
MaxPromptTokens = 10_000,
326+
MaxOutputTokens = 2_000,
327+
},
328+
],
329+
OnPermissionRequest = PermissionHandler.ApproveAll,
330+
});
331+
332+
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
333+
var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create");
334+
Assert.Equal("advanced-create-client", createRequest.GetProperty("clientName").GetString());
335+
Assert.Equal("claude-sonnet-4.5", createRequest.GetProperty("model").GetString());
336+
Assert.Equal("medium", createRequest.GetProperty("reasoningEffort").GetString());
337+
Assert.Equal("detailed", createRequest.GetProperty("reasoningSummary").GetString());
338+
Assert.Equal("long_context", createRequest.GetProperty("contextTier").GetString());
339+
Assert.True(createRequest.GetProperty("enableCitations").GetBoolean());
340+
Assert.False(createRequest.GetProperty("capi").GetProperty("enableWebSocketResponses").GetBoolean());
341+
Assert.Equal("persistent", createRequest.GetProperty("mcpOAuthTokenStorage").GetString());
342+
Assert.Equal("agent-one", createRequest.GetProperty("agent").GetString());
343+
Assert.Equal("edit", createRequest.GetProperty("defaultAgent").GetProperty("excludedTools")[0].GetString());
344+
Assert.Equal("agent-one", createRequest.GetProperty("customAgents")[0].GetProperty("name").GetString());
345+
Assert.Equal("plugins-create", createRequest.GetProperty("pluginDirectories")[0].GetString());
346+
Assert.Equal("disabled-create-skill", createRequest.GetProperty("disabledSkills")[0].GetString());
347+
Assert.False(createRequest.GetProperty("infiniteSessions").GetProperty("enabled").GetBoolean());
348+
Assert.True(createRequest.GetProperty("largeOutput").GetProperty("enabled").GetBoolean());
349+
Assert.Equal(4096, createRequest.GetProperty("largeOutput").GetProperty("maxSizeBytes").GetInt64());
350+
Assert.Equal(outputDirectory, createRequest.GetProperty("largeOutput").GetProperty("outputDir").GetString());
351+
Assert.True(createRequest.GetProperty("memory").GetProperty("enabled").GetBoolean());
352+
Assert.Equal("session-create-token", createRequest.GetProperty("gitHubToken").GetString());
353+
Assert.Equal("export", createRequest.GetProperty("remoteSession").GetString());
354+
Assert.Equal("github", createRequest.GetProperty("cloud").GetProperty("repository").GetProperty("owner").GetString());
355+
Assert.True(createRequest.GetProperty("requestMcpApps").GetBoolean());
356+
Assert.True(createRequest.GetProperty("requestCanvasRenderer").GetBoolean());
357+
Assert.True(createRequest.GetProperty("requestExtensions").GetBoolean());
358+
Assert.Equal("custom-extension-sdk", createRequest.GetProperty("extensionSdkPath").GetString());
359+
Assert.Equal("advanced-create-extension", createRequest.GetProperty("extensionInfo").GetProperty("name").GetString());
360+
Assert.Equal("advanced-create-canvas", createRequest.GetProperty("canvases")[0].GetProperty("id").GetString());
361+
Assert.Equal("create-provider", createRequest.GetProperty("providers")[0].GetProperty("name").GetString());
362+
Assert.Equal("responses", createRequest.GetProperty("providers")[0].GetProperty("wireApi").GetString());
363+
Assert.Equal("create-model", createRequest.GetProperty("models")[0].GetProperty("id").GetString());
364+
Assert.Equal(12000, createRequest.GetProperty("models")[0].GetProperty("maxContextWindowTokens").GetInt32());
365+
366+
await session.DisposeAsync();
367+
}
368+
369+
[Fact]
370+
public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Request()
371+
{
372+
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
373+
374+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
375+
{
376+
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
377+
UseLoggedInUser = false,
378+
});
379+
380+
await client.StartAsync();
381+
382+
var session = await client.CreateSessionAsync(new SessionConfig
383+
{
384+
Model = "claude-sonnet-4.5",
385+
Provider = new ProviderConfig
386+
{
387+
Type = "azure",
388+
WireApi = "responses",
389+
Transport = "http",
390+
BaseUrl = "https://azure-provider.example.test/openai",
391+
ApiKey = "provider-api-key",
392+
BearerToken = "provider-bearer-token",
393+
Azure = new AzureOptions { ApiVersion = "2024-02-15-preview" },
394+
Headers = new Dictionary<string, string> { ["X-Provider-Wire"] = "yes" },
395+
ModelId = "claude-sonnet-4.5",
396+
WireModel = "azure-deployment",
397+
MaxPromptTokens = 8192,
398+
MaxOutputTokens = 1024,
399+
},
400+
OnPermissionRequest = PermissionHandler.ApproveAll,
401+
});
402+
403+
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
404+
var provider = GetCapturedRequestParams(capture.RootElement, "session.create").GetProperty("provider");
405+
Assert.Equal("azure", provider.GetProperty("type").GetString());
406+
Assert.Equal("responses", provider.GetProperty("wireApi").GetString());
407+
Assert.Equal("http", provider.GetProperty("transport").GetString());
408+
Assert.Equal("https://azure-provider.example.test/openai", provider.GetProperty("baseUrl").GetString());
409+
Assert.Equal("provider-api-key", provider.GetProperty("apiKey").GetString());
410+
Assert.Equal("provider-bearer-token", provider.GetProperty("bearerToken").GetString());
411+
Assert.Equal("2024-02-15-preview", provider.GetProperty("azure").GetProperty("apiVersion").GetString());
412+
Assert.Equal("yes", provider.GetProperty("headers").GetProperty("X-Provider-Wire").GetString());
413+
Assert.Equal("claude-sonnet-4.5", provider.GetProperty("modelId").GetString());
414+
Assert.Equal("azure-deployment", provider.GetProperty("wireModel").GetString());
415+
Assert.Equal(8192, provider.GetProperty("maxPromptTokens").GetInt32());
416+
Assert.Equal(1024, provider.GetProperty("maxOutputTokens").GetInt32());
417+
418+
await session.DisposeAsync();
419+
}
420+
222421
[Fact]
223422
public async Task Should_Apply_Empty_Mode_Defaults_To_CreateSession_Wire_Request()
224423
{
@@ -411,6 +610,88 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Resume_Wire_Req
411610
await session.DisposeAsync();
412611
}
413612

613+
[Fact]
614+
public async Task Should_Forward_Advanced_Session_Options_In_Resume_Wire_Request()
615+
{
616+
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
617+
var outputDirectory = Path.Join(Ctx.WorkDir, "large-output-resume");
618+
using var canvasInput = JsonDocument.Parse("{\"start\":41}");
619+
620+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
621+
{
622+
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
623+
UseLoggedInUser = false,
624+
});
625+
626+
await client.StartAsync();
627+
628+
var session = await client.ResumeSessionAsync("advanced-resume-session", new ResumeSessionConfig
629+
{
630+
ClientName = "advanced-resume-client",
631+
Model = "claude-haiku-4.5",
632+
ReasoningEffort = "low",
633+
ReasoningSummary = ReasoningSummary.None,
634+
ContextTier = ContextTier.Default,
635+
SuppressResumeEvent = true,
636+
ContinuePendingWork = true,
637+
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
638+
PluginDirectories = ["plugins-resume"],
639+
LargeOutput = new LargeToolOutputConfig
640+
{
641+
Enabled = false,
642+
MaxSizeBytes = 2048,
643+
OutputDirectory = outputDirectory,
644+
},
645+
Memory = new MemoryConfiguration { Enabled = false },
646+
RemoteSession = GitHub.Copilot.Rpc.RemoteSessionMode.On,
647+
OpenCanvases =
648+
[
649+
new GitHub.Copilot.Rpc.OpenCanvasInstance
650+
{
651+
CanvasId = "resume-canvas",
652+
ExtensionId = "dotnet-sdk-tests/resume-extension",
653+
ExtensionName = "Resume Extension",
654+
InstanceId = "resume-canvas-1",
655+
Input = canvasInput.RootElement.Clone(),
656+
Status = "ready",
657+
Title = "Resume Canvas",
658+
Url = "https://example.com/resume-canvas",
659+
},
660+
],
661+
OnPermissionRequest = PermissionHandler.ApproveAll,
662+
});
663+
664+
using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
665+
var resumeRequest = GetCapturedRequestParams(capture.RootElement, "session.resume");
666+
Assert.Equal("advanced-resume-session", resumeRequest.GetProperty("sessionId").GetString());
667+
Assert.Equal("advanced-resume-client", resumeRequest.GetProperty("clientName").GetString());
668+
Assert.Equal("claude-haiku-4.5", resumeRequest.GetProperty("model").GetString());
669+
Assert.Equal("low", resumeRequest.GetProperty("reasoningEffort").GetString());
670+
Assert.Equal("none", resumeRequest.GetProperty("reasoningSummary").GetString());
671+
Assert.Equal("default", resumeRequest.GetProperty("contextTier").GetString());
672+
Assert.True(resumeRequest.GetProperty("disableResume").GetBoolean());
673+
Assert.True(resumeRequest.GetProperty("continuePendingWork").GetBoolean());
674+
Assert.Equal("persistent", resumeRequest.GetProperty("mcpOAuthTokenStorage").GetString());
675+
Assert.Equal("plugins-resume", resumeRequest.GetProperty("pluginDirectories")[0].GetString());
676+
Assert.False(resumeRequest.GetProperty("largeOutput").GetProperty("enabled").GetBoolean());
677+
Assert.Equal(2048, resumeRequest.GetProperty("largeOutput").GetProperty("maxSizeBytes").GetInt64());
678+
Assert.Equal(outputDirectory, resumeRequest.GetProperty("largeOutput").GetProperty("outputDir").GetString());
679+
Assert.False(resumeRequest.GetProperty("memory").GetProperty("enabled").GetBoolean());
680+
Assert.Equal("on", resumeRequest.GetProperty("remoteSession").GetString());
681+
682+
var openCanvas = resumeRequest.GetProperty("openCanvases")[0];
683+
Assert.Equal("resume-canvas", openCanvas.GetProperty("canvasId").GetString());
684+
Assert.Equal("dotnet-sdk-tests/resume-extension", openCanvas.GetProperty("extensionId").GetString());
685+
Assert.Equal("Resume Extension", openCanvas.GetProperty("extensionName").GetString());
686+
Assert.Equal("resume-canvas-1", openCanvas.GetProperty("instanceId").GetString());
687+
Assert.Equal(41, openCanvas.GetProperty("input").GetProperty("start").GetInt32());
688+
Assert.Equal("ready", openCanvas.GetProperty("status").GetString());
689+
Assert.Equal("Resume Canvas", openCanvas.GetProperty("title").GetString());
690+
Assert.Equal("https://example.com/resume-canvas", openCanvas.GetProperty("url").GetString());
691+
692+
await session.DisposeAsync();
693+
}
694+
414695
[Fact]
415696
public async Task Should_Apply_Empty_Mode_Defaults_To_ResumeSession_Wire_Request()
416697
{

dotnet/test/E2E/McpOAuthE2ETests.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,57 @@ public async Task Should_Satisfy_MCP_OAuth_Using_Host_Provided_Token()
7070
Assert.Contains(requests, request => request.Authorization == $"Bearer {ExpectedToken}");
7171
}
7272

73+
[Fact]
74+
public async Task Should_Resolve_Pending_MCP_OAuth_Request_With_Direct_Rpc()
75+
{
76+
await using var oauthServer = await OAuthMcpServer.StartAsync(ExpectedToken);
77+
var serverName = "oauth-direct-rpc-mcp";
78+
var authRequest = new TaskCompletionSource<McpAuthContext>(TaskCreationOptions.RunContinuationsAsynchronously);
79+
var releaseHandler = new TaskCompletionSource<McpAuthResult?>(TaskCreationOptions.RunContinuationsAsynchronously);
80+
81+
await using var session = await CreateSessionAsync(new SessionConfig
82+
{
83+
OnMcpAuthRequest = request =>
84+
{
85+
authRequest.TrySetResult(request);
86+
return releaseHandler.Task;
87+
},
88+
McpServers = new Dictionary<string, McpServerConfig>
89+
{
90+
[serverName] = new McpHttpServerConfig
91+
{
92+
Url = $"{oauthServer.Url}/mcp",
93+
Tools = ["*"],
94+
},
95+
},
96+
});
97+
98+
var connected = WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected);
99+
var request = await authRequest.Task.WaitAsync(TimeSpan.FromSeconds(30));
100+
Assert.NotEmpty(request.RequestId);
101+
Assert.Equal(serverName, request.ServerName);
102+
Assert.Equal($"{oauthServer.Url}/mcp", request.ServerUrl);
103+
Assert.Equal(McpOauthRequestReason.Initial, request.Reason);
104+
Assert.NotNull(request.WwwAuthenticateParams);
105+
Assert.Equal("mcp.read", request.WwwAuthenticateParams!.Scope);
106+
107+
var handled = await session.Rpc.Mcp.Oauth.HandlePendingRequestAsync(
108+
request.RequestId,
109+
new McpOauthPendingRequestResponseToken
110+
{
111+
AccessToken = ExpectedToken,
112+
TokenType = "Bearer",
113+
ExpiresIn = 3600,
114+
});
115+
Assert.True(handled.Success);
116+
117+
await connected;
118+
var tools = await session.Rpc.Mcp.ListToolsAsync(serverName);
119+
Assert.Contains(tools.Tools, tool => tool.Name == "whoami");
120+
121+
releaseHandler.SetResult(McpAuthResult.FromToken(new McpAuthToken { AccessToken = ExpectedToken }));
122+
}
123+
73124
[Fact]
74125
public async Task Should_Request_Replacement_Tokens_Across_MCP_OAuth_Lifecycle()
75126
{

0 commit comments

Comments
 (0)