Skip to content

Commit a98944a

Browse files
stephentoubCopilot
andcommitted
Fix E2E CI for @github/copilot 1.0.61
1.0.61 marks several session MCP lifecycle RPC methods as internal (startServer, restartServer, registerExternalClient, unregisterExternalClient, reloadWithConfig, configureGitHub, mcp.oauth.respond). Remove the E2E cases that exercised those methods on the public client surface across the Node, Python, Go, and .NET MCP lifecycle suites; the public list-tools/status/stop cases are retained. Also make the abort-streaming snapshot tolerant of a transport timing change. Routing fetch through Rust reqwest in the runtime can land the abort before the in-flight assistant turn is finalized, so the recovery request's history omits the aborted assistant message. Add a second conversation covering that dropped-turn path so the replay proxy matches regardless of which side of the race wins. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 46f86cb commit a98944a

5 files changed

Lines changed: 12 additions & 477 deletions

File tree

dotnet/test/E2E/RpcMcpLifecycleE2ETests.cs

Lines changed: 2 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
namespace GitHub.Copilot.Test.E2E;
1010

1111
/// <summary>
12-
/// E2E coverage for the session-scoped MCP lifecycle RPC methods that were previously untested:
13-
/// listTools, isServerRunning, stopServer, startServer, restartServer, registerExternalClient,
14-
/// unregisterExternalClient, reloadWithConfig, configureGitHub, and oauth.respond.
12+
/// E2E coverage for the public session-scoped MCP lifecycle RPC methods:
13+
/// listTools, isServerRunning, and stopServer.
1514
/// </summary>
1615
public class RpcMcpLifecycleE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
1716
: E2ETestBase(fixture, "rpc_mcp_lifecycle", output)
@@ -71,121 +70,6 @@ public async Task Should_Stop_Running_Mcp_Server()
7170
await WaitForMcpRunningAsync(session, serverName, expectedRunning: false);
7271
}
7372

