Skip to content

Commit b6bb031

Browse files
Update SDK snapshot for Copilot CLI 1.0.91-0
1 parent f8ae645 commit b6bb031

78 files changed

Lines changed: 4269 additions & 331 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.

‎.github/workflows/sdk-java.yml‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ jobs:
6363
run: ./mvnw javadoc:javadoc
6464
- if: github.event_name != 'merge_group' && matrix.test-jdk == '25'
6565
run: ./mvnw spotless:check
66-
- run: npm ci --ignore-scripts
67-
working-directory: ${{ inputs.sdk-home }}/test/harness
6866
- if: matrix.test-jdk == '25'
6967
env:
7068
CI: "true"

‎CONTRIBUTING.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,13 @@ export COPILOT_CLI_PATH="$(npm --prefix nodejs run --silent prepare:runtime -- -
276276
export COPILOT_LEGACY_CLI_PATH="$(npm --prefix nodejs run --silent prepare:runtime -- --print-legacy-path)"
277277
npm --prefix nodejs test -- test/e2e/structured_output.e2e.test.ts
278278
(cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj \
279+
-p:CopilotSkipCliDownload=true \
279280
--filter FullyQualifiedName~StructuredOutputE2ETests)
280281
```
281282

282283
These are shell-local overrides for focused runs, not machine-wide settings.
284+
The .NET flag skips MSBuild's separate release download; the tests use the
285+
prepared runtime from `COPILOT_CLI_PATH`.
283286
The facade sets runtime paths only for its own child processes and clears
284287
stale or cross-target overrides before building the host CLI. Cross-target CI
285288
instead stages explicit artifacts and uses native test commands; do not copy

‎dotnet/src/Client.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ or IOException
699699

700700
if (ctx.FfiHost is { } ffiHost)
701701
{
702-
try { ffiHost.Dispose(); }
702+
try { await Task.Run(ffiHost.Dispose).ConfigureAwait(false); }
703703
catch (Exception ex) { AddCleanupError(errors, ex, _logger); }
704704
_ffiHost = null;
705705
}

‎dotnet/src/Generated/Rpc.cs‎

Lines changed: 444 additions & 52 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dotnet/src/Generated/SessionEvents.cs‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dotnet/test/E2E/McpOAuthE2ETests.cs‎

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,38 @@ public class McpOAuthE2ETests(E2ETestFixture fixture, ITestOutputHelper output)
2222
private const string ReauthToken = ExpectedToken + "-reauth";
2323
private const string CimdUrl = "https://github.com/copilot/cli/client-metadata.json";
2424

25+
[Fact]
26+
public async Task Should_Reclaim_Server_Process_When_Startup_Is_Canceled()
27+
{
28+
using var process = Process.Start(new ProcessStartInfo("node")
29+
{
30+
Arguments = "-e \"console.log('started'); setInterval(() => {}, 1000)\"",
31+
UseShellExecute = false,
32+
RedirectStandardOutput = true,
33+
RedirectStandardError = true,
34+
CreateNoWindow = true,
35+
})!;
36+
using var observer = Process.GetProcessById(process.Id);
37+
using var cancellation = new CancellationTokenSource();
38+
try
39+
{
40+
Assert.Equal("started", await process.StandardOutput.ReadLineAsync().WaitAsync(TimeSpan.FromSeconds(10)));
41+
var startup = OAuthMcpServer.WaitForListeningAsync(process, cancellation.Token);
42+
cancellation.Cancel();
43+
44+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => startup);
45+
Assert.True(observer.HasExited, "Canceled startup retained the OAuth server process.");
46+
}
47+
finally
48+
{
49+
if (!observer.HasExited)
50+
{
51+
observer.Kill(entireProcessTree: true);
52+
await observer.WaitForExitAsync();
53+
}
54+
}
55+
}
56+
2557
[Fact]
2658
public async Task Should_Use_Cimd_Url_Instead_Of_Dynamic_Registration()
2759
{
@@ -324,23 +356,53 @@ public static async Task<OAuthMcpServer> StartAsync(string expectedToken, bool c
324356

325357
var process = Process.Start(startInfo)
326358
?? throw new InvalidOperationException("Failed to start OAuth MCP server.");
327-
var stderrTask = process.StandardError.ReadToEndAsync();
328-
329359
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
330-
while (!cts.IsCancellationRequested)
360+
return await WaitForListeningAsync(process, cts.Token);
361+
}
362+
363+
public static async Task<OAuthMcpServer> WaitForListeningAsync(Process process, CancellationToken cancellationToken)
364+
{
365+
#if NET
366+
var stderrTask = process.StandardError.ReadToEndAsync(CancellationToken.None);
367+
#else
368+
var stderrTask = process.StandardError.ReadToEndAsync();
369+
#endif
370+
try
331371
{
332-
var line = await process.StandardOutput.ReadLineAsync(cts.Token);
333-
if (line is null)
372+
while (true)
334373
{
335-
throw new InvalidOperationException($"OAuth MCP server exited before listening: {await stderrTask}");
374+
var line = await process.StandardOutput.ReadLineAsync(cancellationToken);
375+
if (line is null)
376+
{
377+
throw new InvalidOperationException($"OAuth MCP server exited before listening: {await stderrTask}");
378+
}
379+
if (line.StartsWith("Listening: ", StringComparison.Ordinal))
380+
{
381+
return new OAuthMcpServer(process, line["Listening: ".Length..]);
382+
}
336383
}
337-
if (line.StartsWith("Listening: ", StringComparison.Ordinal))
384+
}
385+
catch (Exception startupError)
386+
{
387+
try
338388
{
339-
return new OAuthMcpServer(process, line["Listening: ".Length..]);
389+
if (!process.HasExited)
390+
{
391+
process.Kill(entireProcessTree: true);
392+
}
393+
await process.WaitForExitAsync(CancellationToken.None);
394+
await stderrTask;
395+
}
396+
catch (Exception cleanupError)
397+
{
398+
throw new AggregateException(startupError, cleanupError);
340399
}
400+
finally
401+
{
402+
process.Dispose();
403+
}
404+
throw;
341405
}
342-
343-
throw new TimeoutException($"Timed out waiting for OAuth MCP server: {await stderrTask}");
344406
}
345407

346408
public async Task<List<OAuthMcpRequest>> GetRequestsAsync()

‎dotnet/test/E2E/SubagentHooksE2ETests.cs‎

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_T
2020
{
2121
var hookLog = new ConcurrentBag<(string Kind, string ToolName, string SessionId)>();
2222
var requestHandler = new RecordingForwardingRequestHandler();
23+
const string waitingText = "I've launched an explore agent to read subagent-test.txt. Waiting for it to complete...";
24+
const string finalText = "The explore agent successfully read the file. The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```";
25+
var parentSessionId = Guid.NewGuid().ToString();
26+
var parentWaiting = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
2327

