Skip to content

Commit d82fd62

Browse files
authored
fix(dotnet): handle unknown session event types gracefully (#881)
* fix(dotnet): handle unknown session event types gracefully Add UnknownSessionEvent type and TryFromJson method so that unrecognized event types from newer CLI versions do not crash GetMessagesAsync or real-time event dispatch. * refactor: use IgnoreUnrecognizedTypeDiscriminators per review feedback
1 parent 7390a28 commit d82fd62

File tree

3 files changed

+106
-6
lines changed

3 files changed

+106
-6
lines changed

dotnet/src/Generated/SessionEvents.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace GitHub.Copilot.SDK;
1717
[DebuggerDisplay("{DebuggerDisplay,nq}")]
1818
[JsonPolymorphic(
1919
TypeDiscriminatorPropertyName = "type",
20-
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
20+
IgnoreUnrecognizedTypeDiscriminators = true)]
2121
[JsonDerivedType(typeof(AbortEvent), "abort")]
2222
[JsonDerivedType(typeof(AssistantIntentEvent), "assistant.intent")]
2323
[JsonDerivedType(typeof(AssistantMessageEvent), "assistant.message")]
@@ -79,7 +79,7 @@ namespace GitHub.Copilot.SDK;
7979
[JsonDerivedType(typeof(UserInputCompletedEvent), "user_input.completed")]
8080
[JsonDerivedType(typeof(UserInputRequestedEvent), "user_input.requested")]
8181
[JsonDerivedType(typeof(UserMessageEvent), "user.message")]
82-
public abstract partial class SessionEvent
82+
public partial class SessionEvent
8383
{
8484
/// <summary>Unique event identifier (UUID v4), generated when the event is emitted.</summary>
8585
[JsonPropertyName("id")]
@@ -102,7 +102,7 @@ public abstract partial class SessionEvent
102102
/// The event type discriminator.
103103
/// </summary>
104104
[JsonIgnore]
105-
public abstract string Type { get; }
105+
public virtual string Type => "unknown";
106106

107107
/// <summary>Deserializes a JSON string into a <see cref="SessionEvent"/>.</summary>
108108
public static SessionEvent FromJson(string json) =>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using Xunit;
6+
7+
namespace GitHub.Copilot.SDK.Test;
8+
9+
/// <summary>
10+
/// Tests for forward-compatible handling of unknown session event types.
11+
/// Verifies that the SDK gracefully handles event types introduced by newer CLI versions.
12+
/// </summary>
13+
public class ForwardCompatibilityTests
14+
{
15+
[Fact]
16+
public void FromJson_KnownEventType_DeserializesNormally()
17+
{
18+
var json = """
19+
{
20+
"id": "00000000-0000-0000-0000-000000000001",
21+
"timestamp": "2026-01-01T00:00:00Z",
22+
"parentId": null,
23+
"type": "user.message",
24+
"data": {
25+
"content": "Hello"
26+
}
27+
}
28+
""";
29+
30+
var result = SessionEvent.FromJson(json);
31+
32+
Assert.IsType<UserMessageEvent>(result);
33+
Assert.Equal("user.message", result.Type);
34+
}
35+
36+
[Fact]
37+
public void FromJson_UnknownEventType_ReturnsBaseSessionEvent()
38+
{
39+
var json = """
40+
{
41+
"id": "12345678-1234-1234-1234-123456789abc",
42+
"timestamp": "2026-06-15T10:30:00Z",
43+
"parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
44+
"type": "future.feature_from_server",
45+
"data": { "key": "value" }
46+
}
47+
""";
48+
49+
var result = SessionEvent.FromJson(json);
50+
51+
Assert.IsType<SessionEvent>(result);
52+
Assert.Equal("unknown", result.Type);
53+
}
54+
55+
[Fact]
56+
public void FromJson_UnknownEventType_PreservesBaseMetadata()
57+
{
58+
var json = """
59+
{
60+
"id": "12345678-1234-1234-1234-123456789abc",
61+
"timestamp": "2026-06-15T10:30:00Z",
62+
"parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
63+
"type": "future.feature_from_server",
64+
"data": {}
65+
}
66+
""";
67+
68+
var result = SessionEvent.FromJson(json);
69+
70+
Assert.Equal(Guid.Parse("12345678-1234-1234-1234-123456789abc"), result.Id);
71+
Assert.Equal(DateTimeOffset.Parse("2026-06-15T10:30:00Z"), result.Timestamp);
72+
Assert.Equal(Guid.Parse("abcdefab-abcd-abcd-abcd-abcdefabcdef"), result.ParentId);
73+
}
74+
75+
[Fact]
76+
public void FromJson_MultipleEvents_MixedKnownAndUnknown()
77+
{
78+
var events = new[]
79+
{
80+
"""{"id":"00000000-0000-0000-0000-000000000001","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Hi"}}""",
81+
"""{"id":"00000000-0000-0000-0000-000000000002","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"future.unknown_type","data":{}}""",
82+
"""{"id":"00000000-0000-0000-0000-000000000003","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Bye"}}""",
83+
};
84+
85+
var results = events.Select(SessionEvent.FromJson).ToList();
86+
87+
Assert.Equal(3, results.Count);
88+
Assert.IsType<UserMessageEvent>(results[0]);
89+
Assert.IsType<SessionEvent>(results[1]);
90+
Assert.IsType<UserMessageEvent>(results[2]);
91+
}
92+
93+
[Fact]
94+
public void SessionEvent_Type_DefaultsToUnknown()
95+
{
96+
var evt = new SessionEvent();
97+
98+
Assert.Equal("unknown", evt.Type);
99+
}
100+
}

scripts/codegen/csharp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,11 +522,11 @@ namespace GitHub.Copilot.SDK;
522522
lines.push(`/// Provides the base class from which all session events derive.`);
523523
lines.push(`/// </summary>`);
524524
lines.push(`[DebuggerDisplay("{DebuggerDisplay,nq}")]`);
525-
lines.push(`[JsonPolymorphic(`, ` TypeDiscriminatorPropertyName = "type",`, ` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]`);
525+
lines.push(`[JsonPolymorphic(`, ` TypeDiscriminatorPropertyName = "type",`, ` IgnoreUnrecognizedTypeDiscriminators = true)]`);
526526
for (const variant of [...variants].sort((a, b) => a.typeName.localeCompare(b.typeName))) {
527527
lines.push(`[JsonDerivedType(typeof(${variant.className}), "${variant.typeName}")]`);
528528
}
529-
lines.push(`public abstract partial class SessionEvent`, `{`);
529+
lines.push(`public partial class SessionEvent`, `{`);
530530
lines.push(...xmlDocComment(baseDesc("id"), " "));
531531
lines.push(` [JsonPropertyName("id")]`, ` public Guid Id { get; set; }`, "");
532532
lines.push(...xmlDocComment(baseDesc("timestamp"), " "));
@@ -536,7 +536,7 @@ namespace GitHub.Copilot.SDK;
536536
lines.push(...xmlDocComment(baseDesc("ephemeral"), " "));
537537
lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`, ` [JsonPropertyName("ephemeral")]`, ` public bool? Ephemeral { get; set; }`, "");
538538
lines.push(` /// <summary>`, ` /// The event type discriminator.`, ` /// </summary>`);
539-
lines.push(` [JsonIgnore]`, ` public abstract string Type { get; }`, "");
539+
lines.push(` [JsonIgnore]`, ` public virtual string Type => "unknown";`, "");
540540
lines.push(` /// <summary>Deserializes a JSON string into a <see cref="SessionEvent"/>.</summary>`);
541541
lines.push(` public static SessionEvent FromJson(string json) =>`, ` JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!;`, "");
542542
lines.push(` /// <summary>Serializes this event to a JSON string.</summary>`);

0 commit comments

Comments
 (0)