74-
[Fact]
75-
public async Task Should_Start_And_Restart_Mcp_Server()
76-
{
77-
const string hostServer = "rpc-lifecycle-host-server";
78-
await using var session = await CreateSessionAsync(new SessionConfig
79-
{
80-
McpServers = CreateTestMcpServers(hostServer),
81-
});
82-
await WaitForMcpServerStatusAsync(session, hostServer, McpServerStatus.Connected);
83-
84-
// Start a brand-new server through the lifecycle API, reusing the exact stdio config shape
85-
// the bulk session-config path uses so the runtime accepts and connects it.
86-
const string startedServer = "rpc-lifecycle-started-server";
87-
var config = CreateTestMcpServers(startedServer)[startedServer];
88-
89-
await session.Rpc.Mcp.StartServerAsync(startedServer, config);
90-
await WaitForMcpRunningAsync(session, startedServer, expectedRunning: true);
91-
92-
// The freshly started server exposes its tools just like a config-provided server.
93-
var tools = await session.Rpc.Mcp.ListToolsAsync(startedServer);
94-
Assert.NotEmpty(tools.Tools);
95-
96-
// Restart stops then starts the same server; it must end up running again.
97-
await session.Rpc.Mcp.RestartServerAsync(startedServer, config);
98-
await WaitForMcpRunningAsync(session, startedServer, expectedRunning: true);
99-
}
100-
101-
[Fact]
102-
public async Task Should_Register_And_Unregister_External_Mcp_Client()
103-
{
104-
const string hostServer = "rpc-lifecycle-extclient-host";
105-
await using var session = await CreateSessionAsync(new SessionConfig
106-
{
107-
McpServers = CreateTestMcpServers(hostServer),
108-
});
109-
await WaitForMcpServerStatusAsync(session, hostServer, McpServerStatus.Connected);
110-
111-
const string externalName = "rpc-lifecycle-external-client";
112-
Assert.False((await session.Rpc.Mcp.IsServerRunningAsync(externalName)).Running);
113-
114-
// The runtime stores the supplied client/transport handles on the host registry, so the
115-
// registered name immediately reports as running until it is unregistered again.
116-
await session.Rpc.Mcp.RegisterExternalClientAsync(
117-
externalName,
118-
client: new Dictionary<string, object> { ["id"] = externalName },
119-
transport: new Dictionary<string, object> { ["kind"] = "in-process" },
120-
config: new Dictionary<string, object> { ["command"] = "noop" });
121-
Assert.True((await session.Rpc.Mcp.IsServerRunningAsync(externalName)).Running);
122-
123-
await session.Rpc.Mcp.UnregisterExternalClientAsync(externalName);
124-
Assert.False((await session.Rpc.Mcp.IsServerRunningAsync(externalName)).Running);
125-
}
126-
127-
[Fact]
128-
public async Task Should_Reload_Mcp_Servers_With_Config()
129-
{
130-
const string hostServer = "rpc-lifecycle-reload-host";
131-
await using var session = await CreateSessionAsync(new SessionConfig
132-
{
133-
McpServers = CreateTestMcpServers(hostServer),
134-
});
135-
await WaitForMcpServerStatusAsync(session, hostServer, McpServerStatus.Connected);
136-
137-
// reloadWithConfig drives the runtime's reloadMcpServers with an explicit host config and
138-
// returns the startup filtering result. Reloading an empty server set is a valid no-op.
139-
var result = await session.Rpc.Mcp.ReloadWithConfigAsync(new Dictionary<string, object>
140-
{
141-
["mcpServers"] = new Dictionary<string, object>(),
142-
["disabledServers"] = new List<string>(),
143-
});
144-
145-
Assert.NotNull(result);
146-
Assert.NotNull(result.FilteredServers);
147-
Assert.Empty(result.FilteredServers);
148-
}
149-
150-
[Fact]
151-
public async Task Should_Configure_GitHub_Mcp_Server()
152-
{
153-
const string hostServer = "rpc-lifecycle-configure-host";
154-
await using var session = await CreateSessionAsync(new SessionConfig
155-
{
156-
McpServers = CreateTestMcpServers(hostServer),
157-
});
158-
await WaitForMcpServerStatusAsync(session, hostServer, McpServerStatus.Connected);
159-
160-
// configureGitHub forwards a typed auth-info union to the runtime. An "api-key" auth info is
161-
// a recognized type that the runtime declines to act on, so configuration is left unchanged
162-
// (changed=false) while still proving the method is wired through to the handler.
163-
var result = await session.Rpc.Mcp.ConfigureGitHubAsync(new Dictionary<string, object?>
164-
{
165-
["type"] = "api-key",
166-
});
167-
168-
Assert.NotNull(result);
169-
Assert.False(result.Changed);
170-
}
171-
172-
[Fact]
173-
public async Task Should_Respond_To_Mcp_Oauth_Request_Without_Pending_Request()
174-
{
175-
const string hostServer = "rpc-lifecycle-oauth-host";
176-
await using var session = await CreateSessionAsync(new SessionConfig
177-
{
178-
McpServers = CreateTestMcpServers(hostServer),
179-
});
180-
await WaitForMcpServerStatusAsync(session, hostServer, McpServerStatus.Connected);
181-
182-
// With no pending OAuth request, the runtime's respondToMcpOAuth is a tolerant no-op: it
183-
// looks up the request id, finds nothing, and returns an empty result without throwing. The
184-
// call must reach the runtime and complete successfully, proving the method is wired.
185-
var result = await session.Rpc.Mcp.Oauth.RespondAsync($"missing-{Guid.NewGuid():N}");
186-
Assert.NotNull(result);
187-
}
188-
18973
private static Task WaitForMcpRunningAsync(CopilotSession session, string serverName, bool expectedRunning) =>
19074
Harness.TestHelper.WaitForConditionAsync(
19175
async () => (await session.Rpc.Mcp.IsServerRunningAsync(serverName)).Running == expectedRunning,

go/internal/e2e/rpc_mcp_lifecycle_e2e_test.go

Lines changed: 0 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -80,126 +80,6 @@ func TestRpcMcpLifecycle(t *testing.T) {
8080
}
8181
waitForPortedMCPRunning(t, session, serverName, false)
8282
})
83-
84-
t.Run("should_start_and_restart_mcp_server", func(t *testing.T) {
85-
ctx.ConfigureForTest(t)
86-
const hostServer = "rpc-lifecycle-host-server"
87-
session := createPortedSession(t, client, &copilot.SessionConfig{MCPServers: testMCPServers(t, hostServer)})
88-
defer session.Disconnect()
89-
waitForPortedMCPServerStatus(t, session, hostServer, rpc.MCPServerStatusConnected)
90-
91-
const startedServer = "rpc-lifecycle-started-server"
92-
config := testMCPServers(t, startedServer)[startedServer]
93-
if _, err := session.RPC.MCP.StartServer(t.Context(), &rpc.MCPStartServerRequest{ServerName: startedServer, Config: config}); err != nil {
94-
t.Fatalf("MCP.StartServer failed: %v", err)
95-
}
96-
waitForPortedMCPRunning(t, session, startedServer, true)
97-
98-
tools, err := session.RPC.MCP.ListTools(t.Context(), &rpc.MCPListToolsRequest{ServerName: startedServer})
99-
if err != nil {
100-
t.Fatalf("MCP.ListTools(started) failed: %v", err)
101-
}
102-
if len(tools.Tools) == 0 {
103-
t.Fatal("Expected started MCP server to expose tools")
104-
}
105-
106-
if _, err := session.RPC.MCP.RestartServer(t.Context(), &rpc.MCPRestartServerRequest{ServerName: startedServer, Config: config}); err != nil {
107-
t.Fatalf("MCP.RestartServer failed: %v", err)
108-
}
109-
waitForPortedMCPRunning(t, session, startedServer, true)
110-
})
111-
112-
t.Run("should_register_and_unregister_external_mcp_client", func(t *testing.T) {
113-
ctx.ConfigureForTest(t)
114-
const hostServer = "rpc-lifecycle-extclient-host"
115-
session := createPortedSession(t, client, &copilot.SessionConfig{MCPServers: testMCPServers(t, hostServer)})
116-
defer session.Disconnect()
117-
waitForPortedMCPServerStatus(t, session, hostServer, rpc.MCPServerStatusConnected)
118-
119-
const externalName = "rpc-lifecycle-external-client"
120-
initial, err := session.RPC.MCP.IsServerRunning(t.Context(), &rpc.MCPIsServerRunningRequest{ServerName: externalName})
121-
if err != nil {
122-
t.Fatalf("MCP.IsServerRunning(initial external) failed: %v", err)
123-
}
124-
if initial.Running {
125-
t.Fatal("Expected external client to start as not running")
126-
}
127-
128-
if _, err := session.RPC.MCP.RegisterExternalClient(t.Context(), &rpc.MCPRegisterExternalClientRequest{
129-
ServerName: externalName,
130-
Client: map[string]any{"id": externalName},
131-
Transport: map[string]any{"kind": "in-process"},
132-
Config: map[string]any{"command": "noop"},
133-
}); err != nil {
134-
t.Fatalf("MCP.RegisterExternalClient failed: %v", err)
135-
}
136-
waitForPortedMCPRunning(t, session, externalName, true)
137-
138-
if _, err := session.RPC.MCP.UnregisterExternalClient(t.Context(), &rpc.MCPUnregisterExternalClientRequest{ServerName: externalName}); err != nil {
139-
t.Fatalf("MCP.UnregisterExternalClient failed: %v", err)
140-
}
141-
waitForPortedMCPRunning(t, session, externalName, false)
142-
})
143-
144-
t.Run("should_reload_mcp_servers_with_config", func(t *testing.T) {
145-
ctx.ConfigureForTest(t)
146-
const hostServer = "rpc-lifecycle-reload-host"
147-
session := createPortedSession(t, client, &copilot.SessionConfig{MCPServers: testMCPServers(t, hostServer)})
148-
defer session.Disconnect()
149-
waitForPortedMCPServerStatus(t, session, hostServer, rpc.MCPServerStatusConnected)
150-
151-
result, err := session.RPC.MCP.ReloadWithConfig(t.Context(), &rpc.MCPReloadWithConfigRequest{Config: map[string]any{
152-
"mcpServers": map[string]any{},
153-
"disabledServers": []string{},
154-
}})
155-
if err != nil {
156-
t.Fatalf("MCP.ReloadWithConfig failed: %v", err)
157-
}
158-
if result == nil {
159-
t.Fatal("Expected non-nil reload result")
160-
}
161-
if result.FilteredServers == nil {
162-
t.Fatal("Expected non-nil FilteredServers")
163-
}
164-
if len(result.FilteredServers) != 0 {
165-
t.Fatalf("Expected no filtered servers, got %+v", result.FilteredServers)
166-
}
167-
})
168-
169-
t.Run("should_configure_github_mcp_server", func(t *testing.T) {
170-
ctx.ConfigureForTest(t)
171-
const hostServer = "rpc-lifecycle-configure-host"
172-
session := createPortedSession(t, client, &copilot.SessionConfig{MCPServers: testMCPServers(t, hostServer)})
173-
defer session.Disconnect()
174-
waitForPortedMCPServerStatus(t, session, hostServer, rpc.MCPServerStatusConnected)
175-
176-
result, err := session.RPC.MCP.ConfigureGitHub(t.Context(), &rpc.MCPConfigureGitHubRequest{AuthInfo: map[string]any{"type": "api-key"}})
177-
if err != nil {
178-
t.Fatalf("MCP.ConfigureGitHub failed: %v", err)
179-
}
180-
if result == nil {
181-
t.Fatal("Expected non-nil configure result")
182-
}
183-
if result.Changed {
184-
t.Fatal("Expected Changed=false")
185-
}
186-
})
187-
188-
t.Run("should_respond_to_mcp_oauth_request_without_pending_request", func(t *testing.T) {
189-
ctx.ConfigureForTest(t)
190-
const hostServer = "rpc-lifecycle-oauth-host"
191-
session := createPortedSession(t, client, &copilot.SessionConfig{MCPServers: testMCPServers(t, hostServer)})
192-
defer session.Disconnect()
193-
waitForPortedMCPServerStatus(t, session, hostServer, rpc.MCPServerStatusConnected)
194-
195-
result, err := session.RPC.MCP.Oauth().Respond(t.Context(), &rpc.MCPOauthRespondRequest{RequestID: "missing-" + randomHex(t)})
196-
if err != nil {
197-
t.Fatalf("MCP.Oauth.Respond failed: %v", err)
198-
}
199-
if result == nil {
200-
t.Fatal("Expected non-nil OAuth respond result")
201-
}
202-
})
20383
}
20484

