Skip to content

Commit ac6de48

Browse files
stephentoubCopilot
andauthored
Add cross-SDK RPC E2E coverage (#1424)
* Add cross-SDK RPC E2E coverage Add non-Canvas RPC E2E coverage across C#, Node, Python, Go, and Rust. The tests exercise server and session RPC surfaces with assertions for stable return shapes, state transitions, events, no-op behavior, and capability-gated error paths. Java is intentionally excluded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python E2E formatting Apply Ruff formatting and import ordering to the new Python RPC E2E tests so the Python SDK checks pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix remaining cross-SDK E2E checks Relax model switch assertions for runtime differences, remove unused Go E2E helper, align Rust fork prompt with the replay snapshot, and normalize Windows permission paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix .NET compaction E2E wait race Use SendAndWaitAsync for the compaction setup turn so the test waits through the SDK send path instead of racing metadata polling and assistant event delivery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Increase Go test timeout Give the Go SDK race test run enough time for the expanded E2E suite on slower Windows runners. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback Tighten review-commented RPC E2E tests across .NET, Python, and Rust by narrowing exception handling, making nullability expectations explicit, avoiding Python static-analysis false positives, and fixing Rust test artifact collisions and queue assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address remaining .NET review feedback Make the context host type assertion explicit and document the intentional remote RPC fallback path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c69ea43 commit ac6de48

62 files changed

Lines changed: 11843 additions & 33 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/test/E2E/CommandsE2ETests.cs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------------------------------------------*/
44

5+
using GitHub.Copilot.Rpc;
6+
using GitHub.Copilot.Test.Harness;
57
using Xunit;
68
using Xunit.Abstractions;
79

@@ -10,6 +12,174 @@ namespace GitHub.Copilot.Test.E2E;
1012
public class CommandsE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
1113
: E2ETestBase(fixture, "commands", output)
1214
{
15+
private static readonly string[] KnownBuiltinCommands = ["help", "model", "compact"];
16+
17+
[Fact]
18+
public async Task Session_Commands_List_Returns_Builtins_And_Respects_Client_Command_Filter()
19+
{
20+
var session = await CreateSessionAsync(new SessionConfig
21+
{
22+
Commands =
23+
[
24+
new CommandDefinition { Name = "deploy", Description = "Deploy the app", Handler = _ => Task.CompletedTask },
25+
new CommandDefinition { Name = "rollback", Description = "Rollback the app", Handler = _ => Task.CompletedTask },
26+
],
27+
});
28+
29+
CommandList? clientCommands = null;
30+
await TestHelper.WaitForConditionAsync(
31+
async () =>
32+
{
33+
clientCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
34+
{
35+
IncludeBuiltins = false,
36+
IncludeClientCommands = true,
37+
IncludeSkills = false,
38+
});
39+
return clientCommands.Commands.Any(c => IsCommand(c, "deploy", SlashCommandKind.Client)) &&
40+
clientCommands.Commands.Any(c => IsCommand(c, "rollback", SlashCommandKind.Client));
41+
},
42+
timeout: TimeSpan.FromSeconds(30),
43+
timeoutMessage: "Timed out waiting for client commands to be listed.");
44+
Assert.Contains(clientCommands!.Commands, c => IsCommand(c, "deploy", SlashCommandKind.Client));
45+
Assert.Contains(clientCommands.Commands, c => IsCommand(c, "rollback", SlashCommandKind.Client));
46+
Assert.DoesNotContain(clientCommands.Commands, c => c.Kind == SlashCommandKind.Builtin);
47+
48+
var builtinCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
49+
{
50+
IncludeBuiltins = true,
51+
IncludeClientCommands = false,
52+
IncludeSkills = false,
53+
});
54+
Assert.True(
55+
builtinCommands.Commands.Any(IsKnownBuiltin),
56+
$"Expected a known built-in command. Actual commands: {FormatCommands(builtinCommands.Commands)}");
57+
Assert.DoesNotContain(builtinCommands.Commands, c => string.Equals(c.Name, "deploy", StringComparison.OrdinalIgnoreCase));
58+
59+
await session.DisposeAsync();
60+
}
61+
62+
[Fact]
63+
public async Task Session_Commands_Invoke_Known_Builtin_Returns_Expected_Result()
64+
{
65+
var session = await CreateSessionAsync();
66+
67+
var builtinCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
68+
{
69+
IncludeBuiltins = true,
70+
IncludeClientCommands = false,
71+
IncludeSkills = false,
72+
});
73+
var commandName = KnownBuiltinCommands.FirstOrDefault(name =>
74+
builtinCommands.Commands.Any(c => IsCommand(c, name, SlashCommandKind.Builtin)));
75+
Assert.NotNull(commandName);
76+
77+
var result = await session.Rpc.Commands.InvokeAsync(commandName);
78+
79+
switch (result)
80+
{
81+
case SlashCommandInvocationResultText text:
82+
Assert.False(string.IsNullOrWhiteSpace(text.Text));
83+
break;
84+
85+
case SlashCommandInvocationResultSelectSubcommand select:
86+
Assert.False(string.IsNullOrWhiteSpace(select.Title));
87+
Assert.NotEmpty(select.Options);
88+
break;
89+
90+
case SlashCommandInvocationResultAgentPrompt prompt:
91+
Assert.False(string.IsNullOrWhiteSpace(prompt.DisplayPrompt));
92+
Assert.False(string.IsNullOrWhiteSpace(prompt.Prompt));
93+
break;
94+
95+
case SlashCommandInvocationResultCompleted completed:
96+
Assert.True(completed.Message is null || !string.IsNullOrWhiteSpace(completed.Message));
97+
break;
98+
99+
default:
100+
Assert.Fail($"Unexpected invocation result: {result.GetType().Name}");
101+
break;
102+
}
103+
104+
await session.DisposeAsync();
105+
}
106+
107+
[Fact]
108+
public async Task Session_Commands_Execute_Runs_Registered_Command_Handler()
109+
{
110+
CommandContext? capturedContext = null;
111+
var session = await CreateSessionAsync(new SessionConfig
112+
{
113+
Commands =
114+
[
115+
new CommandDefinition
116+
{
117+
Name = "deploy",
118+
Description = "Deploy the app",
119+
Handler = ctx =>
120+
{
121+
capturedContext = ctx;
122+
return Task.CompletedTask;
123+
},
124+
},
125+
],
126+
});
127+
128+
await TestHelper.WaitForConditionAsync(
129+
async () =>
130+
{
131+
var commands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
132+
{
133+
IncludeBuiltins = false,
134+
IncludeClientCommands = true,
135+
IncludeSkills = false,
136+
});
137+
return commands.Commands.Any(c => IsCommand(c, "deploy", SlashCommandKind.Client));
138+
},
139+
timeout: TimeSpan.FromSeconds(30),
140+
timeoutMessage: "Timed out waiting for registered command to be listed.");
141+
142+
var result = await session.Rpc.Commands.ExecuteAsync("deploy", "production");
143+
144+
Assert.Null(result.Error);
145+
await TestHelper.WaitForConditionAsync(
146+
() => capturedContext is not null,
147+
timeout: TimeSpan.FromSeconds(10),
148+
timeoutMessage: "Timed out waiting for command handler execution.");
149+
Assert.Equal(session.SessionId, capturedContext!.SessionId);
150+
Assert.Equal("/deploy production", capturedContext.Command);
151+
Assert.Equal("deploy", capturedContext.CommandName);
152+
Assert.Equal("production", capturedContext.Args);
153+
154+
await session.DisposeAsync();
155+
}
156+
157+
[Fact]
158+
public async Task Session_Commands_Enqueue_Accepts_Deterministic_Command()
159+
{
160+
var session = await CreateSessionAsync();
161+
162+
var result = await session.Rpc.Commands.EnqueueAsync("/help");
163+
164+
Assert.True(result.Queued);
165+
166+
await session.DisposeAsync();
167+
}
168+
169+
[Fact]
170+
public async Task Session_Commands_RespondToQueuedCommand_Returns_False_For_Unknown_RequestId()
171+
{
172+
var session = await CreateSessionAsync();
173+
174+
var result = await session.Rpc.Commands.RespondToQueuedCommandAsync(
175+
"missing-queued-command-request",
176+
new QueuedCommandResult { Handled = false });
177+
178+
Assert.False(result.Success);
179+
180+
await session.DisposeAsync();
181+
}
182+
13183
[Fact]
14184
public async Task Session_With_Commands_Creates_Successfully()
15185
{
@@ -134,4 +304,20 @@ public void Resume_Config_Commands_Are_Cloned()
134304
Assert.Single(clone.Commands!);
135305
Assert.Equal("deploy", clone.Commands![0].Name);
136306
}
307+
308+
private static bool IsCommand(SlashCommandInfo command, string name, SlashCommandKind kind)
309+
{
310+
return string.Equals(command.Name, name, StringComparison.OrdinalIgnoreCase) && command.Kind == kind;
311+
}
312+
313+
private static bool IsKnownBuiltin(SlashCommandInfo command)
314+
{
315+
return command.Kind == SlashCommandKind.Builtin &&
316+
KnownBuiltinCommands.Contains(command.Name, StringComparer.OrdinalIgnoreCase);
317+
}
318+
319+
private static string FormatCommands(IEnumerable<SlashCommandInfo> commands)
320+
{
321+
return string.Join(", ", commands.Select(c => $"{c.Name}:{c.Kind.Value}"));
322+
}
137323
}

