Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dotnet/src/Generated/Rpc.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

281 changes: 281 additions & 0 deletions dotnet/test/E2E/ClientOptionsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,205 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Create_Wire_Req
await session.DisposeAsync();
}

[Fact]
public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
var outputDirectory = Path.Join(Ctx.WorkDir, "large-output-create");

await using var client = Ctx.CreateClient(options: new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
UseLoggedInUser = false,
});

await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
{
ClientName = "advanced-create-client",
Model = "claude-sonnet-4.5",
ReasoningEffort = "medium",
ReasoningSummary = ReasoningSummary.Detailed,
ContextTier = ContextTier.LongContext,
EnableCitations = true,
Capi = new CapiSessionOptions { EnableWebSocketResponses = false },
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
CustomAgents =
[
new CustomAgentConfig
{
Name = "agent-one",
DisplayName = "Agent One",
Description = "Handles agent-one tasks.",
Prompt = "Be agent one.",
Tools = ["view"],
Infer = true,
Skills = ["create-skill"],
Model = "claude-haiku-4.5",
},
],
DefaultAgent = new DefaultAgentConfig { ExcludedTools = ["edit"] },
Agent = "agent-one",
SkillDirectories = ["skills-create"],
DisabledSkills = ["disabled-create-skill"],
PluginDirectories = ["plugins-create"],
InfiniteSessions = new InfiniteSessionConfig
{
Enabled = false,
BackgroundCompactionThreshold = 0.5,
BufferExhaustionThreshold = 0.9,
},
LargeOutput = new LargeToolOutputConfig
{
Enabled = true,
MaxSizeBytes = 4096,
OutputDirectory = outputDirectory,
},
Memory = new MemoryConfiguration { Enabled = true },
GitHubToken = "session-create-token",
RemoteSession = GitHub.Copilot.Rpc.RemoteSessionMode.Export,
Cloud = new CloudSessionOptions
{
Repository = new CloudSessionRepository
{
Owner = "github",
Name = "copilot-sdk",
Branch = "main",
},
},
EnableMcpApps = true,
RequestCanvasRenderer = true,
RequestExtensions = true,
ExtensionSdkPath = "custom-extension-sdk",
ExtensionInfo = new ExtensionInfo { Source = "dotnet-sdk-tests", Name = "advanced-create-extension" },
Canvases =
[
new CanvasDeclaration
{
Id = "advanced-create-canvas",
DisplayName = "Advanced Create Canvas",
Description = "Covers create-time canvas options.",
},
],
Providers =
[
new NamedProviderConfig
{
Name = "create-provider",
Type = "openai",
WireApi = "responses",
BaseUrl = "https://create-provider.example.test/v1",
ApiKey = "create-provider-key",
Headers = new Dictionary<string, string> { ["X-Create-Provider"] = "yes" },
},
],
Models =
[
new ProviderModelConfig
{
Provider = "create-provider",
Id = "create-model",
Name = "Create Model",
ModelId = "claude-sonnet-4.5",
WireModel = "create-wire-model",
MaxContextWindowTokens = 12_000,
MaxPromptTokens = 10_000,
MaxOutputTokens = 2_000,
},
],
OnPermissionRequest = PermissionHandler.ApproveAll,
});

