Skip to content

Commit 7390fb5

Browse files
Fix active .NET and Python CI failures (#2093)
1 parent 903b0f8 commit 7390fb5

4 files changed

Lines changed: 92 additions & 36 deletions

File tree

dotnet/test/E2E/SessionE2ETests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,10 @@ await session.SendAsync(new MessageOptions
332332
// Verify an abort event exists in messages
333333
Assert.Contains(messages, m => m is AbortEvent);
334334

335-
// We should be able to send another message
336-
var answer = await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 2+2?" });
337-
Assert.NotNull(answer);
338-
Assert.Contains("4", answer!.Data.Content ?? string.Empty);
335+
await session.SendAsync(new MessageOptions { Prompt = "What is 2+2?" });
336+
var recoveryMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
337+
Assert.NotNull(recoveryMessage);
338+
Assert.Contains("4", recoveryMessage.Data.Content ?? string.Empty);
339339
}
340340

341341
[Fact]

dotnet/test/Harness/E2ETestContext.cs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.Logging;
66
using System.Diagnostics;
77
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
89
using System.Text.RegularExpressions;
910

1011
namespace GitHub.Copilot.Test.Harness;
@@ -141,20 +142,40 @@ private static string GetCliPath(string repoRoot)
141142
if (!string.IsNullOrEmpty(envPath)) return envPath;
142143

143144
// As of CLI 1.0.64-1 the @github/copilot package is a thin loader; the
144-
// runnable index.js ships in the installed platform package
145-
// (e.g. @github/copilot-linux-x64). Exactly one is installed.
145+
// runnable index.js ships in the installed platform package.
146146
var githubModules = Path.Join(repoRoot, "nodejs", "node_modules", "@github");
147-
if (Directory.Exists(githubModules))
147+
var packagePrefix = GetCliPackagePrefix();
148+
var candidates = Directory.Exists(githubModules)
149+
? Directory.EnumerateDirectories(githubModules, $"{packagePrefix}-*", SearchOption.TopDirectoryOnly)
150+
.Select(directory => Path.Join(directory, "index.js"))
151+
.Where(File.Exists)
152+
.ToArray()
153+
: [];
154+
155+
return candidates.Length switch
148156
{
149-
var candidate = Directory.EnumerateDirectories(githubModules, "copilot-*")
150-
.Select(dir => Path.Join(dir, "index.js"))
151-
.FirstOrDefault(File.Exists);
152-
if (candidate != null)
153-
return candidate;
154-
}
157+
1 => candidates[0],
158+
0 => throw new InvalidOperationException(
159+
$"CLI package matching '{packagePrefix}-*' not found under {githubModules}. " +
160+
"Run 'npm install' in the nodejs directory first."),
161+
_ => throw new InvalidOperationException(
162+
$"Multiple CLI packages matching '{packagePrefix}-*' found under {githubModules}: " +
163+
string.Join(", ", candidates.Select(Path.GetDirectoryName))),
164+
};
165+
}
155166

156-
throw new InvalidOperationException(
157-
$"CLI not found under {githubModules}. Run 'npm install' in the nodejs directory first.");
167+
private static string GetCliPackagePrefix()
168+
{
169+
var platform = OperatingSystem.IsWindows()
170+
? "win32"
171+
: OperatingSystem.IsMacOS()
172+
? "darwin"
173+
: OperatingSystem.IsLinux()
174+
? RuntimeInformation.RuntimeIdentifier.StartsWith("linux-musl-", StringComparison.Ordinal)
175+
? "linuxmusl"
176+
: "linux"
177+
: throw new PlatformNotSupportedException("Unsupported operating system for Copilot CLI E2E tests.");
178+
return $"copilot-{platform}";
158179
}
159180

160181
public async Task ConfigureForTestAsync(string testFile, [CallerMemberName] string? testName = null)

python/README.md

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ from copilot import CopilotClient
7676
from copilot.session_events import AssistantMessageData, SessionIdleData
7777
from copilot.session import PermissionHandler
7878

79+
7980
async def main():
8081
# Client automatically starts on enter and cleans up on exit
8182
async with CopilotClient() as client:
@@ -100,6 +101,7 @@ async def main():
100101
await session.send("What is 2+2?")
101102
await done.wait()
102103

104+
103105
asyncio.run(main())
104106
```
105107

@@ -114,6 +116,7 @@ from copilot import CopilotClient
114116
from copilot.session_events import AssistantMessageData, SessionIdleData
115117
from copilot.session import PermissionHandler
116118

119+
117120
async def main():
118121
client = CopilotClient()
119122
await client.start()
@@ -141,6 +144,7 @@ async def main():
141144
await session.disconnect()
142145
await client.stop()
143146

147+
144148
asyncio.run(main())
145149
```
146150