2428
// Create a client with the session-based subagents feature flag
2529
var env = new Dictionary<string, string>(Ctx.GetEnvironment());
@@ -32,6 +36,7 @@ public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_T
3236

3337
var session = await Ctx.CreateSessionAsync(client, new SessionConfig
3438
{
39+
SessionId = parentSessionId,
3540
OnPermissionRequest = PermissionHandler.ApproveAll,
3641
Hooks = new SessionHooks
3742
{
@@ -43,25 +48,52 @@ public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_T
4348
PermissionDecision = "allow"
4449
});
4550
},
46-
OnPostToolUse = (input, invocation) =>
51+
OnPostToolUse = async (input, invocation) =>
4752
{
4853
hookLog.Add(("post", input.ToolName, input.SessionId));
49-
return Task.FromResult<PostToolUseHookOutput?>(null);
54+
// A fast child can inject its result before the fixture's waiting reply is requested.
55+
if (input.ToolName == "view" && input.SessionId != parentSessionId)
56+
{
57+
await parentWaiting.Task;
58+
}
59+
return null;
5060
},
5161
},
5262
});
5363

5464
// Create a file for the sub-agent to read
5565
await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "subagent-test.txt"), "Hello from subagent test!");
5666

57-
await session.SendAndWaitAsync(
58-
new MessageOptions
67+
using var subscription = session.On<AssistantMessageEvent>(message =>
68+
{
69+
if (string.IsNullOrEmpty(message.AgentId) && message.Data.Content == waitingText)
5970
{
60-
Prompt = "Use the task tool to spawn an explore agent that reads the file "
61-
+ "subagent-test.txt in the current directory and reports its contents. "
62-
+ "You must use the task tool."
63-
},
64-
timeout: TimeSpan.FromSeconds(120));
71+
parentWaiting.TrySetResult(true);
72+
}
73+
});
74+
try
75+
{
76+
var response = await session.SendAndWaitAsync(
77+
new MessageOptions
78+
{
79+
Prompt = "Use the task tool to spawn an explore agent that reads the file "
80+
+ "subagent-test.txt in the current directory and reports its contents. "
81+
+ "You must use the task tool."
82+
},
83+
timeout: TimeSpan.FromSeconds(120));
84+
Assert.NotNull(response);
85+
Assert.True(string.IsNullOrEmpty(response.AgentId));
86+
Assert.Equal(finalText, response.Data.Content);
87+
var replies = (await session.GetEventsAsync()).OfType<AssistantMessageEvent>()
88+
.Where(message => string.IsNullOrEmpty(message.AgentId))
89+
.Select(message => message.Data.Content)
90+
.Where(content => content == waitingText || content == finalText);
91+
Assert.Equal([waitingText, finalText], replies);
92+
}
93+
finally
94+
{
95+
parentWaiting.TrySetResult(true);
96+
}
6597

