/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ using Xunit; using System.Collections.Generic; using System.Text.Json; #if !NET8_0_OR_GREATER using System.Runtime.Serialization; #endif using GitHub.Copilot.Rpc; namespace GitHub.Copilot.Test.Unit; /// /// Tests for JSON serialization compatibility with the SDK's configured options. /// public class SerializationTests { [Fact] public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() { var options = GetSerializerOptions(); var original = new ProviderConfig { BaseUrl = "https://example.com/provider", Headers = new Dictionary { ["Authorization"] = "Bearer provider-token" }, ModelId = "gpt-4o", WireModel = "my-finetune-v3", MaxPromptTokens = 100_000, MaxOutputTokens = 4096, Transport = "websockets" }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal("https://example.com/provider", root.GetProperty("baseUrl").GetString()); Assert.Equal("Bearer provider-token", root.GetProperty("headers").GetProperty("Authorization").GetString()); Assert.Equal("gpt-4o", root.GetProperty("modelId").GetString()); Assert.Equal("my-finetune-v3", root.GetProperty("wireModel").GetString()); Assert.Equal(100_000, root.GetProperty("maxPromptTokens").GetInt32()); Assert.Equal(4096, root.GetProperty("maxOutputTokens").GetInt32()); Assert.Equal("websockets", root.GetProperty("transport").GetString()); var deserialized = JsonSerializer.Deserialize(json, options); Assert.NotNull(deserialized); Assert.Equal("https://example.com/provider", deserialized.BaseUrl); Assert.Equal("Bearer provider-token", deserialized.Headers!["Authorization"]); Assert.Equal("gpt-4o", deserialized.ModelId); Assert.Equal("my-finetune-v3", deserialized.WireModel); Assert.Equal(100_000, deserialized.MaxPromptTokens); Assert.Equal(4096, deserialized.MaxOutputTokens); Assert.Equal("websockets", deserialized.Transport); } [Fact] public void CapiSessionOptions_CanSerializeEnableWebSocketResponses_WithSdkOptions() { var options = GetSerializerOptions(); var original = new CapiSessionOptions { EnableWebSocketResponses = false }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.False(root.GetProperty("enableWebSocketResponses").GetBoolean()); var deserialized = JsonSerializer.Deserialize(json, options); Assert.NotNull(deserialized); Assert.False(deserialized.EnableWebSocketResponses); } [Fact] public void ModelBilling_CanSerializeTokenPrices_WithSdkOptions() { var options = GetSerializerOptions(); var original = new ModelBilling { Multiplier = 1.5, TokenPrices = new GitHub.Copilot.Rpc.ModelBillingTokenPrices { InputPrice = 2.0, OutputPrice = 8.0, CacheReadPrice = 0.5, CacheWritePrice = 0.75, BatchSize = 1_000_000L, MaxPromptTokens = 128_000L, LongContext = new GitHub.Copilot.Rpc.ModelBillingTokenPricesLongContext { InputPrice = 4.0, OutputPrice = 16.0, CacheReadPrice = 1.0, CacheWritePrice = 1.25, MaxPromptTokens = 1_000_000L } } }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal(1.5, root.GetProperty("multiplier").GetDouble()); var tokenPrices = root.GetProperty("tokenPrices"); Assert.Equal(2.0, tokenPrices.GetProperty("inputPrice").GetDouble()); Assert.Equal(8.0, tokenPrices.GetProperty("outputPrice").GetDouble()); Assert.Equal(0.5, tokenPrices.GetProperty("cacheReadPrice").GetDouble()); Assert.Equal(0.75, tokenPrices.GetProperty("cacheWritePrice").GetDouble()); Assert.Equal(1_000_000L, tokenPrices.GetProperty("batchSize").GetInt64()); Assert.Equal(128_000L, tokenPrices.GetProperty("maxPromptTokens").GetInt64()); var longContext = tokenPrices.GetProperty("longContext"); Assert.Equal(4.0, longContext.GetProperty("inputPrice").GetDouble()); Assert.Equal(1.25, longContext.GetProperty("cacheWritePrice").GetDouble()); Assert.Equal(1_000_000L, longContext.GetProperty("maxPromptTokens").GetInt64()); var deserialized = JsonSerializer.Deserialize(json, options); Assert.NotNull(deserialized); Assert.Equal(1.5, deserialized.Multiplier); Assert.NotNull(deserialized.TokenPrices); Assert.Equal(2.0, deserialized.TokenPrices.InputPrice); Assert.Equal(1_000_000L, deserialized.TokenPrices.BatchSize); Assert.Equal(128_000L, deserialized.TokenPrices.MaxPromptTokens); Assert.NotNull(deserialized.TokenPrices.LongContext); Assert.Equal(16.0, deserialized.TokenPrices.LongContext.OutputPrice); Assert.Equal(1_000_000L, deserialized.TokenPrices.LongContext.MaxPromptTokens); } [Fact] public void MessageOptions_CanSerializeRequestHeaders_WithSdkOptions() { var options = GetSerializerOptions(); var original = new MessageOptions { Prompt = "real prompt", Mode = "enqueue", RequestHeaders = new Dictionary { ["X-Trace"] = "trace-value" } }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal("real prompt", root.GetProperty("prompt").GetString()); Assert.Equal("enqueue", root.GetProperty("mode").GetString()); Assert.Equal("trace-value", root.GetProperty("requestHeaders").GetProperty("X-Trace").GetString()); var deserialized = JsonSerializer.Deserialize(json, options); Assert.NotNull(deserialized); Assert.Equal("real prompt", deserialized.Prompt); Assert.Equal("enqueue", deserialized.Mode); Assert.Equal("trace-value", deserialized.RequestHeaders!["X-Trace"]); } [Fact] public void SendMessageRequest_CanSerializeRequestHeaders_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotSession), "SendMessageRequest"); var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("Prompt", "real prompt"), ("Mode", "enqueue"), ("RequestHeaders", new Dictionary { ["X-Trace"] = "trace-value" })); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal("session-id", root.GetProperty("sessionId").GetString()); Assert.Equal("real prompt", root.GetProperty("prompt").GetString()); Assert.Equal("enqueue", root.GetProperty("mode").GetString()); Assert.Equal("trace-value", root.GetProperty("requestHeaders").GetProperty("X-Trace").GetString()); } [Fact] public void CreateSessionRequest_CanSerializeInstructionDirectories_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("InstructionDirectories", new List { "C:\\extra-instructions", "C:\\more-instructions" })); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal("C:\\extra-instructions", root.GetProperty("instructionDirectories")[0].GetString()); Assert.Equal("C:\\more-instructions", root.GetProperty("instructionDirectories")[1].GetString()); } [Fact] public void CreateSessionRequest_CanSerializeCloudOptions_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var request = CreateInternalRequest( requestType, ("Cloud", new CloudSessionOptions { Repository = new CloudSessionRepository { Owner = "github", Name = "copilot-sdk", Branch = "main" } })); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var repository = document.RootElement.GetProperty("cloud").GetProperty("repository"); Assert.Equal("github", repository.GetProperty("owner").GetString()); Assert.Equal("copilot-sdk", repository.GetProperty("name").GetString()); Assert.Equal("main", repository.GetProperty("branch").GetString()); } [Fact] public void CreateSessionRequest_CanSerializeModeRequestFlags_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var request = CreateInternalRequest( requestType, ("RequestExitPlanMode", true), ("RequestAutoModeSwitch", true)); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.True(root.GetProperty("requestExitPlanMode").GetBoolean()); Assert.True(root.GetProperty("requestAutoModeSwitch").GetBoolean()); } [Fact] public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("InstructionDirectories", new List { "C:\\resume-instructions" })); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal("C:\\resume-instructions", root.GetProperty("instructionDirectories")[0].GetString()); } [Fact] public void SessionRequests_CanSerializeCapiOptions_WithSdkOptions() { var options = GetSerializerOptions(); var capi = new CapiSessionOptions { EnableWebSocketResponses = false }; var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id"), ("Capi", capi)); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); Assert.False(createDocument.RootElement.GetProperty("capi").GetProperty("enableWebSocketResponses").GetBoolean()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("Capi", capi)); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); Assert.False(resumeDocument.RootElement.GetProperty("capi").GetProperty("enableWebSocketResponses").GetBoolean()); } [Fact] public void SessionRequests_OmitCapiOptions_WhenUnset() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id")); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); Assert.False(createDocument.RootElement.TryGetProperty("capi", out _)); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id")); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); Assert.False(resumeDocument.RootElement.TryGetProperty("capi", out _)); } [Fact] public void SessionRequests_CanSerializeGitHubMcpToolConfig_WithSdkOptions() { var options = GetSerializerOptions(); var githubConfig = new GitHubMcpToolConfig { EnableAllTools = true, AdditionalToolsets = ["repos"], AdditionalTools = ["get_issue"], EnableInsidersMode = true, DisableFormDeferral = true, }; var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("GitHubMcpToolConfig", githubConfig)); using var createDocument = JsonDocument.Parse(JsonSerializer.Serialize(createRequest, createRequestType, options)); var createConfig = createDocument.RootElement.GetProperty("githubMcpToolConfig"); Assert.True(createConfig.GetProperty("enableAllTools").GetBoolean()); Assert.Equal("repos", createConfig.GetProperty("additionalToolsets")[0].GetString()); Assert.True(createConfig.GetProperty("disableFormDeferral").GetBoolean()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("GitHubMcpToolConfig", githubConfig)); using var resumeDocument = JsonDocument.Parse(JsonSerializer.Serialize(resumeRequest, resumeRequestType, options)); Assert.True(resumeDocument.RootElement.TryGetProperty("githubMcpToolConfig", out _)); } [Fact] public void SessionRequests_OmitGitHubMcpToolConfig_WhenUnset() { var options = GetSerializerOptions(); foreach (var requestName in new[] { "CreateSessionRequest", "ResumeSessionRequest" }) { var requestType = GetNestedType(typeof(CopilotClient), requestName); var request = CreateInternalRequest(requestType, ("SessionId", "session-id")); using var document = JsonDocument.Parse(JsonSerializer.Serialize(request, requestType, options)); Assert.False(document.RootElement.TryGetProperty("githubMcpToolConfig", out _)); } } [Fact] public void SessionRequests_CanSerializeReasoningSummary_WithSdkOptions() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id"), ("ReasoningSummary", ReasoningSummary.Detailed)); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); Assert.Equal("detailed", createDocument.RootElement.GetProperty("reasoningSummary").GetString()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("ReasoningSummary", ReasoningSummary.None)); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); Assert.Equal("none", resumeDocument.RootElement.GetProperty("reasoningSummary").GetString()); } [Fact] public void SessionRequests_CanSerializeContextTier_WithSdkOptions() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id"), ("ContextTier", ContextTier.LongContext)); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); Assert.Equal("long_context", createDocument.RootElement.GetProperty("contextTier").GetString()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("ContextTier", ContextTier.Default)); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); Assert.Equal("default", resumeDocument.RootElement.GetProperty("contextTier").GetString()); } [Fact] public void SessionRequests_CanSerializePluginDirectoriesAndLargeOutput_WithSdkOptions() { var options = GetSerializerOptions(); var pluginDirs = new List { "/tmp/plugins/a", "/tmp/plugins/b" }; var largeOutput = new LargeToolOutputConfig { Enabled = true, MaxSizeBytes = 1024, OutputDirectory = "/tmp/large-output", }; var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id"), ("PluginDirectories", pluginDirs), ("DisabledMcpServers", new List { "local-files", "remote-github" }), ("LargeOutput", largeOutput)); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); var createRoot = createDocument.RootElement; Assert.Equal("/tmp/plugins/a", createRoot.GetProperty("pluginDirectories")[0].GetString()); Assert.Equal("local-files", createRoot.GetProperty("disabledMcpServers")[0].GetString()); Assert.Equal("/tmp/plugins/b", createRoot.GetProperty("pluginDirectories")[1].GetString()); var createLargeOutput = createRoot.GetProperty("largeOutput"); Assert.True(createLargeOutput.GetProperty("enabled").GetBoolean()); Assert.Equal(1024, createLargeOutput.GetProperty("maxSizeBytes").GetInt64()); Assert.Equal("/tmp/large-output", createLargeOutput.GetProperty("outputDir").GetString()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("PluginDirectories", pluginDirs), ("DisabledMcpServers", new List { "local-files", "remote-github" }), ("LargeOutput", largeOutput)); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); var resumeRoot = resumeDocument.RootElement; Assert.Equal("/tmp/plugins/a", resumeRoot.GetProperty("pluginDirectories")[0].GetString()); Assert.Equal("local-files", resumeRoot.GetProperty("disabledMcpServers")[0].GetString()); var resumeLargeOutput = resumeRoot.GetProperty("largeOutput"); Assert.True(resumeLargeOutput.GetProperty("enabled").GetBoolean()); Assert.Equal(1024, resumeLargeOutput.GetProperty("maxSizeBytes").GetInt64()); Assert.Equal("/tmp/large-output", resumeLargeOutput.GetProperty("outputDir").GetString()); } [Fact] public void SessionRequests_CanSerializeMemory_WithSdkOptions() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id"), ("Memory", new MemoryConfiguration { Enabled = true })); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); var createRoot = createDocument.RootElement; Assert.True(createRoot.GetProperty("memory").GetProperty("enabled").GetBoolean()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("Memory", new MemoryConfiguration { Enabled = false })); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); var resumeRoot = resumeDocument.RootElement; Assert.False(resumeRoot.GetProperty("memory").GetProperty("enabled").GetBoolean()); } [Fact] public void SessionRequests_CanSerializeCitationAgentExclusionAndLimits_WithSdkOptions() { var options = GetSerializerOptions(); var excludedAgents = new List { "explore", "task" }; var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id"), ("EnableCitations", true), ("EnableFileChangeTracking", true), ("ExcludedBuiltInAgents", excludedAgents), ("SessionLimits", new SessionLimitsConfig { MaxAiCredits = 12.5 })); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); var createRoot = createDocument.RootElement; Assert.True(createRoot.GetProperty("enableCitations").GetBoolean()); Assert.True(createRoot.GetProperty("enableFileChangeTracking").GetBoolean()); Assert.Equal("explore", createRoot.GetProperty("excludedBuiltinAgents")[0].GetString()); Assert.Equal(12.5, createRoot.GetProperty("sessionLimits").GetProperty("maxAiCredits").GetDouble()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("EnableCitations", true), ("EnableFileChangeTracking", true), ("ExcludedBuiltInAgents", excludedAgents), ("SessionLimits", new SessionLimitsConfig { MaxAiCredits = 7.25 })); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); var resumeRoot = resumeDocument.RootElement; Assert.True(resumeRoot.GetProperty("enableCitations").GetBoolean()); Assert.True(resumeRoot.GetProperty("enableFileChangeTracking").GetBoolean()); Assert.Equal("task", resumeRoot.GetProperty("excludedBuiltinAgents")[1].GetString()); Assert.Equal(7.25, resumeRoot.GetProperty("sessionLimits").GetProperty("maxAiCredits").GetDouble()); } [Fact] public void SessionRequests_OmitMemory_WhenUnset() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id")); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); Assert.False(createDocument.RootElement.TryGetProperty("memory", out _)); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id")); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); Assert.False(resumeDocument.RootElement.TryGetProperty("memory", out _)); } [Fact] public void SessionRequests_CanSerializeExpAssignments_WithSdkOptions() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id"), ("ExpAssignments", new CopilotExpAssignmentResponse { Configs = new List { new() { Id = "exp-create" } }, })); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); var createRoot = createDocument.RootElement; Assert.Equal("exp-create", createRoot.GetProperty("expAssignments").GetProperty("Configs")[0].GetProperty("Id").GetString()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("ExpAssignments", new CopilotExpAssignmentResponse { Configs = new List { new() { Id = "exp-resume" } }, })); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); var resumeRoot = resumeDocument.RootElement; Assert.Equal("exp-resume", resumeRoot.GetProperty("expAssignments").GetProperty("Configs")[0].GetProperty("Id").GetString()); } [Fact] public void SessionRequests_OmitExpAssignments_WhenUnset() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id")); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); Assert.False(createDocument.RootElement.TryGetProperty("expAssignments", out _)); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id")); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); Assert.False(resumeDocument.RootElement.TryGetProperty("expAssignments", out _)); } [Fact] public void SessionConfigClone_PreservesExpAssignments() { var config = new SessionConfig { SessionId = "session-id", ExpAssignments = new CopilotExpAssignmentResponse { Configs = new List { new() { Id = "exp-create" } }, }, }; var clone = config.Clone(); Assert.NotNull(clone.ExpAssignments); Assert.Equal("exp-create", clone.ExpAssignments!.Configs[0].Id); } [Fact] public void ResumeSessionConfigClone_PreservesExpAssignments() { var config = new ResumeSessionConfig { ExpAssignments = new CopilotExpAssignmentResponse { Configs = new List { new() { Id = "exp-resume" } }, }, }; var clone = config.Clone(); Assert.NotNull(clone.ExpAssignments); Assert.Equal("exp-resume", clone.ExpAssignments!.Configs[0].Id); } [Fact] public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("EnableSessionTelemetry", false)); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.False(root.GetProperty("enableSessionTelemetry").GetBoolean()); } [Fact] public void CreateSessionRequest_CanSerializeCustomAgentsLocalOnly_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("CustomAgentsLocalOnly", true)); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); Assert.True(document.RootElement.GetProperty("customAgentsLocalOnly").GetBoolean()); } [Fact] public void ResumeSessionRequest_CanSerializeCustomAgentsLocalOnly_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("CustomAgentsLocalOnly", true)); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); Assert.True(document.RootElement.GetProperty("customAgentsLocalOnly").GetBoolean()); } [Fact] public void SessionRequests_OmitCustomAgentsLocalOnly_WhenUnset() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id")); var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); Assert.False(createDocument.RootElement.TryGetProperty("customAgentsLocalOnly", out _)); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id")); var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); Assert.False(resumeDocument.RootElement.TryGetProperty("customAgentsLocalOnly", out _)); } [Fact] public void ResumeSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("EnableSessionTelemetry", false)); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.False(root.GetProperty("enableSessionTelemetry").GetBoolean()); } [Fact] public void SessionRequests_CanSerializeEnableExperimentalMode_WithSdkOptions() { var options = GetSerializerOptions(); var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( createRequestType, ("SessionId", "session-id"), ("IsExperimentalMode", false)); var createRoot = JsonDocument.Parse(JsonSerializer.Serialize(createRequest, createRequestType, options)).RootElement; Assert.False(createRoot.GetProperty("isExperimentalMode").GetBoolean()); var createRequestOmitted = CreateInternalRequest( createRequestType, ("SessionId", "session-id")); var createOmittedRoot = JsonDocument.Parse(JsonSerializer.Serialize(createRequestOmitted, createRequestType, options)).RootElement; Assert.False(createOmittedRoot.TryGetProperty("isExperimentalMode", out _)); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id"), ("IsExperimentalMode", true)); var resumeRoot = JsonDocument.Parse(JsonSerializer.Serialize(resumeRequest, resumeRequestType, options)).RootElement; Assert.True(resumeRoot.GetProperty("isExperimentalMode").GetBoolean()); var resumeRequestOmitted = CreateInternalRequest( resumeRequestType, ("SessionId", "session-id")); var resumeOmittedRoot = JsonDocument.Parse(JsonSerializer.Serialize(resumeRequestOmitted, resumeRequestType, options)).RootElement; Assert.False(resumeOmittedRoot.TryGetProperty("isExperimentalMode", out _)); } [Fact] public void CreateSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var requestTrue = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("EnableOnDemandInstructionDiscovery", true)); var rootTrue = JsonDocument.Parse(JsonSerializer.Serialize(requestTrue, requestType, options)).RootElement; Assert.True(rootTrue.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean()); var requestFalse = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("EnableOnDemandInstructionDiscovery", false)); var rootFalse = JsonDocument.Parse(JsonSerializer.Serialize(requestFalse, requestType, options)).RootElement; Assert.False(rootFalse.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean()); var requestOmitted = CreateInternalRequest( requestType, ("SessionId", "session-id")); var rootOmitted = JsonDocument.Parse(JsonSerializer.Serialize(requestOmitted, requestType, options)).RootElement; Assert.False(rootOmitted.TryGetProperty("enableOnDemandInstructionDiscovery", out _)); } [Fact] public void ResumeSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var requestTrue = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("EnableOnDemandInstructionDiscovery", true)); var rootTrue = JsonDocument.Parse(JsonSerializer.Serialize(requestTrue, requestType, options)).RootElement; Assert.True(rootTrue.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean()); var requestFalse = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("EnableOnDemandInstructionDiscovery", false)); var rootFalse = JsonDocument.Parse(JsonSerializer.Serialize(requestFalse, requestType, options)).RootElement; Assert.False(rootFalse.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean()); var requestOmitted = CreateInternalRequest( requestType, ("SessionId", "session-id")); var rootOmitted = JsonDocument.Parse(JsonSerializer.Serialize(requestOmitted, requestType, options)).RootElement; Assert.False(rootOmitted.TryGetProperty("enableOnDemandInstructionDiscovery", out _)); } [Fact] public void ResumeSessionRequest_CanSerializeOpenCanvases_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var instances = new List { new() { CanvasId = "canvas-id", ExtensionId = "ext-id", InstanceId = "instance-1", }, }; var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("OpenCanvases", instances)); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; var openCanvases = root.GetProperty("openCanvases"); Assert.Equal(1, openCanvases.GetArrayLength()); Assert.Equal("canvas-id", openCanvases[0].GetProperty("canvasId").GetString()); } [Fact] public void ResumeSessionRequest_CanSerializeModeRequestFlags_WithSdkOptions() { var options = GetSerializerOptions(); var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var request = CreateInternalRequest( requestType, ("SessionId", "session-id"), ("RequestExitPlanMode", true), ("RequestAutoModeSwitch", true)); var json = JsonSerializer.Serialize(request, requestType, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.True(root.GetProperty("requestExitPlanMode").GetBoolean()); Assert.True(root.GetProperty("requestAutoModeSwitch").GetBoolean()); } [Fact] public void AutoModeSwitchResponse_CanSerialize_WithSdkOptions() { var options = GetSerializerOptions(); var json = JsonSerializer.Serialize(AutoModeSwitchResponse.YesAlways, options); Assert.Equal("\"yes_always\"", json); } [Fact] public void McpHttpServerConfig_CanSerializeOauthOptions_WithSdkOptions() { var options = GetSerializerOptions(); McpServerConfig original = new McpHttpServerConfig { Url = "https://example.com/mcp", Headers = new Dictionary { ["Authorization"] = "Bearer token" }, OauthClientId = "client-id", OauthPublicClient = false, OauthGrantType = McpHttpServerConfigOauthGrantType.ClientCredentials, Tools = ["*"], Timeout = 3000 }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal("http", root.GetProperty("type").GetString()); Assert.Equal("https://example.com/mcp", root.GetProperty("url").GetString()); Assert.Equal("Bearer token", root.GetProperty("headers").GetProperty("Authorization").GetString()); Assert.Equal("client-id", root.GetProperty("oauthClientId").GetString()); Assert.False(root.GetProperty("oauthPublicClient").GetBoolean()); Assert.Equal("client_credentials", root.GetProperty("oauthGrantType").GetString()); Assert.Equal("*", root.GetProperty("tools")[0].GetString()); Assert.Equal(3000, root.GetProperty("timeout").GetInt32()); var deserialized = JsonSerializer.Deserialize(json, options); var httpConfig = Assert.IsType(deserialized); Assert.Equal("https://example.com/mcp", httpConfig.Url); Assert.Equal("Bearer token", httpConfig.Headers!["Authorization"]); Assert.Equal("client-id", httpConfig.OauthClientId); Assert.False(httpConfig.OauthPublicClient); Assert.Equal(McpHttpServerConfigOauthGrantType.ClientCredentials, httpConfig.OauthGrantType); Assert.Equal("*", Assert.Single(httpConfig.Tools!)); Assert.Equal(3000, httpConfig.Timeout); } [Fact] public void QueuedCommandResult_SerializesHandledAsBoolean_WithSdkOptions() { var options = GetSerializerOptions(); var original = new QueuedCommandResult { Handled = true, StopProcessingQueue = false }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal(JsonValueKind.True, root.GetProperty("handled").ValueKind); Assert.Equal(JsonValueKind.False, root.GetProperty("stopProcessingQueue").ValueKind); var deserialized = JsonSerializer.Deserialize("""{"handled":false}""", options); Assert.NotNull(deserialized); Assert.False(deserialized.Handled); Assert.Null(deserialized.StopProcessingQueue); } [Fact] public void PermissionDecision_SerializesBaseDiscriminator_WithSdkOptions() { var options = GetSerializerOptions(); var original = PermissionDecision.ApproveOnce(); var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); Assert.Equal("approve-once", document.RootElement.GetProperty("kind").GetString()); } [Fact] public void AgentStopHookInput_DeserializesWireFields_WithSdkOptions() { var options = GetSerializerOptions(); var input = JsonSerializer.Deserialize( """ { "sessionId": "session-1", "timestamp": 1700000000000, "cwd": "/repo", "stopReason": "end_turn", "transcriptPath": "/tmp/transcript.jsonl", "stop_hook_active": true } """, options); Assert.NotNull(input); Assert.Equal("session-1", input.SessionId); Assert.Equal("/repo", input.WorkingDirectory); Assert.Equal("end_turn", input.StopReason); Assert.Equal("/tmp/transcript.jsonl", input.TranscriptPath); Assert.True(input.StopHookActive); Assert.Equal(DateTimeOffset.FromUnixTimeMilliseconds(1700000000000), input.Timestamp); } [Fact] public void AgentStopHookOutput_SerializesBlockDecision_WithSdkOptions() { var options = GetSerializerOptions(); var output = new AgentStopHookOutput { Decision = "block", Reason = "finish the remaining work" }; var json = JsonSerializer.SerializeToElement(output, options); Assert.Equal("block", json.GetProperty("decision").GetString()); Assert.Equal("finish the remaining work", json.GetProperty("reason").GetString()); } [Fact] public void HooksInvokeResponse_SerializesPreMcpToolCallHookOutput_WithMetaToUse() { var options = GetSerializerOptions(); // Create the PreMcpToolCallHookOutput with meta using var doc = JsonDocument.Parse("""{"injected":"by-hook","source":"test"}"""); var meta = doc.RootElement.Clone(); var hookOutput = new PreMcpToolCallHookOutput { MetaToUse = meta }; // Create the HooksInvokeResponse using reflection (it's internal) var responseType = GetNestedType(typeof(CopilotClient), "HooksInvokeResponse"); var response = CreateInternalRequest(responseType, ("Output", hookOutput)); // Serialize using the exact same path as SendResultResponseAsync var typeInfo = options.GetTypeInfo(response.GetType()); var json = JsonSerializer.SerializeToElement(response, typeInfo); // The JSON should be {"output":{"metaToUse":{"injected":"by-hook","source":"test"}}} Assert.True(json.TryGetProperty("output", out var outputProp), $"Expected 'output' property. Got: {json}"); Assert.True(outputProp.TryGetProperty("metaToUse", out var metaToUseProp), $"Expected 'metaToUse' property. Got: {outputProp}"); Assert.Equal("by-hook", metaToUseProp.GetProperty("injected").GetString()); Assert.Equal("test", metaToUseProp.GetProperty("source").GetString()); } [Fact] public void HooksInvokeResponse_SerializesPreMcpToolCallHookOutput_WithNullMetaToUse() { var options = GetSerializerOptions(); // Create the PreMcpToolCallHookOutput with null meta (remove meta) var hookOutput = new PreMcpToolCallHookOutput { MetaToUse = null }; // Create the HooksInvokeResponse using reflection (it's internal) var responseType = GetNestedType(typeof(CopilotClient), "HooksInvokeResponse"); var response = CreateInternalRequest(responseType, ("Output", hookOutput)); // Serialize var typeInfo = options.GetTypeInfo(response.GetType()); var json = JsonSerializer.SerializeToElement(response, typeInfo); // Should be {"output":{"metaToUse":null}} Assert.True(json.TryGetProperty("output", out var outputProp), $"Expected 'output' property. Got: {json}"); Assert.True(outputProp.TryGetProperty("metaToUse", out var metaToUseProp), $"Expected 'metaToUse' property. Got: {outputProp}"); Assert.Equal(JsonValueKind.Null, metaToUseProp.ValueKind); } [Fact] public void HooksInvokeResponse_SerializesNullOutput_AsEmptyOrNoOutputProperty() { var options = GetSerializerOptions(); // Create the HooksInvokeResponse with null Output (preserve meta) var responseType = GetNestedType(typeof(CopilotClient), "HooksInvokeResponse"); var response = CreateInternalRequest(responseType, ("Output", (object?)null)); // Serialize var typeInfo = options.GetTypeInfo(response.GetType()); var json = JsonSerializer.SerializeToElement(response, typeInfo); // With WhenWritingNull, output property should be omitted when null // OR if present, should be null if (json.TryGetProperty("output", out var outputProp)) { Assert.Equal(JsonValueKind.Null, outputProp.ValueKind); } // else: property omitted, which is fine (runtime treats undefined output as no-op) } [Fact] public void ToolResultObject_SerializesToolReferences_WithSdkOptions() { var options = GetSerializerOptions(); var original = new ToolResultObject { TextResultForLlm = "found 2 tools", ResultType = "success", ToolReferences = ["get_weather", "check_status"], }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; Assert.Equal("found 2 tools", root.GetProperty("textResultForLlm").GetString()); var refs = root.GetProperty("toolReferences"); Assert.Equal(JsonValueKind.Array, refs.ValueKind); Assert.Equal(2, refs.GetArrayLength()); Assert.Equal("get_weather", refs[0].GetString()); Assert.Equal("check_status", refs[1].GetString()); var deserialized = JsonSerializer.Deserialize(json, options); Assert.NotNull(deserialized); string[] expectedReferences = ["get_weather", "check_status"]; Assert.Equal(expectedReferences, deserialized!.ToolReferences); } [Fact] public void ToolResultObject_OmitsToolReferences_WhenNull_WithSdkOptions() { var options = GetSerializerOptions(); var original = new ToolResultObject { TextResultForLlm = "ok", ResultType = "success", }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); Assert.False(document.RootElement.TryGetProperty("toolReferences", out _)); } private static JsonSerializerOptions GetSerializerOptions() { var prop = typeof(CopilotClient) .GetProperty("SerializerOptionsForMessageFormatter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); var options = (JsonSerializerOptions?)prop?.GetValue(null); Assert.NotNull(options); return options; } private static Type GetNestedType(Type containingType, string name) { var type = containingType.GetNestedType(name, System.Reflection.BindingFlags.NonPublic); Assert.NotNull(type); return type!; } [Fact] public void HooksInvokeResponse_SerializesBoxedJsonElement_AsOutput() { // This tests the EXACT path used by SerializeHookOutput: // PreMcpToolCallHookOutput -> serialize to JsonElement -> box as object? in HooksInvokeResponse.Output var options = GetSerializerOptions(); using var metaDoc = JsonDocument.Parse("""{"injected":"by-hook","source":"test"}"""); var hookOutput = new PreMcpToolCallHookOutput { MetaToUse = metaDoc.RootElement.Clone() }; // SerializeHookOutput returns a JsonElement (value type) var hookTypeInfo = options.GetTypeInfo(typeof(PreMcpToolCallHookOutput)); JsonElement serializedOutput = JsonSerializer.SerializeToElement(hookOutput, hookTypeInfo); // HooksInvokeResponse stores this as object? (boxed JsonElement) var responseType = GetNestedType(typeof(CopilotClient), "HooksInvokeResponse"); var response = CreateInternalRequest(responseType, ("Output", (object)serializedOutput)); // Serialize via GetTypeInfo(response.GetType()) — same as SendResultResponseAsync var typeInfo = options.GetTypeInfo(response.GetType()); var json = JsonSerializer.SerializeToElement(response, typeInfo); // Expected: {"output":{"metaToUse":{"injected":"by-hook","source":"test"}}} Assert.True(json.TryGetProperty("output", out var outputProp), $"Expected 'output'. Got: {json}"); Assert.True(outputProp.TryGetProperty("metaToUse", out var metaToUseProp), $"Expected 'metaToUse' in output. Got: {outputProp}"); Assert.Equal("by-hook", metaToUseProp.GetProperty("injected").GetString()); Assert.Equal("test", metaToUseProp.GetProperty("source").GetString()); } private static object CreateInternalRequest(Type type, params (string Name, object? Value)[] properties) { #if NET8_0_OR_GREATER var instance = System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(type); #else var instance = FormatterServices.GetUninitializedObject(type); #endif foreach (var (name, value) in properties) { var property = type.GetProperty(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); Assert.NotNull(property); if (property!.SetMethod is not null) { property.SetValue(instance, value); continue; } var field = type.GetField($"<{name}>k__BackingField", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); Assert.NotNull(field); field!.SetValue(instance, value); } return instance; } }