dotnet/test/E2E/CompactionE2ETests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,43 @@ public async Task Should_Not_Emit_Compaction_Events_When_Infinite_Sessions_Disab
9898
// Should not have any compaction events when disabled
9999
Assert.Empty(compactionEvents);
100100
}
101+
102+
[Fact]
103+
public async Task Should_Return_Empty_Handoff_Summary_For_Fresh_Session()
104+
{
105+
await using var session = await CreateSessionAsync();
106+
107+
var result = await session.Rpc.History.SummarizeForHandoffAsync();
108+
109+
Assert.NotNull(result);
110+
Assert.NotNull(result.Summary);
111+
Assert.Equal(string.Empty, result.Summary);
112+
}
113+
114+
[Fact]
115+
public async Task Should_Summarize_For_Handoff_After_NonEphemeral_Log_Event()
116+
{
117+
await using var session = await CreateSessionAsync();
118+
119+
await session.LogAsync("handoff summary log coverage");
120+
121+
var result = await session.Rpc.History.SummarizeForHandoffAsync();
122+
123+
Assert.NotNull(result);
124+
Assert.NotNull(result.Summary);
125+
}
126+
127+
[Fact]
128+
public async Task Should_Report_No_Op_When_Cancelling_Compaction_Without_In_Flight_Work()
129+
{
130+
await using var session = await CreateSessionAsync();
131+
132+
var backgroundResult = await session.Rpc.History.CancelBackgroundCompactionAsync();
133+
var manualResult = await session.Rpc.History.AbortManualCompactionAsync();
134+
135+
Assert.NotNull(backgroundResult);
136+
Assert.False(backgroundResult.Cancelled);
137+
Assert.NotNull(manualResult);
138+
Assert.False(manualResult.Aborted);
139+
}
101140
}

0 commit comments

Comments
 (0)