From d51be5178822cdd904285fc8177ec231cc5646b3 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Mon, 16 Mar 2026 18:55:11 +0100 Subject: [PATCH] Fix serialization of SessionEvent --- dotnet/src/Generated/SessionEvents.cs | 1 + dotnet/test/SessionEventSerializationTests.cs | 180 ++++++++++++++++++ scripts/codegen/csharp.ts | 1 + 3 files changed, 182 insertions(+) create mode 100644 dotnet/test/SessionEventSerializationTests.cs diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 5ef1be352..08c6bf5e0 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -3527,4 +3527,5 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(UserMessageDataAttachmentsItemSelectionSelectionEnd))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemSelectionSelectionStart))] [JsonSerializable(typeof(UserMessageEvent))] +[JsonSerializable(typeof(JsonElement))] internal partial class SessionEventsJsonContext : JsonSerializerContext; \ No newline at end of file diff --git a/dotnet/test/SessionEventSerializationTests.cs b/dotnet/test/SessionEventSerializationTests.cs new file mode 100644 index 000000000..e7be64422 --- /dev/null +++ b/dotnet/test/SessionEventSerializationTests.cs @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace GitHub.Copilot.SDK.Test; + +public class SessionEventSerializationTests +{ + public static TheoryData JsonElementBackedEvents => new() + { + { + new AssistantMessageEvent + { + Id = Guid.Parse("11111111-1111-1111-1111-111111111111"), + Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"), + ParentId = Guid.Parse("22222222-2222-2222-2222-222222222222"), + Data = new AssistantMessageData + { + MessageId = "msg-1", + Content = "", + ToolRequests = + [ + new AssistantMessageDataToolRequestsItem + { + ToolCallId = "call-1", + Name = "view", + Arguments = ParseJsonElement("""{"path":"README.md"}"""), + Type = AssistantMessageDataToolRequestsItemType.Function, + }, + ], + }, + }, + "assistant.message" + }, + { + new ToolExecutionStartEvent + { + Id = Guid.Parse("33333333-3333-3333-3333-333333333333"), + Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"), + ParentId = Guid.Parse("44444444-4444-4444-4444-444444444444"), + Data = new ToolExecutionStartData + { + ToolCallId = "call-1", + ToolName = "view", + Arguments = ParseJsonElement("""{"path":"README.md"}"""), + }, + }, + "tool.execution_start" + }, + { + new ToolExecutionCompleteEvent + { + Id = Guid.Parse("55555555-5555-5555-5555-555555555555"), + Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"), + ParentId = Guid.Parse("66666666-6666-6666-6666-666666666666"), + Data = new ToolExecutionCompleteData + { + ToolCallId = "call-1", + Success = true, + Result = new ToolExecutionCompleteDataResult + { + Content = "ok", + DetailedContent = "ok", + }, + ToolTelemetry = new Dictionary + { + ["properties"] = ParseJsonElement("""{"command":"view"}"""), + ["metrics"] = ParseJsonElement("""{"resultLength":2}"""), + }, + }, + }, + "tool.execution_complete" + }, + { + new SessionShutdownEvent + { + Id = Guid.Parse("77777777-7777-7777-7777-777777777777"), + Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:52.987Z"), + ParentId = Guid.Parse("88888888-8888-8888-8888-888888888888"), + Data = new SessionShutdownData + { + ShutdownType = SessionShutdownDataShutdownType.Routine, + TotalPremiumRequests = 1, + TotalApiDurationMs = 100, + SessionStartTime = 1773609948932, + CodeChanges = new SessionShutdownDataCodeChanges + { + LinesAdded = 1, + LinesRemoved = 0, + FilesModified = ["README.md"], + }, + ModelMetrics = new Dictionary + { + ["gpt-5.4"] = ParseJsonElement(""" + { + "requests": { + "count": 1, + "cost": 1 + }, + "usage": { + "inputTokens": 10, + "outputTokens": 5, + "cacheReadTokens": 0, + "cacheWriteTokens": 0 + } + } + """), + }, + CurrentModel = "gpt-5.4", + }, + }, + "session.shutdown" + } + }; + + private static JsonElement ParseJsonElement(string json) + { + using var document = JsonDocument.Parse(json); + return document.RootElement.Clone(); + } + + [Theory] + [MemberData(nameof(JsonElementBackedEvents))] + public void SessionEvent_ToJson_RoundTrips_JsonElementBackedPayloads(SessionEvent sessionEvent, string expectedType) + { + var serialized = sessionEvent.ToJson(); + + using var document = JsonDocument.Parse(serialized); + var root = document.RootElement; + + Assert.Equal(expectedType, root.GetProperty("type").GetString()); + + switch (expectedType) + { + case "assistant.message": + Assert.Equal( + "README.md", + root.GetProperty("data") + .GetProperty("toolRequests")[0] + .GetProperty("arguments") + .GetProperty("path") + .GetString()); + break; + + case "tool.execution_start": + Assert.Equal( + "README.md", + root.GetProperty("data") + .GetProperty("arguments") + .GetProperty("path") + .GetString()); + break; + + case "tool.execution_complete": + Assert.Equal( + "view", + root.GetProperty("data") + .GetProperty("toolTelemetry") + .GetProperty("properties") + .GetProperty("command") + .GetString()); + break; + + case "session.shutdown": + Assert.Equal( + 1, + root.GetProperty("data") + .GetProperty("modelMetrics") + .GetProperty("gpt-5.4") + .GetProperty("requests") + .GetProperty("count") + .GetInt32()); + break; + } + } +} diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 3aeb0eef3..49994d9d8 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -574,6 +574,7 @@ namespace GitHub.Copilot.SDK; const types = ["SessionEvent", ...variants.flatMap((v) => [v.className, v.dataClassName]), ...nestedClasses.keys()].sort(); lines.push(`[JsonSourceGenerationOptions(`, ` JsonSerializerDefaults.Web,`, ` AllowOutOfOrderMetadataProperties = true,`, ` NumberHandling = JsonNumberHandling.AllowReadingFromString,`, ` DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]`); for (const t of types) lines.push(`[JsonSerializable(typeof(${t}))]`); + lines.push(`[JsonSerializable(typeof(JsonElement))]`); lines.push(`internal partial class SessionEventsJsonContext : JsonSerializerContext;`); return lines.join("\n");