6698
var log = hookLog.ToArray();
6799

‎dotnet/test/Harness/E2ETestContext.cs‎

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public sealed class E2ETestContext : IAsyncDisposable
2020

2121
public string HomeDir { get; }
2222
public string WorkDir { get; }
23-
public string ProxyUrl { get; }
23+
public string ProxyUrl { get; private set; }
2424
internal static bool UsesInProcessTransport => IsInProcess(null);
2525

2626
/// <summary>Optional logger injected by tests; applied to all clients created via <see cref="CreateClient"/>.</summary>
@@ -47,7 +47,12 @@ private E2ETestContext(string homeDir, string workDir, string proxyUrl, ReplayPr
4747
_legacyCliPath = GetCachedCliPath(repoRoot, "--print-legacy-path");
4848
}
4949

50-
public static async Task<E2ETestContext> CreateAsync()
50+
public static Task<E2ETestContext> CreateAsync() => CreateAsync(
51+
new ReplayProxy(),
52+
Path.Combine(Path.GetTempPath(), $"copilot-test-config-{Guid.NewGuid()}"),
53+
Path.Combine(Path.GetTempPath(), $"copilot-test-work-{Guid.NewGuid()}"));
54+
55+
internal static async Task<E2ETestContext> CreateAsync(ReplayProxy proxy, string homeDir, string workDir)
5156
{
5257
// A previous in-process context may have left this process's cwd inside a work
5358
// directory that has since been deleted. getcwd() then fails, which breaks
@@ -57,33 +62,45 @@ public static async Task<E2ETestContext> CreateAsync()
5762

5863
var repoRoot = FindRepoRoot();
5964

60-
var homeDir = Path.Combine(Path.GetTempPath(), $"copilot-test-config-{Guid.NewGuid()}");
61-
var workDir = Path.Combine(Path.GetTempPath(), $"copilot-test-work-{Guid.NewGuid()}");
62-
63-
Directory.CreateDirectory(homeDir);
64-
Directory.CreateDirectory(workDir);
65-
6665
// Resolve symlinks (e.g., macOS /var -> /private/var) so paths
6766
// match what spawned subprocesses see when they resolve their cwd.
6867
homeDir = ResolveSymlinks(homeDir);
6968
workDir = ResolveSymlinks(workDir);
7069

71-
var proxy = new ReplayProxy();
72-
var proxyUrl = await proxy.StartAsync();
73-
// Creating an in-process fixture applies this URL before its first
74-
// test-specific configuration is posted, so early runtime requests need
75-
// an empty but valid replay state.
76-
await proxy.ConfigureAsync(
77-
Path.Combine(workDir, "__unconfigured__.yaml"),
78-
workDir,
79-
"capi");
80-
await proxy.SetCopilotUserByTokenAsync(DefaultGitHubToken, new CopilotUserConfig(
81-
Login: "e2e-test-user",
82-
CopilotPlan: "individual_pro",
83-
Endpoints: new CopilotUserEndpoints(Api: proxyUrl, Telemetry: "https://localhost:1/telemetry"),
84-
AnalyticsTrackingId: "e2e-test-tracking-id", Id: 12345));
85-
86-
return new E2ETestContext(homeDir, workDir, proxyUrl, proxy, repoRoot);
70+
var context = new E2ETestContext(homeDir, workDir, string.Empty, proxy, repoRoot);
71+
try
72+
{
73+
Directory.CreateDirectory(homeDir);
74+
Directory.CreateDirectory(workDir);
75+
76+
context.ProxyUrl = await proxy.StartAsync();
77+
// Creating an in-process fixture applies this URL before its first
78+
// test-specific configuration is posted, so early runtime requests need
79+
// an empty but valid replay state.
80+
await proxy.ConfigureAsync(
81+
Path.Combine(workDir, "__unconfigured__.yaml"),
82+
workDir,
83+
"capi");
84+
await proxy.SetCopilotUserByTokenAsync(DefaultGitHubToken, new CopilotUserConfig(
85+
Login: "e2e-test-user",
86+
CopilotPlan: "individual_pro",
87+
Endpoints: new CopilotUserEndpoints(Api: context.ProxyUrl, Telemetry: "https://localhost:1/telemetry"),
88+
AnalyticsTrackingId: "e2e-test-tracking-id", Id: 12345));
89+
90+
return context;
91+
}
92+
catch (Exception startupError)
93+
{
94+
try
95+
{
96+
await context.DisposeAsync();
97+
}
98+
catch (Exception cleanupError)
99+
{
100+
throw new AggregateException(startupError, cleanupError);
101+
}
102+
throw;
103+
}
87104
}
88105

89106
/// <summary>

‎dotnet/test/Harness/E2ETestFixture.cs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ internal static RuntimeConnection CreateSharedConnection(bool useInProcessTransp
3030

3131
public async Task DisposeAsync()
3232
{
33-
await Ctx.DisposeAsync();
33+
if (Ctx is not null)
34+
{
35+
await Ctx.DisposeAsync();
36+
}
3437
}
3538
}

‎dotnet/test/Harness/ReplayProxy.cs‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,17 @@ public async Task StopAsync(bool skipWritingCache = false)
139139
catch { /* Best effort */ }
140140
}
141141

142-
if (_process is { HasExited: false })
142+
try
143143
{
144-
try { _process.Kill(entireProcessTree: true); await _process.WaitForExitAsync(); }
145-
catch { /* Ignore */ }
144+
if (_process is { HasExited: false })
145+
{
146+
try { _process.Kill(entireProcessTree: true); await _process.WaitForExitAsync(); }
147+
catch { /* Ignore */ }
148+
}
149+
}
150+
catch (InvalidOperationException)
151+
{
152+
// Process.Start failed before this object was associated with a child.
146153
}
147154

148155
_process?.Dispose();

0 commit comments

Comments
 (0)