Skip to content
Merged
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
34 changes: 34 additions & 0 deletions dotnet/test/Unit/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,40 @@ public void ToolResultObject_OmitsToolReferences_WhenNull_WithSdkOptions()
Assert.False(document.RootElement.TryGetProperty("toolReferences", out _));
}

#pragma warning disable GHCP001 // The queue management surface is intentionally experimental.
[Theory]
[InlineData("message-1")]
[InlineData(null)]
public void QueuePendingItems_MessageId_UsesCamelCaseAndIsOptional(string? messageId)
{
var options = GetSerializerOptions();
var messageIdProperty = messageId is null ? "" : $""","messageId":"{messageId}" """;
var json = $$"""
{
"id": "queue-1",
"kind": "message",
"displayText": "hello",
"agentMode": "interactive"
{{messageIdProperty}}
}
""";

var item = JsonSerializer.Deserialize<QueuePendingItems>(json, options);
Assert.NotNull(item);
Assert.Equal(messageId, item.MessageId);

using var document = JsonDocument.Parse(JsonSerializer.Serialize(item, options));
if (messageId is null)
{
Assert.False(document.RootElement.TryGetProperty("messageId", out _));
}
else
{
Assert.Equal(messageId, document.RootElement.GetProperty("messageId").GetString());
}
}
#pragma warning restore GHCP001

private static JsonSerializerOptions GetSerializerOptions()
{
var prop = typeof(CopilotClient)
Expand Down
34 changes: 34 additions & 0 deletions dotnet/test/Unit/SessionEventSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,40 @@ public void SessionEvent_Deserializes_AutoTier(AutoTier? expectedTier, string? w
}
}

[Theory]
[InlineData("message-1")]
[InlineData(null)]
public void UserMessageEvent_MessageId_UsesCamelCaseAndIsOptional(string? messageId)
{
var messageIdProperty = messageId is null ? "" : $""", "messageId": "{messageId}" """;
var json = $$"""
{
"id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-08-28T00:00:00Z",
"parentId": null,
"type": "user.message",
"data": {
"content": "hello"
{{messageIdProperty}}
}
}
""";

var sessionEvent = Assert.IsType<UserMessageEvent>(SessionEvent.FromJson(json));
Assert.Equal(messageId, sessionEvent.Data.MessageId);

using var document = JsonDocument.Parse(sessionEvent.ToJson());
var data = document.RootElement.GetProperty("data");
if (messageId is null)
{
Assert.False(data.TryGetProperty("messageId", out _));
}
else
{
Assert.Equal(messageId, data.GetProperty("messageId").GetString());
}
}

