/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
using System.Text.Json;
using System.Text.Json.Serialization;
using Xunit;
namespace GitHub.Copilot.Test.Unit;
///
/// Tests for forward-compatible handling of unknown session event types.
/// Verifies that the SDK gracefully handles event types introduced by newer CLI versions.
///
public class ForwardCompatibilityTests
{
[Fact]
public void FromJson_KnownEventType_DeserializesNormally()
{
var json = """
{
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
"agentId": "agent-1",
"type": "user.message",
"data": {
"content": "Hello"
}
}
""";
var result = SessionEvent.FromJson(json);
Assert.IsType(result);
Assert.Equal("user.message", result.Type);
Assert.Equal("agent-1", result.AgentId);
}
[Fact]
public void FromJson_UnknownEventType_ReturnsBaseSessionEvent()
{
var json = """
{
"id": "12345678-1234-1234-1234-123456789abc",
"timestamp": "2026-06-15T10:30:00Z",
"parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
"agentId": "future-agent",
"type": "future.feature_from_server",
"data": { "key": "value" }
}
""";
var result = SessionEvent.FromJson(json);
Assert.IsType(result);
Assert.Equal("unknown", result.Type);
Assert.Equal("future-agent", result.AgentId);
}
[Fact]
public void FromJson_UnknownEventType_PreservesBaseMetadata()
{
var json = """
{
"id": "12345678-1234-1234-1234-123456789abc",
"timestamp": "2026-06-15T10:30:00Z",
"parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
"type": "future.feature_from_server",
"data": {}
}
""";
var result = SessionEvent.FromJson(json);
Assert.Equal(Guid.Parse("12345678-1234-1234-1234-123456789abc"), result.Id);
Assert.Equal(DateTimeOffset.Parse("2026-06-15T10:30:00Z"), result.Timestamp);
Assert.Equal(Guid.Parse("abcdefab-abcd-abcd-abcd-abcdefabcdef"), result.ParentId);
}
[Fact]
public void FromJson_MultipleEvents_MixedKnownAndUnknown()
{
var events = new[]
{
"""{"id":"00000000-0000-0000-0000-000000000001","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Hi"}}""",
"""{"id":"00000000-0000-0000-0000-000000000002","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"future.unknown_type","data":{}}""",
"""{"id":"00000000-0000-0000-0000-000000000003","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Bye"}}""",
};
var results = events.Select(SessionEvent.FromJson).ToList();
Assert.Equal(3, results.Count);
Assert.IsType(results[0]);
Assert.IsType(results[1]);
Assert.IsType(results[2]);
}
[Fact]
public void FromJson_KnownEventType_WithExtraUnknownFields_IgnoresExtras()
{
// Forward-compat: when the runtime adds new fields to a known event,
// older SDK versions must ignore them and still successfully parse the event.
var json = """
{
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
"agentId": "agent-1",
"type": "user.message",
"futureEnvelopeField": {"someShape": [1,2,3]},
"data": {
"content": "Hello",
"futureDataField": "ignored",
"anotherFutureField": {"nested": true}
}
}
""";
var result = SessionEvent.FromJson(json);
var msg = Assert.IsType(result);
Assert.Equal("Hello", msg.Data.Content);
}
[Fact]
public void FromJson_KnownEventType_WithExtraUnknownEnvelopeFields_IgnoresExtras()
{
// Pure envelope-level extra field (no inner data extras).
var json = """
{
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
"agentId": "agent-1",
"type": "session.idle",
"newServerOnlyField": 42,
"data": {}
}
""";
var result = SessionEvent.FromJson(json);
Assert.IsType(result);
Assert.Equal("agent-1", result.AgentId);
}
[Fact]
public void FromJson_UnknownEventType_WithUnknownEnumInData_DoesNotThrow()
{
// Unknown event types are mapped to base SessionEvent which does not parse data.
// So unknown enum values inside the data of an unknown event must not throw.
var json = """
{
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
"type": "future.event_with_enum",
"data": {
"futureMode": "future_value_not_in_sdk_enum"
}
}
""";
var result = SessionEvent.FromJson(json);
Assert.IsType(result);
Assert.Equal("unknown", result.Type);
}
[Fact]
public void FromJson_KnownEventType_WithUnknownEnumInData_PreservesValue()
{
var json = """
{
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
"type": "abort",
"data": {
"reason": "future_abort_reason"
}
}
""";
var result = SessionEvent.FromJson(json);
var abort = Assert.IsType(result);
Assert.Equal("future_abort_reason", abort.Data.Reason.Value);
}
[Fact]
public void FromJson_KnownEventType_WithNonStringEnumInData_ThrowsJsonException()
{
var json = """
{
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
"type": "abort",
"data": {
"reason": false
}
}
""";
var exception = Assert.Throws(() => SessionEvent.FromJson(json));
Assert.Contains("AbortReason", exception.Message);
}
[Fact]
public void RpcEnum_WithUnknownValue_PreservesValue()
{
var mode = JsonSerializer.Deserialize(
"""
"future_mode"
""",
ForwardCompatibilityJsonContext.Default.SessionMode);
Assert.Equal("future_mode", mode.Value);
Assert.Equal(
"""
"future_mode"
""",
JsonSerializer.Serialize(mode, ForwardCompatibilityJsonContext.Default.SessionMode));
}
[Fact]
public void RpcEnum_WithNonStringValue_ThrowsJsonException()
{
var exception = Assert.Throws(() => JsonSerializer.Deserialize(
"""
42
""",
ForwardCompatibilityJsonContext.Default.SessionMode));
Assert.Contains("SessionMode", exception.Message);
}
[Fact]
public void RpcEnum_DefaultValue_HasEmptyStringValue()
{
GitHub.Copilot.SessionMode mode = default;
Assert.Equal(string.Empty, mode.Value);
Assert.Equal(string.Empty, mode.ToString());
}
[Fact]
public void RpcEnum_DefaultValueSerialization_ThrowsJsonException()
{
GitHub.Copilot.SessionMode mode = default;
var exception = Assert.Throws(() => JsonSerializer.Serialize(
mode,
ForwardCompatibilityJsonContext.Default.SessionMode));
Assert.Contains("SessionMode", exception.Message);
}
[Fact]
public void FromJson_KnownEventType_WithNullOptionalFields_DoesNotThrow()
{
// The CLI may emit null for optional fields. Verify parsing doesn't throw.
var json = """
{
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
"agentId": null,
"type": "user.message",
"data": {
"content": "Hello"
}
}
""";
var result = SessionEvent.FromJson(json);
var msg = Assert.IsType(result);
Assert.Null(msg.AgentId);
Assert.Null(msg.ParentId);
Assert.Equal("Hello", msg.Data.Content);
}
[Fact]
public void FromJson_UnknownEventType_PreservesAgentIdNull()
{
// Some events legitimately have no agent id. Verify it round-trips as null.
var json = """
{
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
"type": "future.something",
"data": {}
}
""";
var result = SessionEvent.FromJson(json);
Assert.Equal("unknown", result.Type);
Assert.Null(result.AgentId);
}
}
[JsonSerializable(typeof(GitHub.Copilot.SessionMode))]
internal partial class ForwardCompatibilityJsonContext : JsonSerializerContext;