using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create");
Assert.Equal("advanced-create-client", createRequest.GetProperty("clientName").GetString());
Assert.Equal("claude-sonnet-4.5", createRequest.GetProperty("model").GetString());
Assert.Equal("medium", createRequest.GetProperty("reasoningEffort").GetString());
Assert.Equal("detailed", createRequest.GetProperty("reasoningSummary").GetString());
Assert.Equal("long_context", createRequest.GetProperty("contextTier").GetString());
Assert.True(createRequest.GetProperty("enableCitations").GetBoolean());
Assert.False(createRequest.GetProperty("capi").GetProperty("enableWebSocketResponses").GetBoolean());
Assert.Equal("persistent", createRequest.GetProperty("mcpOAuthTokenStorage").GetString());
Assert.Equal("agent-one", createRequest.GetProperty("agent").GetString());
Assert.Equal("edit", createRequest.GetProperty("defaultAgent").GetProperty("excludedTools")[0].GetString());
Assert.Equal("agent-one", createRequest.GetProperty("customAgents")[0].GetProperty("name").GetString());
Assert.Equal("plugins-create", createRequest.GetProperty("pluginDirectories")[0].GetString());
Assert.Equal("disabled-create-skill", createRequest.GetProperty("disabledSkills")[0].GetString());
Assert.False(createRequest.GetProperty("infiniteSessions").GetProperty("enabled").GetBoolean());
Assert.True(createRequest.GetProperty("largeOutput").GetProperty("enabled").GetBoolean());
Assert.Equal(4096, createRequest.GetProperty("largeOutput").GetProperty("maxSizeBytes").GetInt64());
Assert.Equal(outputDirectory, createRequest.GetProperty("largeOutput").GetProperty("outputDir").GetString());
Assert.True(createRequest.GetProperty("memory").GetProperty("enabled").GetBoolean());
Assert.Equal("session-create-token", createRequest.GetProperty("gitHubToken").GetString());
Assert.Equal("export", createRequest.GetProperty("remoteSession").GetString());
Assert.Equal("github", createRequest.GetProperty("cloud").GetProperty("repository").GetProperty("owner").GetString());
Assert.True(createRequest.GetProperty("requestMcpApps").GetBoolean());
Assert.True(createRequest.GetProperty("requestCanvasRenderer").GetBoolean());
Assert.True(createRequest.GetProperty("requestExtensions").GetBoolean());
Assert.Equal("custom-extension-sdk", createRequest.GetProperty("extensionSdkPath").GetString());
Assert.Equal("advanced-create-extension", createRequest.GetProperty("extensionInfo").GetProperty("name").GetString());
Assert.Equal("advanced-create-canvas", createRequest.GetProperty("canvases")[0].GetProperty("id").GetString());
Assert.Equal("create-provider", createRequest.GetProperty("providers")[0].GetProperty("name").GetString());
Assert.Equal("responses", createRequest.GetProperty("providers")[0].GetProperty("wireApi").GetString());
Assert.Equal("create-model", createRequest.GetProperty("models")[0].GetProperty("id").GetString());
Assert.Equal(12000, createRequest.GetProperty("models")[0].GetProperty("maxContextWindowTokens").GetInt32());

await session.DisposeAsync();
}

[Fact]
public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();

await using var client = Ctx.CreateClient(options: new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
UseLoggedInUser = false,
});

await client.StartAsync();

var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "claude-sonnet-4.5",
Provider = new ProviderConfig
{
Type = "azure",
WireApi = "responses",
Transport = "http",
BaseUrl = "https://azure-provider.example.test/openai",
ApiKey = "provider-api-key",
BearerToken = "provider-bearer-token",
Azure = new AzureOptions { ApiVersion = "2024-02-15-preview" },
Headers = new Dictionary<string, string> { ["X-Provider-Wire"] = "yes" },
ModelId = "claude-sonnet-4.5",
WireModel = "azure-deployment",
MaxPromptTokens = 8192,
MaxOutputTokens = 1024,
},
OnPermissionRequest = PermissionHandler.ApproveAll,
});