public static TheoryData<SessionEvent, string> JsonElementBackedEvents => new()
{
{
Expand Down
101 changes: 101 additions & 0 deletions go/rpc/message_identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package rpc

import (
"encoding/json"
"testing"
)

func TestQueuePendingItemsMessageIDJSONCompatibility(t *testing.T) {
var item QueuePendingItems
if err := json.Unmarshal([]byte(`{
"id": "queue-1",
"messageId": "message-1",
"kind": "message",
"displayText": "hello",
"agentMode": "interactive"
}`), &item); err != nil {
t.Fatal(err)
}
if item.MessageID == nil || *item.MessageID != "message-1" {
t.Fatalf("MessageID = %v, want message-1", item.MessageID)
}

encoded, err := json.Marshal(item)
if err != nil {
t.Fatal(err)
}
var wire map[string]any
if err := json.Unmarshal(encoded, &wire); err != nil {
t.Fatal(err)
}
if got := wire["messageId"]; got != "message-1" {
t.Fatalf("messageId = %v, want message-1", got)
}

var olderItem QueuePendingItems
if err := json.Unmarshal([]byte(`{
"id": "queue-2",
"kind": "command",
"displayText": "/help",
"agentMode": "interactive"
}`), &olderItem); err != nil {
t.Fatal(err)
}
if olderItem.MessageID != nil {
t.Fatalf("MessageID = %v, want nil", olderItem.MessageID)
}

encoded, err = json.Marshal(olderItem)
if err != nil {
t.Fatal(err)
}
wire = nil
if err := json.Unmarshal(encoded, &wire); err != nil {
t.Fatal(err)
}
if _, ok := wire["messageId"]; ok {
t.Fatal("messageId should be omitted when absent")
}
}

func TestUserMessageDataMessageIDJSONCompatibility(t *testing.T) {
var message UserMessageData
if err := json.Unmarshal([]byte(`{"content":"hello","messageId":"message-1"}`), &message); err != nil {
t.Fatal(err)
}
if message.MessageID == nil || *message.MessageID != "message-1" {
t.Fatalf("MessageID = %v, want message-1", message.MessageID)
}

encoded, err := json.Marshal(message)
if err != nil {
t.Fatal(err)
}
var wire map[string]any
if err := json.Unmarshal(encoded, &wire); err != nil {
t.Fatal(err)
}
if got := wire["messageId"]; got != "message-1" {
t.Fatalf("messageId = %v, want message-1", got)
}

var olderMessage UserMessageData
if err := json.Unmarshal([]byte(`{"content":"hello"}`), &olderMessage); err != nil {
t.Fatal(err)
}
if olderMessage.MessageID != nil {
t.Fatalf("MessageID = %v, want nil", olderMessage.MessageID)
}

encoded, err = json.Marshal(olderMessage)
if err != nil {
t.Fatal(err)
}
wire = nil
if err := json.Unmarshal(encoded, &wire); err != nil {
t.Fatal(err)
}
if _, ok := wire["messageId"]; ok {
t.Fatal("messageId should be omitted when absent")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.generated;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.copilot.generated.UserMessageEvent.UserMessageEventData;
import com.github.copilot.generated.rpc.QueuePendingItems;

class MessageIdentitySerializationTest {

private static final ObjectMapper MAPPER = new ObjectMapper();

@Test
void testQueuePendingMessageIdUsesCamelCaseAndIsOptional() throws Exception {
var item = MAPPER.readValue("""
{
"id": "queue-1",
"messageId": "message-1",
"kind": "message",
"displayText": "hello",
"agentMode": "interactive"
}
""", QueuePendingItems.class);

assertEquals("message-1", item.messageId());
assertEquals("message-1", MAPPER.valueToTree(item).get("messageId").textValue());

var olderItem = MAPPER.readValue("""
{
"id": "queue-2",
"kind": "command",
"displayText": "/help",
"agentMode": "interactive"
}
""", QueuePendingItems.class);

assertNull(olderItem.messageId());
assertFalse(MAPPER.<JsonNode>valueToTree(olderItem).has("messageId"));
}

@Test
void testUserMessageIdUsesCamelCaseAndIsOptional() throws Exception {
var message = MAPPER.readValue("""
{"content": "hello", "messageId": "message-1"}
""", UserMessageEventData.class);

assertEquals("message-1", message.messageId());
assertEquals("message-1", MAPPER.valueToTree(message).get("messageId").textValue());

var olderMessage = MAPPER.readValue("""
{"content": "hello"}
""", UserMessageEventData.class);

assertNull(olderMessage.messageId());
assertFalse(MAPPER.<JsonNode>valueToTree(olderMessage).has("messageId"));
}
}
33 changes: 33 additions & 0 deletions nodejs/test/message-identity-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import type { QueuePendingItems } from "../src/generated/rpc.js";
import type { UserMessageData } from "../src/generated/session-events.js";

describe("generated message identity types", () => {
it("exposes optional camelCase message IDs", () => {
const queueItemWithIdentity: QueuePendingItems = {
id: "queue-1",
messageId: "message-1",
kind: "message",
displayText: "hello",
agentMode: "interactive",
};
const queueItemFromOlderRuntime: QueuePendingItems = {
id: "queue-2",
kind: "command",
displayText: "/help",
agentMode: "interactive",
};
const userMessageWithIdentity: UserMessageData = {
content: "hello",
messageId: "message-1",
};
const userMessageFromOlderRuntime: UserMessageData = {
content: "hello",
};

expect(queueItemWithIdentity.messageId).toBe("message-1");
expect(queueItemFromOlderRuntime.messageId).toBeUndefined();
expect(userMessageWithIdentity.messageId).toBe("message-1");
expect(userMessageFromOlderRuntime.messageId).toBeUndefined();
});
});
41 changes: 41 additions & 0 deletions python/test_message_identity_generated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from copilot.generated.rpc import QueuePendingItems
from copilot.generated.session_events import UserMessageData


def test_queue_pending_message_id_uses_camel_case_and_is_optional():
item = QueuePendingItems.from_dict(
{
"id": "queue-1",
"messageId": "message-1",
"kind": "message",
"displayText": "hello",
"agentMode": "interactive",
}
)

assert item.message_id == "message-1"
assert item.to_dict()["messageId"] == "message-1"

older_item = QueuePendingItems.from_dict(
{
"id": "queue-2",
"kind": "command",
"displayText": "/help",
"agentMode": "interactive",
}
)

assert older_item.message_id is None
assert "messageId" not in older_item.to_dict()


def test_user_message_id_uses_camel_case_and_is_optional():
message = UserMessageData.from_dict({"content": "hello", "messageId": "message-1"})

assert message.message_id == "message-1"
assert message.to_dict()["messageId"] == "message-1"

older_message = UserMessageData.from_dict({"content": "hello"})

assert older_message.message_id is None
assert "messageId" not in older_message.to_dict()
40 changes: 39 additions & 1 deletion rust/tests/api_types_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
use github_copilot_sdk::AutoTier;
use github_copilot_sdk::rpc::{
Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, QueuePendingItems,
QueuePendingItemsKind, SendAgentMode, TasksStartAgentRequest,
};
use github_copilot_sdk::session_events::{
PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent,
Expand Down Expand Up @@ -146,6 +147,43 @@ fn permission_event_exposes_managed_approval_required() {
assert_eq!(request.managed_approval_required, Some(true));
}

#[test]
fn queue_pending_message_id_uses_camel_case_wire_name() {
let item = QueuePendingItems {
agent_mode: SendAgentMode::Interactive,
display_text: "second message".to_string(),
id: "batch-1".to_string(),
kind: QueuePendingItemsKind::Message,
message_id: Some("message-2".to_string()),
};

let serialized = serde_json::to_value(&item).unwrap();
assert_eq!(serialized["id"], "batch-1");
assert_eq!(serialized["messageId"], "message-2");

let deserialized: QueuePendingItems = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.message_id.as_deref(), Some("message-2"));
}

#[test]
fn queue_pending_message_id_is_optional_for_older_hosts() {
let item: QueuePendingItems = serde_json::from_value(serde_json::json!({
"agentMode": "interactive",
"displayText": "/model gpt-5",
"id": "command-1",
"kind": "command"
}))
.unwrap();

assert_eq!(item.message_id, None);
assert!(
serde_json::to_value(item)
.unwrap()
.get("messageId")
.is_none()
);
}

fn running_extension(id: &str, name: &str) -> Extension {
Extension {
id: id.to_string(),
Expand Down
Loading
Loading