20585
func waitForPortedMCPServerStatus(t *testing.T, session *copilot.Session, serverName string, expectedStatus rpc.MCPServerStatus) {

nodejs/test/e2e/rpc_mcp_lifecycle.e2e.test.ts

Lines changed: 0 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -148,115 +148,4 @@ describe("Session-scoped MCP lifecycle RPC", async () => {
148148
await session.disconnect();
149149
}
150150
});
151-
152-
it("should start and restart mcp server", { timeout: 180_000 }, async () => {
153-
const hostServer = "rpc-lifecycle-host-server";
154-
const session = await createSessionWithMcp(hostServer);
155-
try {
156-
await waitForMcpServerStatus(session, hostServer);
157-
158-
const startedServer = "rpc-lifecycle-started-server";
159-
const config = createTestMcpServers(startedServer)[startedServer] as unknown as Record<
160-
string,
161-
unknown
162-
>;
163-
164-
await session.rpc.mcp.startServer({ serverName: startedServer, config });
165-
await waitForMcpRunning(session, startedServer, true);
166-
167-
const tools = await session.rpc.mcp.listTools({ serverName: startedServer });
168-
expect(tools.tools.length).toBeGreaterThan(0);
169-
170-
await session.rpc.mcp.restartServer({ serverName: startedServer, config });
171-
await waitForMcpRunning(session, startedServer, true);
172-
} finally {
173-
await session.disconnect();
174-
}
175-
});
176-
177-
it("should register and unregister external mcp client", { timeout: 120_000 }, async () => {
178-
const hostServer = "rpc-lifecycle-extclient-host";
179-
const session = await createSessionWithMcp(hostServer);
180-
try {
181-
await waitForMcpServerStatus(session, hostServer);
182-
183-
const externalName = "rpc-lifecycle-external-client";
184-
expect(
185-
(await session.rpc.mcp.isServerRunning({ serverName: externalName })).running
186-
).toBe(false);
187-
188-
await session.rpc.mcp.registerExternalClient({
189-
serverName: externalName,
190-
client: { id: externalName },
191-
transport: { kind: "in-process" },
192-
config: { command: "noop" },
193-
});
194-
expect(
195-
(await session.rpc.mcp.isServerRunning({ serverName: externalName })).running
196-
).toBe(true);
197-
198-
await session.rpc.mcp.unregisterExternalClient({ serverName: externalName });
199-
expect(
200-
(await session.rpc.mcp.isServerRunning({ serverName: externalName })).running
201-
).toBe(false);
202-
} finally {
203-
await session.disconnect();
204-
}
205-
});
206-
207-
it("should reload mcp servers with config", { timeout: 120_000 }, async () => {
208-
const hostServer = "rpc-lifecycle-reload-host";
209-
const session = await createSessionWithMcp(hostServer);
210-
try {
211-
await waitForMcpServerStatus(session, hostServer);
212-
213-
const result = await session.rpc.mcp.reloadWithConfig({
214-
config: {
215-
mcpServers: {},
216-
disabledServers: [],
217-
},
218-
});
219-
220-
expect(result).toBeDefined();
221-
expect(result.filteredServers).toEqual([]);
222-
} finally {
223-
await session.disconnect();
224-
}
225-
});
226-
227-
it("should configure github mcp server", { timeout: 120_000 }, async () => {
228-
const hostServer = "rpc-lifecycle-configure-host";
229-
const session = await createSessionWithMcp(hostServer);
230-
try {
231-
await waitForMcpServerStatus(session, hostServer);
232-
233-
const result = await session.rpc.mcp.configureGitHub({
234-
authInfo: { type: "api-key" },
235-
});
236-
237-
expect(result).toBeDefined();
238-
expect(result.changed).toBe(false);
239-
} finally {
240-
await session.disconnect();
241-
}
242-
});
243-
244-
it(
245-
"should respond to mcp oauth request without pending request",
246-
{ timeout: 120_000 },
247-
async () => {
248-
const hostServer = "rpc-lifecycle-oauth-host";
249-
const session = await createSessionWithMcp(hostServer);
250-
try {
251-
await waitForMcpServerStatus(session, hostServer);
252-
253-
const result = await session.rpc.mcp.oauth.respond({
254-
requestId: missingName("missing"),
255-
});
256-
expect(result).toBeDefined();
257-
} finally {
258-
await session.disconnect();
259-
}
260-
}
261-
);
262151
});

0 commit comments

Comments
 (0)