using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
var provider = GetCapturedRequestParams(capture.RootElement, "session.create").GetProperty("provider");
Assert.Equal("azure", provider.GetProperty("type").GetString());
Assert.Equal("responses", provider.GetProperty("wireApi").GetString());
Assert.Equal("http", provider.GetProperty("transport").GetString());
Assert.Equal("https://azure-provider.example.test/openai", provider.GetProperty("baseUrl").GetString());
Assert.Equal("provider-api-key", provider.GetProperty("apiKey").GetString());
Assert.Equal("provider-bearer-token", provider.GetProperty("bearerToken").GetString());
Assert.Equal("2024-02-15-preview", provider.GetProperty("azure").GetProperty("apiVersion").GetString());
Assert.Equal("yes", provider.GetProperty("headers").GetProperty("X-Provider-Wire").GetString());
Assert.Equal("claude-sonnet-4.5", provider.GetProperty("modelId").GetString());
Assert.Equal("azure-deployment", provider.GetProperty("wireModel").GetString());
Assert.Equal(8192, provider.GetProperty("maxPromptTokens").GetInt32());
Assert.Equal(1024, provider.GetProperty("maxOutputTokens").GetInt32());

await session.DisposeAsync();
}

[Fact]
public async Task Should_Apply_Empty_Mode_Defaults_To_CreateSession_Wire_Request()
{
Expand Down Expand Up @@ -411,6 +610,88 @@ public async Task Should_Forward_Granular_Multitenancy_Fields_In_Resume_Wire_Req
await session.DisposeAsync();
}

[Fact]
public async Task Should_Forward_Advanced_Session_Options_In_Resume_Wire_Request()
{
var (cliPath, capturePath) = await CreateFakeCliCaptureAsync();
var outputDirectory = Path.Join(Ctx.WorkDir, "large-output-resume");
using var canvasInput = JsonDocument.Parse("{\"start\":41}");

await using var client = Ctx.CreateClient(options: new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
UseLoggedInUser = false,
});

await client.StartAsync();

var session = await client.ResumeSessionAsync("advanced-resume-session", new ResumeSessionConfig
{
ClientName = "advanced-resume-client",
Model = "claude-haiku-4.5",
ReasoningEffort = "low",
ReasoningSummary = ReasoningSummary.None,
ContextTier = ContextTier.Default,
SuppressResumeEvent = true,
ContinuePendingWork = true,
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
PluginDirectories = ["plugins-resume"],
LargeOutput = new LargeToolOutputConfig
{
Enabled = false,
MaxSizeBytes = 2048,
OutputDirectory = outputDirectory,
},
Memory = new MemoryConfiguration { Enabled = false },
RemoteSession = GitHub.Copilot.Rpc.RemoteSessionMode.On,
OpenCanvases =
[
new GitHub.Copilot.Rpc.OpenCanvasInstance
{
CanvasId = "resume-canvas",
ExtensionId = "dotnet-sdk-tests/resume-extension",
ExtensionName = "Resume Extension",
InstanceId = "resume-canvas-1",
Input = canvasInput.RootElement.Clone(),
Status = "ready",
Title = "Resume Canvas",
Url = "https://example.com/resume-canvas",
},
],
OnPermissionRequest = PermissionHandler.ApproveAll,
});

using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath));
var resumeRequest = GetCapturedRequestParams(capture.RootElement, "session.resume");
Assert.Equal("advanced-resume-session", resumeRequest.GetProperty("sessionId").GetString());
Assert.Equal("advanced-resume-client", resumeRequest.GetProperty("clientName").GetString());
Assert.Equal("claude-haiku-4.5", resumeRequest.GetProperty("model").GetString());
Assert.Equal("low", resumeRequest.GetProperty("reasoningEffort").GetString());
Assert.Equal("none", resumeRequest.GetProperty("reasoningSummary").GetString());
Assert.Equal("default", resumeRequest.GetProperty("contextTier").GetString());
Assert.True(resumeRequest.GetProperty("disableResume").GetBoolean());
Assert.True(resumeRequest.GetProperty("continuePendingWork").GetBoolean());
Assert.Equal("persistent", resumeRequest.GetProperty("mcpOAuthTokenStorage").GetString());
Assert.Equal("plugins-resume", resumeRequest.GetProperty("pluginDirectories")[0].GetString());
Assert.False(resumeRequest.GetProperty("largeOutput").GetProperty("enabled").GetBoolean());
Assert.Equal(2048, resumeRequest.GetProperty("largeOutput").GetProperty("maxSizeBytes").GetInt64());
Assert.Equal(outputDirectory, resumeRequest.GetProperty("largeOutput").GetProperty("outputDir").GetString());
Assert.False(resumeRequest.GetProperty("memory").GetProperty("enabled").GetBoolean());
Assert.Equal("on", resumeRequest.GetProperty("remoteSession").GetString());