@@ -167,6 +171,7 @@ async with CopilotClient() as client:
167171
on_permission_request=PermissionHandler.approve_all,
168172
model="gpt-5",
169173
) as session:
174+
170175
def on_event(event):
171176
print(f"Event: {event.type}")
172177

@@ -287,14 +292,18 @@ session_id = await client.get_foreground_session_id()
287292
# Request TUI to display a specific session (TUI+server mode only)
288293
await client.set_foreground_session_id("session-123")
289294

295+
290296
# Subscribe to all lifecycle events
291297
def on_lifecycle(event):
292298
print(f"{event.type}: {event.session_id}")
293299

300+
294301
unsubscribe = client.on_lifecycle(on_lifecycle)
295302

296303
# Subscribe to specific event type
297-
unsubscribe = client.on_lifecycle("session.foreground", lambda e: print(f"Foreground: {e.session_id}"))
304+
unsubscribe = client.on_lifecycle(
305+
"session.foreground", lambda e: print(f"Foreground: {e.session_id}")
306+
)
298307

299308
# Later, to stop receiving events:
300309
unsubscribe()
@@ -316,14 +325,17 @@ Define tools with automatic JSON schema generation using the `@define_tool` deco
316325
from pydantic import BaseModel, Field
317326
from copilot import CopilotClient, define_tool
318327

328+
319329
class LookupIssueParams(BaseModel):
320330
id: str = Field(description="Issue identifier")
321331

332+
322333
@define_tool(description="Fetch issue details from our tracker")
323334
async def lookup_issue(params: LookupIssueParams) -> str:
324335
issue = await fetch_issue(params.id)
325336
return issue.summary
326337

338+
327339
async with await client.create_session(
328340
on_permission_request=PermissionHandler.approve_all,
329341
model="gpt-5",
@@ -343,6 +355,7 @@ from copilot import CopilotClient
343355
from copilot.tools import Tool, ToolInvocation, ToolResult
344356
from copilot.session import PermissionHandler
345357

358+
346359
async def lookup_issue(invocation: ToolInvocation) -> ToolResult:
347360
issue_id = invocation.arguments["id"]
348361
issue = await fetch_issue(issue_id)
@@ -352,6 +365,7 @@ async def lookup_issue(invocation: ToolInvocation) -> ToolResult:
352365
session_log=f"Fetched issue {issue_id}",
353366
)
354367

