,
@@ -13742,6 +13745,28 @@ pub enum OptionsUpdateEnvValueMode {
Unknown,
}
+/// Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set.
+///
+///
+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub enum OptionsUpdateToolFilterPrecedence {
+ /// If availableTools is set, it is the only constraint that applies (excludedTools is ignored). Preserves CLI / pre-existing client behavior. Default.
+ #[serde(rename = "available")]
+ Available,
+ /// A tool is enabled if and only if it matches the allowlist (or the allowlist is unset) AND it does not match the denylist. Makes 'all except X' expressible by combining the two lists.
+ #[serde(rename = "excluded")]
+ Excluded,
+ /// Unknown variant for forward compatibility.
+ #[default]
+ #[serde(other)]
+ Unknown,
+}
+
/// Approve this single request only
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum PermissionDecisionApproveOnceKind {
diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json
index 903a12d1a..fea6ff417 100644
--- a/test/harness/package-lock.json
+++ b/test/harness/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
- "@github/copilot": "^1.0.55-4",
+ "@github/copilot": "^1.0.55-5",
"@modelcontextprotocol/sdk": "^1.26.0",
"@types/node": "^25.3.3",
"@types/node-forge": "^1.3.14",
@@ -464,9 +464,9 @@
}
},
"node_modules/@github/copilot": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.55-4.tgz",
- "integrity": "sha512-998fK3QIrajrUV94l8piSjDD8wnOGosCWE/0YBKoyOhvlqAKAN0smmgizu4unj8fGZKaX1uBNT+qOrxAS9tY/g==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.55-5.tgz",
+ "integrity": "sha512-n6Vr876Iz41PW8pSpOa7SbrNCqaV+6HDLNf/n8V4gIwwlOlIz7Jb00r/fboXZFIT+0dyAGGLoGgd7xUujVL/Xw==",
"dev": true,
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
@@ -476,20 +476,20 @@
"copilot": "npm-loader.js"
},
"optionalDependencies": {
- "@github/copilot-darwin-arm64": "1.0.55-4",
- "@github/copilot-darwin-x64": "1.0.55-4",
- "@github/copilot-linux-arm64": "1.0.55-4",
- "@github/copilot-linux-x64": "1.0.55-4",
- "@github/copilot-linuxmusl-arm64": "1.0.55-4",
- "@github/copilot-linuxmusl-x64": "1.0.55-4",
- "@github/copilot-win32-arm64": "1.0.55-4",
- "@github/copilot-win32-x64": "1.0.55-4"
+ "@github/copilot-darwin-arm64": "1.0.55-5",
+ "@github/copilot-darwin-x64": "1.0.55-5",
+ "@github/copilot-linux-arm64": "1.0.55-5",
+ "@github/copilot-linux-x64": "1.0.55-5",
+ "@github/copilot-linuxmusl-arm64": "1.0.55-5",
+ "@github/copilot-linuxmusl-x64": "1.0.55-5",
+ "@github/copilot-win32-arm64": "1.0.55-5",
+ "@github/copilot-win32-x64": "1.0.55-5"
}
},
"node_modules/@github/copilot-darwin-arm64": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.55-4.tgz",
- "integrity": "sha512-byEpmUpmFnVfDNONaseG/hKBqQWnyRElViu1d+MpoupzOHlfo8/O2ZA7Fq8RkkqrXcCipjhUDBNavdl7j6EbrA==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.55-5.tgz",
+ "integrity": "sha512-Mult62GJVnxR3MOP2QNiVU5RRGXPJ+7BpjEMIvkoaMuWX6J7F4bz7N+HUXVHJUiGUp3hnL3M16kjkewWfNdoNg==",
"cpu": [
"arm64"
],
@@ -504,9 +504,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.55-4.tgz",
- "integrity": "sha512-4/+tldImEu4tLo9x+wiY/7pnOLlJ8GkgUG23izMqIQ1I9otM85irfIrtNHy/bvvT+RQ/HYikNlGv00H2nhIuhw==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.55-5.tgz",
+ "integrity": "sha512-IfY3WhNvHwXHldI2ARsiAYuPlKWlI07Fo1ALq+SViHhn0Zfp2yIr9laJRofyj0G1EbyUxkbNlqQm7UrXhkEVeg==",
"cpu": [
"x64"
],
@@ -521,9 +521,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.55-4.tgz",
- "integrity": "sha512-LwDvphIUMs94HShErbDu0foeasqi/IqIUDo4azWqUFrAp4n4jsiH3MBpC8UwvlSBJSSsND3zGxMrCD/fptqWjA==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.55-5.tgz",
+ "integrity": "sha512-UPZ5Y5QotcZvo3f4yFwJVOtAgUT3mq+q2fim82kWa/MA0+EkkADZ3kb+R4OnV1Nqv5EaoZiCFh0Ukk++IMSYwQ==",
"cpu": [
"arm64"
],
@@ -538,9 +538,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.55-4.tgz",
- "integrity": "sha512-lp8Ae3V/8hB6HuUdWGrdm8au+HyfkFzrvf/rxk7YJe+zkq4D0O+WLi/7JbG8oZ5spQhzSyac/9gUu830nQu2WA==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.55-5.tgz",
+ "integrity": "sha512-Fdwiir53Ogg8C9xv6sTc7/C4vFfQHt6VWFB74kojbDgIbYEpm57wNygQVwJvrwtVW3w/b1MLtGGTp7pEvUBACQ==",
"cpu": [
"x64"
],
@@ -555,9 +555,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-arm64": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.55-4.tgz",
- "integrity": "sha512-4xJCXKHw9OZba+1ysUNxcdkRXK4Mf6s8VA9klcCAnY4bSvkTDVNQVGQuonxjvLGjagyORLymINbKRg6GnrKK/Q==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.55-5.tgz",
+ "integrity": "sha512-NqPmeAA1+iI8Xd4wJUHNNCmVTmHCl+R3nqdXhEVQDLIau9ouGqGGay/91d2ZIgFXJn7J0UTAEdHbdBcfhbnhvg==",
"cpu": [
"arm64"
],
@@ -572,9 +572,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-x64": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.55-4.tgz",
- "integrity": "sha512-z3U6LsXcBssAeolWNlcw+nUm3cSKHvYDSGynvdYVORaEMNN/qemx4ICRAFNfTVRmP+t/3IWtTpCtHm2U7pSO9A==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.55-5.tgz",
+ "integrity": "sha512-bOB4vKw1R7Mekn8z34xpNViYUQ4LQAEFzpkyxhc0uOliFmfku/YcIgo42aMWFzf/Bi3iBazBNfCN+L2lz/Jc9A==",
"cpu": [
"x64"
],
@@ -589,9 +589,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.55-4.tgz",
- "integrity": "sha512-j2kqG0c2JJvOZS2O5E/Ye8F6htA7o7UHpJ55vH6USfzColL0YI51yrP3fZVrDC0nhhtfLnVBK6Q0nv+riZnaew==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.55-5.tgz",
+ "integrity": "sha512-pR2KaiXUanjxolaWgRPlFdeTEpb7jcN1Rk8xVnBCD2ORwERXdYrqXaLCyDbgdplI9mI6IjM+kkUbyXzXoWz/HQ==",
"cpu": [
"arm64"
],
@@ -606,9 +606,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
- "version": "1.0.55-4",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.55-4.tgz",
- "integrity": "sha512-4za8pRAEO6z9SOPP3IYVpZHn4yvPtjAMH17XKYEVYFHTZ+NNHjzYYfyqwhZfJlGgvOOkuh3kYbNWms5a2DW/Jg==",
+ "version": "1.0.55-5",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.55-5.tgz",
+ "integrity": "sha512-EuQBgqSnRFjavgeFifbnSYUJ4elTQBLC/kf+WHolrHR2oUGyiqCQZz/cV2DYVSLP1TGxDKAV4AQCM1AdUT1xEA==",
"cpu": [
"x64"
],
diff --git a/test/harness/package.json b/test/harness/package.json
index 3b1c589fe..29c0603a9 100644
--- a/test/harness/package.json
+++ b/test/harness/package.json
@@ -11,7 +11,7 @@
"test": "vitest run"
},
"devDependencies": {
- "@github/copilot": "^1.0.55-4",
+ "@github/copilot": "^1.0.55-5",
"@modelcontextprotocol/sdk": "^1.26.0",
"@types/node": "^25.3.3",
"@types/node-forge": "^1.3.14",
From 799a7159484c79b6bb29b535d433ac08fd4f529d Mon Sep 17 00:00:00 2001
From: Stephen Toub
Date: Wed, 27 May 2026 13:13:00 -0400
Subject: [PATCH 2/2] Fix flaky E2E CI tests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
dotnet/test/E2E/SessionE2ETests.cs | 23 +--
go/internal/e2e/session_e2e_test.go | 11 +-
nodejs/test/e2e/session.e2e.test.ts | 55 +++----
python/e2e/test_multi_client_e2e.py | 215 +++++++++++++++++-----------
python/e2e/test_session_e2e.py | 18 +--
5 files changed, 186 insertions(+), 136 deletions(-)
diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs
index 77827f413..fa56415b5 100644
--- a/dotnet/test/E2E/SessionE2ETests.cs
+++ b/dotnet/test/E2E/SessionE2ETests.cs
@@ -110,16 +110,19 @@ public async Task Should_Create_A_Session_With_Customized_SystemMessage_Config()
}
});
- await session.SendAsync(new MessageOptions { Prompt = "Who are you?" });
- var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
- Assert.NotNull(assistantMessage);
-
- var traffic = await Ctx.GetExchangesAsync();
- Assert.NotEmpty(traffic);
- var systemMessage = GetSystemMessage(traffic[0]);
- Assert.Contains(customTone, systemMessage);
- Assert.Contains(appendedContent, systemMessage);
- Assert.DoesNotContain("", systemMessage);
+ try
+ {
+ await session.SendAsync(new MessageOptions { Prompt = "Who are you?" });
+ var traffic = await WaitForExchangesAsync();
+ var systemMessage = GetSystemMessage(traffic[0]);
+ Assert.Contains(customTone, systemMessage);
+ Assert.Contains(appendedContent, systemMessage);
+ Assert.DoesNotContain("", systemMessage);
+ }
+ finally
+ {
+ await session.DisposeAsync();
+ }
}
[Fact]
diff --git a/go/internal/e2e/session_e2e_test.go b/go/internal/e2e/session_e2e_test.go
index ab4d9fab4..c2b9f57c9 100644
--- a/go/internal/e2e/session_e2e_test.go
+++ b/go/internal/e2e/session_e2e_test.go
@@ -209,20 +209,15 @@ func TestSessionE2E(t *testing.T) {
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}
+ t.Cleanup(func() { _ = session.Disconnect() })
- _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Who are you?"})
+ _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Who are you?"})
if err != nil {
t.Fatalf("Failed to send message: %v", err)
}
// Validate the system message sent to the model
- traffic, err := ctx.GetExchanges()
- if err != nil {
- t.Fatalf("Failed to get exchanges: %v", err)
- }
- if len(traffic) == 0 {
- t.Fatal("Expected at least one exchange")
- }
+ traffic := ctx.WaitForExchanges(t, 1)
systemMessage := getSystemMessage(traffic[0])
if !strings.Contains(systemMessage, customTone) {
t.Errorf("Expected system message to contain custom tone, got %q", systemMessage)
diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts
index b529eb2bd..5e7766752 100644
--- a/nodejs/test/e2e/session.e2e.test.ts
+++ b/nodejs/test/e2e/session.e2e.test.ts
@@ -212,32 +212,39 @@ describe("Sessions", async () => {
expect(systemMessage).toEqual(testSystemMessage); // Exact match
});
- it("should create a session with customized systemMessage config", async () => {
- const customTone = "Respond in a warm, professional tone. Be thorough in explanations.";
- const appendedContent = "Always mention quarterly earnings.";
- const session = await client.createSession({
- onPermissionRequest: approveAll,
- systemMessage: {
- mode: "customize",
- sections: {
- tone: { action: "replace", content: customTone },
- code_change_rules: { action: "remove" },
+ it(
+ "should create a session with customized systemMessage config",
+ { timeout: 90_000 },
+ async () => {
+ const customTone = "Respond in a warm, professional tone. Be thorough in explanations.";
+ const appendedContent = "Always mention quarterly earnings.";
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ tone: { action: "replace", content: customTone },
+ code_change_rules: { action: "remove" },
+ },
+ content: appendedContent,
},
- content: appendedContent,
- },
- });
-
- const assistantMessage = await session.sendAndWait({ prompt: "Who are you?" });
- expect(assistantMessage?.data.content).toBeDefined();
+ });
- // Validate the system message sent to the model
- const traffic = await openAiEndpoint.getExchanges();
- const systemMessage = getSystemMessage(traffic[0]);
- expect(systemMessage).toContain(customTone);
- expect(systemMessage).toContain(appendedContent);
- // The code_change_rules section should have been removed
- expect(systemMessage).not.toContain("");
- });
+ try {
+ await session.send({ prompt: "Who are you?" });
+
+ // Validate the system message sent to the model
+ const traffic = await waitForExchanges();
+ const systemMessage = getSystemMessage(traffic[0]);
+ expect(systemMessage).toContain(customTone);
+ expect(systemMessage).toContain(appendedContent);
+ // The code_change_rules section should have been removed
+ expect(systemMessage).not.toContain("");
+ } finally {
+ await session.disconnect();
+ }
+ }
+ );
it("should create a session with availableTools", async () => {
const session = await client.createSession({
diff --git a/python/e2e/test_multi_client_e2e.py b/python/e2e/test_multi_client_e2e.py
index deadbfc86..90492e883 100644
--- a/python/e2e/test_multi_client_e2e.py
+++ b/python/e2e/test_multi_client_e2e.py
@@ -184,6 +184,25 @@ async def configure_multi_test(request, mctx):
yield
+def wait_for_event(session, predicate, timeout: float = 30.0):
+ loop = asyncio.get_running_loop()
+ future = loop.create_future()
+
+ def on_event(event):
+ if not future.done() and predicate(event):
+ future.set_result(event)
+
+ unsubscribe = session.on(on_event)
+
+ async def wait():
+ try:
+ return await asyncio.wait_for(future, timeout=timeout)
+ finally:
+ unsubscribe()
+
+ return loop.create_task(wait())
+
+
class TestMultiClientBroadcast:
async def test_both_clients_see_tool_request_and_completion_events(
self, mctx: MultiClientContext
@@ -206,30 +225,38 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str:
session2 = await mctx.client2.resume_session(
session1.session_id, on_permission_request=PermissionHandler.approve_all
)
- client1_events = []
- client2_events = []
- session1.on(lambda event: client1_events.append(event))
- session2.on(lambda event: client2_events.append(event))
-
- # Send a prompt that triggers the custom tool
- await session1.send("Use the magic_number tool with seed 'hello' and tell me the result")
- # Use a longer timeout: first multi-client TCP test on Windows CI needs extra time
- response = await get_final_assistant_message(session1, timeout=30.0)
- assert "MAGIC_hello_42" in (response.data.content or "")
-
- # Both clients should have seen the external_tool.requested event
- c1_tool_requested = [e for e in client1_events if e.type.value == "external_tool.requested"]
- c2_tool_requested = [e for e in client2_events if e.type.value == "external_tool.requested"]
- assert len(c1_tool_requested) > 0
- assert len(c2_tool_requested) > 0
-
- # Both clients should have seen the external_tool.completed event
- c1_tool_completed = [e for e in client1_events if e.type.value == "external_tool.completed"]
- c2_tool_completed = [e for e in client2_events if e.type.value == "external_tool.completed"]
- assert len(c1_tool_completed) > 0
- assert len(c2_tool_completed) > 0
+ waiters = []
+ try:
+ client1_requested = wait_for_event(
+ session1, lambda event: event.type.value == "external_tool.requested"
+ )
+ client2_requested = wait_for_event(
+ session2, lambda event: event.type.value == "external_tool.requested"
+ )
+ client1_completed = wait_for_event(
+ session1, lambda event: event.type.value == "external_tool.completed"
+ )
+ client2_completed = wait_for_event(
+ session2, lambda event: event.type.value == "external_tool.completed"
+ )
+ waiters = [client1_requested, client2_requested, client1_completed, client2_completed]
- await session2.disconnect()
+ # Send a prompt that triggers the custom tool
+ await session1.send(
+ "Use the magic_number tool with seed 'hello' and tell me the result"
+ )
+ # Use a longer timeout: first multi-client TCP test on Windows CI needs extra time
+ response = await get_final_assistant_message(session1, timeout=30.0)
+ assert "MAGIC_hello_42" in (response.data.content or "")
+
+ # Both clients should have seen the external_tool.requested and completed events
+ await asyncio.gather(*waiters)
+ finally:
+ for waiter in waiters:
+ if not waiter.done():
+ waiter.cancel()
+ await asyncio.gather(*waiters, return_exceptions=True)
+ await session2.disconnect()
async def test_one_client_approves_permission_and_both_see_the_result(
self, mctx: MultiClientContext
@@ -249,35 +276,43 @@ async def test_one_client_approves_permission_and_both_see_the_result(
session1.session_id,
on_permission_request=lambda request, invocation: PermissionNoResult(),
)
-
- client1_events = []
- client2_events = []
- session1.on(lambda event: client1_events.append(event))
- session2.on(lambda event: client2_events.append(event))
-
- # Send a prompt that triggers a write operation (requires permission)
- await session1.send("Create a file called hello.txt containing the text 'hello world'")
- response = await get_final_assistant_message(session1)
- assert response.data.content
-
- # Client 1 should have handled permission requests
- assert len(permission_requests) > 0
-
- # Both clients should have seen permission.requested events
- c1_perm_requested = [e for e in client1_events if e.type.value == "permission.requested"]
- c2_perm_requested = [e for e in client2_events if e.type.value == "permission.requested"]
- assert len(c1_perm_requested) > 0
- assert len(c2_perm_requested) > 0
-
- # Both clients should have seen permission.completed events with approved result
- c1_perm_completed = [e for e in client1_events if e.type.value == "permission.completed"]
- c2_perm_completed = [e for e in client2_events if e.type.value == "permission.completed"]
- assert len(c1_perm_completed) > 0
- assert len(c2_perm_completed) > 0
- for event in c1_perm_completed + c2_perm_completed:
- assert event.data.result.kind == "approved"
-
- await session2.disconnect()
+ waiters = []
+ try:
+ client1_requested = wait_for_event(
+ session1, lambda event: event.type.value == "permission.requested"
+ )
+ client2_requested = wait_for_event(
+ session2, lambda event: event.type.value == "permission.requested"
+ )
+ client1_completed = wait_for_event(
+ session1, lambda event: event.type.value == "permission.completed"
+ )
+ client2_completed = wait_for_event(
+ session2, lambda event: event.type.value == "permission.completed"
+ )
+ waiters = [client1_requested, client2_requested, client1_completed, client2_completed]
+
+ # Send a prompt that triggers a write operation (requires permission)
+ await session1.send("Create a file called hello.txt containing the text 'hello world'")
+ response = await get_final_assistant_message(session1)
+ assert response.data.content
+
+ # Client 1 should have handled permission requests
+ assert len(permission_requests) > 0
+
+ # Both clients should have seen permission.requested events
+ await asyncio.gather(client1_requested, client2_requested)
+
+ # Both clients should have seen permission.completed events with approved result
+ completed_events = await asyncio.gather(client1_completed, client2_completed)
+ for event in completed_events:
+ assert event.data.result.kind == "approved"
+ finally:
+ for waiter in waiters:
+ if not waiter.done():
+ waiter.cancel()
+ await asyncio.gather(*waiters, return_exceptions=True)
+ await session2.disconnect()
async def test_one_client_rejects_permission_and_both_see_the_result(
self, mctx: MultiClientContext
@@ -293,40 +328,48 @@ async def test_one_client_rejects_permission_and_both_see_the_result(
session1.session_id,
on_permission_request=lambda request, invocation: PermissionNoResult(),
)
-
- client1_events = []
- client2_events = []
- session1.on(lambda event: client1_events.append(event))
- session2.on(lambda event: client2_events.append(event))
-
- # Create a file that the agent will try to edit
- test_file = os.path.join(mctx.work_dir, "protected.txt")
- with open(test_file, "w") as f:
- f.write("protected content")
-
- await session1.send("Edit protected.txt and replace 'protected' with 'hacked'.")
- await get_final_assistant_message(session1)
-
- # Verify the file was NOT modified (permission was denied)
- with open(test_file) as f:
- content = f.read()
- assert content == "protected content"
-
- # Both clients should have seen permission.requested and permission.completed
- c1_perm_requested = [e for e in client1_events if e.type.value == "permission.requested"]
- c2_perm_requested = [e for e in client2_events if e.type.value == "permission.requested"]
- assert len(c1_perm_requested) > 0
- assert len(c2_perm_requested) > 0
-
- # Both clients should see the denial
- c1_perm_completed = [e for e in client1_events if e.type.value == "permission.completed"]
- c2_perm_completed = [e for e in client2_events if e.type.value == "permission.completed"]
- assert len(c1_perm_completed) > 0
- assert len(c2_perm_completed) > 0
- for event in c1_perm_completed + c2_perm_completed:
- assert event.data.result.kind == "denied-interactively-by-user"
-
- await session2.disconnect()
+ waiters = []
+ try:
+ client1_requested = wait_for_event(
+ session1, lambda event: event.type.value == "permission.requested"
+ )
+ client2_requested = wait_for_event(
+ session2, lambda event: event.type.value == "permission.requested"
+ )
+ client1_completed = wait_for_event(
+ session1, lambda event: event.type.value == "permission.completed"
+ )
+ client2_completed = wait_for_event(
+ session2, lambda event: event.type.value == "permission.completed"
+ )
+ waiters = [client1_requested, client2_requested, client1_completed, client2_completed]
+
+ # Create a file that the agent will try to edit
+ test_file = os.path.join(mctx.work_dir, "protected.txt")
+ with open(test_file, "w") as f:
+ f.write("protected content")
+
+ await session1.send("Edit protected.txt and replace 'protected' with 'hacked'.")
+ await get_final_assistant_message(session1)
+
+ # Verify the file was NOT modified (permission was denied)
+ with open(test_file) as f:
+ content = f.read()
+ assert content == "protected content"
+
+ # Both clients should have seen permission.requested and permission.completed
+ await asyncio.gather(client1_requested, client2_requested)
+
+ # Both clients should see the denial
+ completed_events = await asyncio.gather(client1_completed, client2_completed)
+ for event in completed_events:
+ assert event.data.result.kind == "denied-interactively-by-user"
+ finally:
+ for waiter in waiters:
+ if not waiter.done():
+ waiter.cancel()
+ await asyncio.gather(*waiters, return_exceptions=True)
+ await session2.disconnect()
@pytest.mark.timeout(90)
async def test_two_clients_register_different_tools_and_agent_uses_both(
diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py
index a6044a203..ddc429a2e 100644
--- a/python/e2e/test_session_e2e.py
+++ b/python/e2e/test_session_e2e.py
@@ -103,15 +103,17 @@ async def test_should_create_a_session_with_customized_systemMessage_config(
},
)
- assistant_message = await session.send_and_wait("Who are you?")
- assert assistant_message is not None
+ try:
+ await session.send("Who are you?")
- # Validate the system message sent to the model
- traffic = await ctx.get_exchanges()
- system_message = _get_system_message(traffic[0])
- assert custom_tone in system_message
- assert appended_content in system_message
- assert "" not in system_message
+ # Validate the system message sent to the model
+ traffic = await ctx.wait_for_exchanges()
+ system_message = _get_system_message(traffic[0])
+ assert custom_tone in system_message
+ assert appended_content in system_message
+ assert "" not in system_message
+ finally:
+ await session.disconnect()
async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestContext):
session = await ctx.client.create_session(