var openCanvas = resumeRequest.GetProperty("openCanvases")[0];
Assert.Equal("resume-canvas", openCanvas.GetProperty("canvasId").GetString());
Assert.Equal("dotnet-sdk-tests/resume-extension", openCanvas.GetProperty("extensionId").GetString());
Assert.Equal("Resume Extension", openCanvas.GetProperty("extensionName").GetString());
Assert.Equal("resume-canvas-1", openCanvas.GetProperty("instanceId").GetString());
Assert.Equal(41, openCanvas.GetProperty("input").GetProperty("start").GetInt32());
Assert.Equal("ready", openCanvas.GetProperty("status").GetString());
Assert.Equal("Resume Canvas", openCanvas.GetProperty("title").GetString());
Assert.Equal("https://example.com/resume-canvas", openCanvas.GetProperty("url").GetString());

await session.DisposeAsync();
}

[Fact]
public async Task Should_Apply_Empty_Mode_Defaults_To_ResumeSession_Wire_Request()
{
Expand Down
51 changes: 51 additions & 0 deletions dotnet/test/E2E/McpOAuthE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,57 @@ public async Task Should_Satisfy_MCP_OAuth_Using_Host_Provided_Token()
Assert.Contains(requests, request => request.Authorization == $"Bearer {ExpectedToken}");
}

[Fact]
public async Task Should_Resolve_Pending_MCP_OAuth_Request_With_Direct_Rpc()
{
await using var oauthServer = await OAuthMcpServer.StartAsync(ExpectedToken);
var serverName = "oauth-direct-rpc-mcp";
var authRequest = new TaskCompletionSource<McpAuthContext>(TaskCreationOptions.RunContinuationsAsynchronously);
var releaseHandler = new TaskCompletionSource<McpAuthResult?>(TaskCreationOptions.RunContinuationsAsynchronously);

await using var session = await CreateSessionAsync(new SessionConfig
{
OnMcpAuthRequest = request =>
{
authRequest.TrySetResult(request);
return releaseHandler.Task;
},
McpServers = new Dictionary<string, McpServerConfig>
{
[serverName] = new McpHttpServerConfig
{
Url = $"{oauthServer.Url}/mcp",
Tools = ["*"],
},
},
});

var connected = WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected);
var request = await authRequest.Task.WaitAsync(TimeSpan.FromSeconds(30));
Assert.NotEmpty(request.RequestId);
Assert.Equal(serverName, request.ServerName);
Assert.Equal($"{oauthServer.Url}/mcp", request.ServerUrl);
Assert.Equal(McpOauthRequestReason.Initial, request.Reason);
Assert.NotNull(request.WwwAuthenticateParams);
Assert.Equal("mcp.read", request.WwwAuthenticateParams!.Scope);

var handled = await session.Rpc.Mcp.Oauth.HandlePendingRequestAsync(
request.RequestId,
new McpOauthPendingRequestResponseToken
{
AccessToken = ExpectedToken,
TokenType = "Bearer",
ExpiresIn = 3600,
});
Assert.True(handled.Success);

await connected;
var tools = await session.Rpc.Mcp.ListToolsAsync(serverName);
Assert.Contains(tools.Tools, tool => tool.Name == "whoami");

releaseHandler.SetResult(McpAuthResult.FromToken(new McpAuthToken { AccessToken = ExpectedToken }));
}

[Fact]
public async Task Should_Request_Replacement_Tokens_Across_MCP_OAuth_Lifecycle()
{
Expand Down
Loading
Loading