368+
355369
async with await client.create_session(
356370
on_permission_request=PermissionHandler.approve_all,
357371
model="gpt-5",
@@ -471,6 +485,7 @@ from copilot.session_events import (
471485
)
472486
from copilot.session import PermissionHandler
473487

488+
474489
async def main():
475490
async with CopilotClient() as client:
476491
async with await client.create_session(
@@ -507,6 +522,7 @@ async def main():
507522
await session.send("Tell me a short story")
508523
await done.wait() # Wait for streaming to complete
509524

525+
510526
asyncio.run(main())
511527
```
512528

@@ -678,7 +694,10 @@ async with await client.create_session(
678694
system_message={
679695
"mode": "customize",
680696
"sections": {
681-
"tone": {"action": "replace", "content": "Respond in a warm, professional tone. Be thorough in explanations."},
697+
"tone": {
698+
"action": "replace",
699+
"content": "Respond in a warm, professional tone. Be thorough in explanations.",
700+
},
682701
"code_change_rules": {"action": "remove"},
683702
"guidelines": {"action": "append", "content": "\n* Always cite data sources"},
684703
},
@@ -698,6 +717,7 @@ You can also pass a transform callback as the `action` instead of a string. The
698717
def redact_paths(content: str) -> str:
699718
return content.replace("/home/user", "/***")
700719

720+
701721
async with await client.create_session(
702722
on_permission_request=PermissionHandler.approve_all,
703723
model="gpt-5",
@@ -785,9 +805,7 @@ from copilot.rpc import (
785805
from copilot.session_events import PermissionRequestShell
786806

787807

788-
def on_permission_request(
789-
request: PermissionRequest, invocation: dict
790-
) -> PermissionRequestResult:
808+
def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
791809
# ``PermissionRequest`` is a discriminated union — pattern-match on
792810
# the variant class to access the per-kind fields.
793811
match request:
@@ -871,6 +889,7 @@ async def handle_user_input(request, invocation):
871889
"wasFreeform": True, # Whether the answer was freeform (not from choices)
872890
}
873891

892+
874893
async with await client.create_session(
875894
on_permission_request=PermissionHandler.approve_all,
876895
model="gpt-5",
@@ -893,12 +912,14 @@ async def on_pre_tool_use(input, invocation):
893912
"additionalContext": "Extra context for the model",
894913
}
895914

915+
896916
async def on_post_tool_use(input, invocation):
897917
print(f"Tool {input['toolName']} completed")
898918
return {
899919
"additionalContext": "Post-execution notes",
900920
}
901921

922+
902923
async def on_post_tool_use_failure(input, invocation):
903924
# Fires when a tool's result was a failure. `on_post_tool_use` only fires
904925
# on success, so register this handler to observe failed tool calls. The
@@ -908,27 +929,32 @@ async def on_post_tool_use_failure(input, invocation):
908929
"additionalContext": f"Retry guidance for {input['toolName']}",
909930
}
910931

932+
911933
async def on_user_prompt_submitted(input, invocation):
912934
print(f"User prompt: {input['prompt']}")
913935
return {
914936
"modifiedPrompt": input["prompt"], # Optionally modify the prompt
915937
}
916938

939+
917940
async def on_session_start(input, invocation):
918941
print(f"Session started from: {input['source']}") # "startup", "resume", "new"
919942
return {
920943
"additionalContext": "Session initialization context",
921944
}
922945

946+
923947
async def on_session_end(input, invocation):
924948
print(f"Session ended: {input['reason']}")
925949

950+
926951
async def on_error_occurred(input, invocation):
927952
print(f"Error in {input['errorContext']}: {input['error']}")
928953
return {
929954
"errorHandling": "retry", # "retry", "skip", or "abort"
930955
}
931956

957+
932958
async with await client.create_session(
933959
on_permission_request=PermissionHandler.approve_all,
934960
model="gpt-5",
@@ -962,13 +988,15 @@ Register slash commands that users can invoke from the CLI TUI. When the user ty
962988
```python
963989
from copilot.session import CommandDefinition, CommandContext, PermissionHandler
964990

991+
965992
async def handle_deploy(ctx: CommandContext) -> None:
966993
print(f"Deploying with args: {ctx.args}")
967994
# ctx.session_id — the session where the command was invoked
968995
# ctx.command — full command text (e.g. "/deploy production")
969996
# ctx.command_name — command name without leading / (e.g. "deploy")
970997
# ctx.args — raw argument string (e.g. "production")
971998

999+
9721000
async with await client.create_session(
9731001
on_permission_request=PermissionHandler.approve_all,
9741002
commands=[
@@ -1030,29 +1058,34 @@ Shows a text input dialog with optional constraints:
10301058
name = await session.ui.input("Enter your name:")
10311059

10321060
# With options
1033-
email = await session.ui.input("Enter email:", {
1034-
"title": "Email Address",
1035-
"description": "We'll use this for notifications",
1036-
"format": "email",
1037-
})
1061+
email = await session.ui.input(
1062+
"Enter email:",
1063+
{
1064+
"title": "Email Address",
1065+
"description": "We'll use this for notifications",
1066+
"format": "email",
1067+
},
1068+
)
10381069
```
10391070

10401071
### Custom Elicitation
10411072

10421073
For full control, use the `elicitation()` method with a custom JSON schema:
10431074

10441075
```python
1045-
result = await session.ui.elicitation({
1046-
"message": "Configure deployment",
1047-
"requestedSchema": {
1048-
"type": "object",
1049-
"properties": {
1050-
"region": {"type": "string", "enum": ["us-east-1", "eu-west-1"]},
1051-
"replicas": {"type": "number", "minimum": 1, "maximum": 10},
1076+
result = await session.ui.elicitation(
1077+
{
1078+
"message": "Configure deployment",
1079+
"requestedSchema": {
1080+
"type": "object",
1081+
"properties": {
1082+
"region": {"type": "string", "enum": ["us-east-1", "eu-west-1"]},
1083+
"replicas": {"type": "number", "minimum": 1, "maximum": 10},
1084+
},
1085+
"required": ["region"],
10521086
},
1053-
"required": ["region"],
1054-
},
1055-
})
1087+
}
1088+
)
10561089

10571090
if result["action"] == "accept":
10581091
region = result["content"]["region"]
@@ -1066,6 +1099,7 @@ When the server (or an MCP tool) needs to ask the end-user a question, it sends
10661099
```python
10671100
from copilot.session import ElicitationContext, ElicitationResult, PermissionHandler
10681101

1102+
10691103
async def handle_elicitation(
10701104
context: ElicitationContext,
10711105
) -> ElicitationResult:
@@ -1082,6 +1116,7 @@ async def handle_elicitation(
10821116
"content": {"answer": "yes"},
10831117
}
10841118

1119+
10851120
async with await client.create_session(
10861121
on_permission_request=PermissionHandler.approve_all,
10871122
on_elicitation_request=handle_elicitation,

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ telemetry = [
4141

4242
[dependency-groups]
4343
dev = [
44-
"ruff>=0.1.0",
44+
"ruff==0.16.0",
4545
"ty>=0.0.2,<0.0.25",
4646
"pytest>=7.0.0",
4747
"pytest-asyncio>=0.21.0",

0 commit comments

Comments
 (0)