Skip to content

Commit 203f7b6

Browse files
Use shared TCP connection token in multi-client E2E tests
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a460019 commit 203f7b6

11 files changed

Lines changed: 70 additions & 36 deletions

dotnet/test/E2E/PendingWorkResumeE2ETests.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class PendingWorkResumeE2ETests(E2ETestFixture fixture, ITestOutputHelper
1515
: E2ETestBase(fixture, "pending_work_resume", output)
1616
{
1717
private static readonly TimeSpan PendingWorkTimeout = TimeSpan.FromSeconds(60);
18+
private const string SharedToken = "pending-work-resume-shared-token";
1819

1920
[Fact]
2021
public async Task Should_Continue_Pending_Permission_Request_After_Resume()
@@ -23,11 +24,11 @@ public async Task Should_Continue_Pending_Permission_Request_After_Resume()
2324
var releaseOriginalPermission = new TaskCompletionSource<PermissionRequestResult>(TaskCreationOptions.RunContinuationsAsynchronously);
2425
var resumedToolInvoked = false;
2526

26-
await using var server = Ctx.CreateClient(useStdio: false);
27+
await using var server = Ctx.CreateClient(useStdio: false, options: new CopilotClientOptions { TcpConnectionToken = SharedToken });
2728
await server.StartAsync();
2829
var cliUrl = GetCliUrl(server);
2930

30-
using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl });
31+
using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = SharedToken });
3132
var session1 = await suspendedClient.CreateSessionAsync(new SessionConfig
3233
{
3334
Tools = [AIFunctionFactory.Create(ResumePermissionTool, "resume_permission_tool")],
@@ -54,7 +55,7 @@ await session1.SendAsync(new MessageOptions
5455

5556
await suspendedClient.ForceStopAsync();
5657

57-
await using var resumedTcpClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl });
58+
await using var resumedTcpClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = SharedToken });
5859
var session2 = await resumedTcpClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig
5960
{
6061
ContinuePendingWork = true,
@@ -106,11 +107,11 @@ public async Task Should_Continue_Pending_External_Tool_Request_After_Resume()
106107
var originalToolStarted = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
107108
var releaseOriginalTool = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
108109

109-
await using var server = Ctx.CreateClient(useStdio: false);
110+
await using var server = Ctx.CreateClient(useStdio: false, options: new CopilotClientOptions { TcpConnectionToken = SharedToken });
110111
await server.StartAsync();
111112
var cliUrl = GetCliUrl(server);
112113

113-
using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl });
114+
using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = SharedToken });
114115
var session1 = await suspendedClient.CreateSessionAsync(new SessionConfig
115116
{
116117
Tools = [AIFunctionFactory.Create(BlockingExternalTool, "resume_external_tool")],
@@ -131,7 +132,7 @@ await session1.SendAsync(new MessageOptions
131132
Assert.Equal("beta", await originalToolStarted.Task.WaitAsync(PendingWorkTimeout));
132133
await suspendedClient.ForceStopAsync();
133134

134-
await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl });
135+
await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = SharedToken });
135136
var session2 = await resumedClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig
136137
{
137138
ContinuePendingWork = true,
@@ -171,11 +172,11 @@ public async Task Should_Continue_Parallel_Pending_External_Tool_Requests_After_
171172
var releaseOriginalToolA = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
172173
var releaseOriginalToolB = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
173174

174-
await using var server = Ctx.CreateClient(useStdio: false);
175+
await using var server = Ctx.CreateClient(useStdio: false, options: new CopilotClientOptions { TcpConnectionToken = SharedToken });
175176
await server.StartAsync();
176177
var cliUrl = GetCliUrl(server);
177178

178-
using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl });
179+
using var suspendedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = SharedToken });
179180
var session1 = await suspendedClient.CreateSessionAsync(new SessionConfig
180181
{
181182
Tools =
@@ -205,7 +206,7 @@ await Task.WhenAll(
205206

206207
await suspendedClient.ForceStopAsync();
207208

208-
await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl });
209+
await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = SharedToken });
209210
var session2 = await resumedClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig
210211
{
211212
ContinuePendingWork = true,
@@ -256,12 +257,12 @@ async Task<string> BlockingToolB([Description("Value to look up")] string value)
256257
[Fact]
257258
public async Task Should_Resume_Successfully_When_No_Pending_Work_Exists()
258259
{
259-
await using var server = Ctx.CreateClient(useStdio: false);
260+
await using var server = Ctx.CreateClient(useStdio: false, options: new CopilotClientOptions { TcpConnectionToken = SharedToken });
260261
await server.StartAsync();
261262
var cliUrl = GetCliUrl(server);
262263

263264
string sessionId;
264-
await using (var firstClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl }))
265+
await using (var firstClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = SharedToken }))
265266
{
266267
var firstSession = await firstClient.CreateSessionAsync(new SessionConfig
267268
{
@@ -275,7 +276,7 @@ public async Task Should_Resume_Successfully_When_No_Pending_Work_Exists()
275276
await firstSession.DisposeAsync();
276277
}
277278

278-
await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl });
279+
await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = SharedToken });
279280
var resumedSession = await resumedClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig
280281
{
281282
ContinuePendingWork = true,

dotnet/test/E2E/SuspendE2ETests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@ public async Task Should_Suspend_Idle_Session_Without_Throwing()
5050
[Fact]
5151
public async Task Should_Allow_Resume_And_Continue_Conversation_After_Suspend()
5252
{
53-
await using var server = Ctx.CreateClient(useStdio: false);
53+
const string sharedToken = "suspend-shared-token";
54+
await using var server = Ctx.CreateClient(useStdio: false, options: new CopilotClientOptions { TcpConnectionToken = sharedToken });
5455
await server.StartAsync();
5556
var cliUrl = GetCliUrl(server);
5657

5758
string sessionId;
58-
await using (var client1 = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl }))
59+
await using (var client1 = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = sharedToken }))
5960
{
6061
var session1 = await client1.CreateSessionAsync(new SessionConfig
6162
{
@@ -76,7 +77,7 @@ await session1.SendAndWaitAsync(new MessageOptions
7677

7778
// A different client should be able to pick the session back up. The previous
7879
// turn was completed before suspend, so there is no pending work to continue.
79-
await using var client2 = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl });
80+
await using var client2 = Ctx.CreateClient(options: new CopilotClientOptions { CliUrl = cliUrl, TcpConnectionToken = sharedToken });
8081
var session2 = await client2.ResumeSessionAsync(sessionId, new ResumeSessionConfig
8182
{
8283
OnPermissionRequest = PermissionHandler.ApproveAll,

go/internal/e2e/commands_and_elicitation_e2e_test.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func TestCommandsE2E(t *testing.T) {
1515
ctx := testharness.NewTestContext(t)
1616
client1 := ctx.NewClient(func(opts *copilot.ClientOptions) {
1717
opts.UseStdio = copilot.Bool(false)
18+
opts.TCPConnectionToken = sharedTcpToken
1819
})
1920
t.Cleanup(func() { client1.ForceStop() })
2021

@@ -33,7 +34,8 @@ func TestCommandsE2E(t *testing.T) {
3334
}
3435

3536
client2 := copilot.NewClient(&copilot.ClientOptions{
36-
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
37+
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
38+
TCPConnectionToken: sharedTcpToken,
3739
})
3840
t.Cleanup(func() { client2.ForceStop() })
3941

@@ -509,6 +511,7 @@ func TestUIElicitationMultiClientE2E(t *testing.T) {
509511
ctx := testharness.NewTestContext(t)
510512
client1 := ctx.NewClient(func(opts *copilot.ClientOptions) {
511513
opts.UseStdio = copilot.Bool(false)
514+
opts.TCPConnectionToken = sharedTcpToken
512515
})
513516
t.Cleanup(func() { client1.ForceStop() })
514517

@@ -558,7 +561,8 @@ func TestUIElicitationMultiClientE2E(t *testing.T) {
558561

559562
// Client2 joins with elicitation handler — should trigger capabilities.changed
560563
client2 := copilot.NewClient(&copilot.ClientOptions{
561-
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
564+
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
565+
TCPConnectionToken: sharedTcpToken,
562566
})
563567
session2, err := client2.ResumeSession(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{
564568
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
@@ -620,7 +624,8 @@ func TestUIElicitationMultiClientE2E(t *testing.T) {
620624

621625
// Client3 (dedicated for this test) joins with elicitation handler
622626
client3 := copilot.NewClient(&copilot.ClientOptions{
623-
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
627+
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
628+
TCPConnectionToken: sharedTcpToken,
624629
})
625630
_, err = client3.ResumeSession(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{
626631
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,

go/internal/e2e/multi_client_e2e_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func TestMultiClientE2E(t *testing.T) {
1818
ctx := testharness.NewTestContext(t)
1919
client1 := ctx.NewClient(func(opts *copilot.ClientOptions) {
2020
opts.UseStdio = copilot.Bool(false)
21+
opts.TCPConnectionToken = sharedTcpToken
2122
})
2223
t.Cleanup(func() { client1.ForceStop() })
2324

@@ -36,7 +37,8 @@ func TestMultiClientE2E(t *testing.T) {
3637
}
3738

3839
client2 := copilot.NewClient(&copilot.ClientOptions{
39-
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
40+
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
41+
TCPConnectionToken: sharedTcpToken,
4042
})
4143
t.Cleanup(func() { client2.ForceStop() })
4244

@@ -475,7 +477,8 @@ func TestMultiClientE2E(t *testing.T) {
475477

476478
// Recreate client2 for cleanup (but don't rejoin the session)
477479
client2 = copilot.NewClient(&copilot.ClientOptions{
478-
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
480+
CLIUrl: fmt.Sprintf("localhost:%d", actualPort),
481+
TCPConnectionToken: sharedTcpToken,
479482
})
480483

481484
// Now only stable_tool should be available

go/internal/e2e/pending_work_resume_e2e_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func TestPendingWorkResumeE2E(t *testing.T) {
4545
suspendedClient := ctx.NewClient(func(opts *copilot.ClientOptions) {
4646
opts.CLIUrl = cliURL
4747
opts.CLIPath = ""
48+
opts.TCPConnectionToken = sharedTcpToken
4849
})
4950
session1, err := suspendedClient.CreateSession(t.Context(), &copilot.SessionConfig{
5051
Tools: []copilot.Tool{originalTool},
@@ -111,6 +112,7 @@ func TestPendingWorkResumeE2E(t *testing.T) {
111112
resumedClient := ctx.NewClient(func(opts *copilot.ClientOptions) {
112113
opts.CLIUrl = cliURL
113114
opts.CLIPath = ""
115+
opts.TCPConnectionToken = sharedTcpToken
114116
})
115117
t.Cleanup(func() { resumedClient.ForceStop() })
116118

@@ -189,6 +191,7 @@ func TestPendingWorkResumeE2E(t *testing.T) {
189191
suspendedClient := ctx.NewClient(func(opts *copilot.ClientOptions) {
190192
opts.CLIUrl = cliURL
191193
opts.CLIPath = ""
194+
opts.TCPConnectionToken = sharedTcpToken
192195
})
193196
session1, err := suspendedClient.CreateSession(t.Context(), &copilot.SessionConfig{
194197
Tools: []copilot.Tool{originalTool},
@@ -226,6 +229,7 @@ func TestPendingWorkResumeE2E(t *testing.T) {
226229
resumedClient := ctx.NewClient(func(opts *copilot.ClientOptions) {
227230
opts.CLIUrl = cliURL
228231
opts.CLIPath = ""
232+
opts.TCPConnectionToken = sharedTcpToken
229233
})
230234
t.Cleanup(func() { resumedClient.ForceStop() })
231235

@@ -301,6 +305,7 @@ func TestPendingWorkResumeE2E(t *testing.T) {
301305
suspendedClient := ctx.NewClient(func(opts *copilot.ClientOptions) {
302306
opts.CLIUrl = cliURL
303307
opts.CLIPath = ""
308+
opts.TCPConnectionToken = sharedTcpToken
304309
})
305310
session1, err := suspendedClient.CreateSession(t.Context(), &copilot.SessionConfig{
306311
Tools: []copilot.Tool{originalA, originalB},
@@ -345,6 +350,7 @@ func TestPendingWorkResumeE2E(t *testing.T) {
345350
resumedClient := ctx.NewClient(func(opts *copilot.ClientOptions) {
346351
opts.CLIUrl = cliURL
347352
opts.CLIPath = ""
353+
opts.TCPConnectionToken = sharedTcpToken
348354
})
349355
t.Cleanup(func() { resumedClient.ForceStop() })
350356

@@ -411,6 +417,7 @@ func TestPendingWorkResumeE2E(t *testing.T) {
411417
firstClient := ctx.NewClient(func(opts *copilot.ClientOptions) {
412418
opts.CLIUrl = cliURL
413419
opts.CLIPath = ""
420+
opts.TCPConnectionToken = sharedTcpToken
414421
})
415422
defer firstClient.ForceStop()
416423

@@ -438,6 +445,7 @@ func TestPendingWorkResumeE2E(t *testing.T) {
438445
resumedClient := ctx.NewClient(func(opts *copilot.ClientOptions) {
439446
opts.CLIUrl = cliURL
440447
opts.CLIPath = ""
448+
opts.TCPConnectionToken = sharedTcpToken
441449
})
442450
t.Cleanup(func() { resumedClient.ForceStop() })
443451

@@ -475,11 +483,20 @@ func serverCliURL(t *testing.T, server *copilot.Client) string {
475483
return fmt.Sprintf("localhost:%d", port)
476484
}
477485

486+
// sharedTcpToken is the connection token used by startTcpServer and any sibling
487+
// client that connects via the resulting CLI URL. Tests use a fixed token rather
488+
// than the auto-generated one because the second client is constructed without
489+
// access to the first client's internal state.
490+
const sharedTcpToken = "tcp-shared-test-token"
491+
478492
// startTcpServer starts a TCP-mode server client and returns its CLI URL.
479493
// It triggers an initial connection so ActualPort is populated.
480494
func startTcpServer(t *testing.T, ctx *testharness.TestContext) (*copilot.Client, string) {
481495
t.Helper()
482-
server := ctx.NewClient(func(opts *copilot.ClientOptions) { opts.UseStdio = copilot.Bool(false) })
496+
server := ctx.NewClient(func(opts *copilot.ClientOptions) {
497+
opts.UseStdio = copilot.Bool(false)
498+
opts.TCPConnectionToken = sharedTcpToken
499+
})
483500
t.Cleanup(func() { server.ForceStop() })
484501
// Trigger connection so we can read the port. CreateSession+Disconnect is the
485502
// established pattern (see multi_client_test.go).

go/internal/e2e/suspend_e2e_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func TestSuspendE2E(t *testing.T) {
5252
client1 := ctx.NewClient(func(opts *copilot.ClientOptions) {
5353
opts.CLIUrl = cliURL
5454
opts.CLIPath = ""
55+
opts.TCPConnectionToken = sharedTcpToken
5556
})
5657
t.Cleanup(func() { client1.ForceStop() })
5758

@@ -77,6 +78,7 @@ func TestSuspendE2E(t *testing.T) {
7778
client2 := ctx.NewClient(func(opts *copilot.ClientOptions) {
7879
opts.CLIUrl = cliURL
7980
opts.CLIPath = ""
81+
opts.TCPConnectionToken = sharedTcpToken
8082
})
8183
t.Cleanup(func() { client2.ForceStop() })
8284

python/e2e/test_commands_e2e.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ async def setup(self):
6262
env=self._get_env(),
6363
use_stdio=False,
6464
github_token=github_token,
65+
tcp_connection_token="py-tcp-shared-test-token",
6566
)
6667
)
6768

@@ -74,7 +75,7 @@ async def setup(self):
7475
actual_port = self._client1.actual_port
7576
assert actual_port is not None
7677

77-
self._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}"))
78+
self._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token"))
7879

7980
async def teardown(self, test_failed: bool = False):
8081
for c in (self._client2, self._client1):

python/e2e/test_multi_client_e2e.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ async def setup(self):
5959
env=self.get_env(),
6060
use_stdio=False,
6161
github_token=github_token,
62+
tcp_connection_token="py-tcp-shared-test-token",
6263
)
6364
)
6465

@@ -72,7 +73,7 @@ async def setup(self):
7273
actual_port = self._client1.actual_port
7374
assert actual_port is not None, "Client 1 should have an actual port after connecting"
7475

75-
self._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}"))
76+
self._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token"))
7677

7778
async def teardown(self, test_failed: bool = False):
7879
if self._client2:
@@ -422,7 +423,7 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str:
422423

423424
# Recreate client2 for future tests (but don't rejoin the session)
424425
actual_port = mctx.client1.actual_port
425-
mctx._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}"))
426+
mctx._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token"))
426427

427428
# Now only stable_tool should be available
428429
await session1.send(

0 commit comments

Comments
 (0)