mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into features/foundry-hosting-it-msb3026-fix
This commit is contained in:
@@ -102,18 +102,20 @@ public sealed class AGUIChatMessageExtensionsTests
|
||||
new AGUISystemMessage { Id = "msg1", Content = "System message" },
|
||||
new AGUIUserMessage { Id = "msg2", Content = "User message" },
|
||||
new AGUIAssistantMessage { Id = "msg3", Content = "Assistant message" },
|
||||
new AGUIDeveloperMessage { Id = "msg4", Content = "Developer message" }
|
||||
new AGUIDeveloperMessage { Id = "msg4", Content = "Developer message" },
|
||||
new AGUIReasoningMessage { Id = "msg5", Content = "Reasoning message" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatMessage> chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, chatMessages.Count);
|
||||
Assert.Equal(5, chatMessages.Count);
|
||||
Assert.Equal(ChatRole.System, chatMessages[0].Role);
|
||||
Assert.Equal(ChatRole.User, chatMessages[1].Role);
|
||||
Assert.Equal(ChatRole.Assistant, chatMessages[2].Role);
|
||||
Assert.Equal("developer", chatMessages[3].Role.Value);
|
||||
Assert.Equal(ChatRole.Assistant, chatMessages[4].Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -367,6 +369,277 @@ public sealed class AGUIChatMessageExtensionsTests
|
||||
Assert.Equal(ChatRole.Tool, role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsChatMessages_WithReasoningMessage_ConvertsToTextReasoningContent()
|
||||
{
|
||||
// Arrange
|
||||
List<AGUIMessage> aguiMessages =
|
||||
[
|
||||
new AGUIReasoningMessage
|
||||
{
|
||||
Id = "reason1",
|
||||
Content = "I need to consider the user's request.",
|
||||
EncryptedValue = "ErgDCkgIDB..."
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatMessage> chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
ChatMessage message = Assert.Single(chatMessages);
|
||||
Assert.Equal(ChatRole.Assistant, message.Role);
|
||||
Assert.Equal("reason1", message.MessageId);
|
||||
var reasoningContent = Assert.IsType<TextReasoningContent>(message.Contents[0]);
|
||||
Assert.Equal("I need to consider the user's request.", reasoningContent.Text);
|
||||
Assert.Equal("ErgDCkgIDB...", reasoningContent.ProtectedData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsChatMessages_WithReasoningMessageWithoutEncryptedValue_ConvertsToTextReasoningContent()
|
||||
{
|
||||
// Arrange
|
||||
List<AGUIMessage> aguiMessages =
|
||||
[
|
||||
new AGUIReasoningMessage
|
||||
{
|
||||
Id = "reason1",
|
||||
Content = "Thinking about this problem."
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatMessage> chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
ChatMessage message = Assert.Single(chatMessages);
|
||||
Assert.Equal(ChatRole.Assistant, message.Role);
|
||||
var reasoningContent = Assert.IsType<TextReasoningContent>(message.Contents[0]);
|
||||
Assert.Equal("Thinking about this problem.", reasoningContent.Text);
|
||||
Assert.Null(reasoningContent.ProtectedData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsChatMessages_WithReasoningMessageWithOnlyEncryptedValue_ConvertsToTextReasoningContent()
|
||||
{
|
||||
// Arrange
|
||||
List<AGUIMessage> aguiMessages =
|
||||
[
|
||||
new AGUIReasoningMessage
|
||||
{
|
||||
Id = "reason1",
|
||||
Content = string.Empty,
|
||||
EncryptedValue = "ErgDCkgIDB..."
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatMessage> chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
ChatMessage message = Assert.Single(chatMessages);
|
||||
var reasoningContent = Assert.IsType<TextReasoningContent>(message.Contents[0]);
|
||||
Assert.Equal("", reasoningContent.Text);
|
||||
Assert.Equal("ErgDCkgIDB...", reasoningContent.ProtectedData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsChatMessages_WithEmptyReasoningMessage_ProducesEmptyContents()
|
||||
{
|
||||
// Arrange
|
||||
List<AGUIMessage> aguiMessages =
|
||||
[
|
||||
new AGUIReasoningMessage
|
||||
{
|
||||
Id = "reason1",
|
||||
Content = string.Empty
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatMessage> chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
ChatMessage message = Assert.Single(chatMessages);
|
||||
Assert.Equal(ChatRole.Assistant, message.Role);
|
||||
Assert.Empty(message.Contents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapChatRole_WithReasoningRole_ReturnsAssistantChatRole()
|
||||
{
|
||||
// Arrange & Act
|
||||
ChatRole role = AGUIChatMessageExtensions.MapChatRole(AGUIRoles.Reasoning);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ChatRole.Assistant, role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsChatMessages_WithMixedMessagesIncludingReasoning_PreservesOrder()
|
||||
{
|
||||
// Arrange
|
||||
List<AGUIMessage> aguiMessages =
|
||||
[
|
||||
new AGUIUserMessage { Id = "msg1", Content = "What is 2+2?" },
|
||||
new AGUIReasoningMessage { Id = "msg2", Content = "I need to add 2 and 2.", EncryptedValue = "tok-123" },
|
||||
new AGUIAssistantMessage { Id = "msg3", Content = "The answer is 4." }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatMessage> chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, chatMessages.Count);
|
||||
Assert.Equal(ChatRole.User, chatMessages[0].Role);
|
||||
Assert.Equal(ChatRole.Assistant, chatMessages[1].Role);
|
||||
Assert.IsType<TextReasoningContent>(chatMessages[1].Contents[0]);
|
||||
Assert.Equal(ChatRole.Assistant, chatMessages[2].Role);
|
||||
Assert.Equal("The answer is 4.", chatMessages[2].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAGUIMessages_WithReasoningContent_ProducesReasoningMessage()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> chatMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, [
|
||||
new TextReasoningContent("I need to think about this.") { ProtectedData = "encrypted-tok-1" }
|
||||
]) { MessageId = "reason-1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<AGUIMessage> aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
AGUIMessage message = Assert.Single(aguiMessages);
|
||||
var reasoningMessage = Assert.IsType<AGUIReasoningMessage>(message);
|
||||
Assert.Equal("reason-1", reasoningMessage.Id);
|
||||
Assert.Equal(AGUIRoles.Reasoning, reasoningMessage.Role);
|
||||
Assert.Equal("I need to think about this.", reasoningMessage.Content);
|
||||
Assert.Equal("encrypted-tok-1", reasoningMessage.EncryptedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAGUIMessages_WithReasoningContentWithoutProtectedData_ProducesReasoningMessage()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> chatMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, [
|
||||
new TextReasoningContent("Just thinking.")
|
||||
]) { MessageId = "reason-2" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<AGUIMessage> aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
AGUIMessage message = Assert.Single(aguiMessages);
|
||||
var reasoningMessage = Assert.IsType<AGUIReasoningMessage>(message);
|
||||
Assert.Equal("Just thinking.", reasoningMessage.Content);
|
||||
Assert.Null(reasoningMessage.EncryptedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAGUIMessages_WithMultipleReasoningChunksInOneMessage_ConcatenatesText()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> chatMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, [
|
||||
new TextReasoningContent("First part. "),
|
||||
new TextReasoningContent("Second part.") { ProtectedData = "final-token" }
|
||||
]) { MessageId = "reason-3" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<AGUIMessage> aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
AGUIMessage message = Assert.Single(aguiMessages);
|
||||
var reasoningMessage = Assert.IsType<AGUIReasoningMessage>(message);
|
||||
Assert.Equal("First part. Second part.", reasoningMessage.Content);
|
||||
Assert.Equal("final-token", reasoningMessage.EncryptedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAGUIMessages_WithMixedReasoningAndTextContent_EmitsBothMessages()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> chatMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, [
|
||||
new TextReasoningContent("Thinking about the answer.") { ProtectedData = "enc-tok" },
|
||||
new TextContent("The answer is 42.")
|
||||
]) { MessageId = "msg-mixed" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<AGUIMessage> aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, aguiMessages.Count);
|
||||
var reasoningMessage = Assert.IsType<AGUIReasoningMessage>(aguiMessages[0]);
|
||||
Assert.Equal("msg-mixed", reasoningMessage.Id);
|
||||
Assert.Equal("Thinking about the answer.", reasoningMessage.Content);
|
||||
Assert.Equal("enc-tok", reasoningMessage.EncryptedValue);
|
||||
var assistantMessage = Assert.IsType<AGUIAssistantMessage>(aguiMessages[1]);
|
||||
Assert.Equal("msg-mixed", assistantMessage.Id);
|
||||
Assert.Equal("The answer is 42.", assistantMessage.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAGUIMessages_WithReasoningAndToolCallInSameMessage_EmitsBothMessages()
|
||||
{
|
||||
// Arrange
|
||||
var arguments = new Dictionary<string, object?> { ["location"] = "Seattle" };
|
||||
List<ChatMessage> chatMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, [
|
||||
new TextReasoningContent("I should look up the weather."),
|
||||
new FunctionCallContent("call-1", "GetWeather", arguments)
|
||||
]) { MessageId = "msg-toolcall" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<AGUIMessage> aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, aguiMessages.Count);
|
||||
var reasoningMessage = Assert.IsType<AGUIReasoningMessage>(aguiMessages[0]);
|
||||
Assert.Equal("I should look up the weather.", reasoningMessage.Content);
|
||||
var assistantMessage = Assert.IsType<AGUIAssistantMessage>(aguiMessages[1]);
|
||||
Assert.NotNull(assistantMessage.ToolCalls);
|
||||
var toolCall = Assert.Single(assistantMessage.ToolCalls);
|
||||
Assert.Equal("call-1", toolCall.Id);
|
||||
Assert.Equal("GetWeather", toolCall.Function.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_ReasoningMessage_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> originalMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, [
|
||||
new TextReasoningContent("Thinking about the problem.") { ProtectedData = "ErgDCkgIDB..." }
|
||||
]) { MessageId = "reason-rt" }
|
||||
];
|
||||
|
||||
// Act - Convert to AGUI and back
|
||||
AGUIMessage aguiMessage = originalMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).Single();
|
||||
List<AGUIMessage> aguiList = [aguiMessage];
|
||||
ChatMessage reconstructed = aguiList.AsChatMessages(AGUIJsonSerializerContext.Default.Options).Single();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ChatRole.Assistant, reconstructed.Role);
|
||||
var reasoningContent = Assert.IsType<TextReasoningContent>(reconstructed.Contents[0]);
|
||||
Assert.Equal("Thinking about the problem.", reasoningContent.Text);
|
||||
Assert.Equal("ErgDCkgIDB...", reasoningContent.ProtectedData);
|
||||
}
|
||||
|
||||
#region Custom Type Serialization Tests
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -64,6 +64,49 @@ public sealed class AGUIJsonSerializerContextTests
|
||||
Assert.Single(input.Messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunAgentInput_Deserializes_FromJsonWithReasoningMessages()
|
||||
{
|
||||
// Arrange
|
||||
const string Json = """
|
||||
{
|
||||
"threadId": "thread1",
|
||||
"runId": "run1",
|
||||
"messages": [
|
||||
{
|
||||
"id": "m1",
|
||||
"role": "user",
|
||||
"content": "Hello"
|
||||
},
|
||||
{
|
||||
"id": "m2",
|
||||
"role": "reasoning",
|
||||
"content": "I need to consider this.",
|
||||
"encryptedValue": "ErgDCkgIDB..."
|
||||
},
|
||||
{
|
||||
"id": "m3",
|
||||
"role": "assistant",
|
||||
"content": "Here is my answer."
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
RunAgentInput? input = JsonSerializer.Deserialize(Json, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(input);
|
||||
var messages = input.Messages.ToList();
|
||||
Assert.Equal(3, messages.Count);
|
||||
Assert.IsType<AGUIUserMessage>(messages[0]);
|
||||
var reasoningMessage = Assert.IsType<AGUIReasoningMessage>(messages[1]);
|
||||
Assert.Equal("I need to consider this.", reasoningMessage.Content);
|
||||
Assert.Equal("ErgDCkgIDB...", reasoningMessage.EncryptedValue);
|
||||
Assert.IsType<AGUIAssistantMessage>(messages[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunAgentInput_HandlesOptionalFields_StateContextAndForwardedProperties()
|
||||
{
|
||||
@@ -963,7 +1006,76 @@ public sealed class AGUIJsonSerializerContextTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllFiveMessageTypes_SerializeAsPolymorphicArray_Correctly()
|
||||
public void AGUIReasoningMessage_SerializesAndDeserializes_Correctly()
|
||||
{
|
||||
// Arrange
|
||||
var originalMessage = new AGUIReasoningMessage
|
||||
{
|
||||
Id = "reason1",
|
||||
Content = "I need to consider the user's request carefully.",
|
||||
EncryptedValue = "ErgDCkgIDB..."
|
||||
};
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(originalMessage, AGUIJsonSerializerContext.Default.AGUIReasoningMessage);
|
||||
var deserialized = JsonSerializer.Deserialize(json, AGUIJsonSerializerContext.Default.AGUIReasoningMessage);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal("reason1", deserialized.Id);
|
||||
Assert.Equal("I need to consider the user's request carefully.", deserialized.Content);
|
||||
Assert.Equal("ErgDCkgIDB...", deserialized.EncryptedValue);
|
||||
Assert.Equal(AGUIRoles.Reasoning, deserialized.Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGUIReasoningMessage_WithoutEncryptedValue_SerializesAndDeserializes_Correctly()
|
||||
{
|
||||
// Arrange
|
||||
var originalMessage = new AGUIReasoningMessage
|
||||
{
|
||||
Id = "reason2",
|
||||
Content = "Thinking about this problem."
|
||||
};
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(originalMessage, AGUIJsonSerializerContext.Default.AGUIReasoningMessage);
|
||||
var deserialized = JsonSerializer.Deserialize(json, AGUIJsonSerializerContext.Default.AGUIReasoningMessage);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal("reason2", deserialized.Id);
|
||||
Assert.Equal("Thinking about this problem.", deserialized.Content);
|
||||
Assert.Null(deserialized.EncryptedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AGUIReasoningMessage_DeserializesViaPolymorphicConverter_Correctly()
|
||||
{
|
||||
// Arrange
|
||||
const string Json = """
|
||||
{
|
||||
"id": "reason1",
|
||||
"role": "reasoning",
|
||||
"content": "Let me think about this.",
|
||||
"encryptedValue": "tok-encrypted"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
AGUIMessage? message = JsonSerializer.Deserialize(Json, AGUIJsonSerializerContext.Default.AGUIMessage);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(message);
|
||||
var reasoningMessage = Assert.IsType<AGUIReasoningMessage>(message);
|
||||
Assert.Equal("reason1", reasoningMessage.Id);
|
||||
Assert.Equal(AGUIRoles.Reasoning, reasoningMessage.Role);
|
||||
Assert.Equal("Let me think about this.", reasoningMessage.Content);
|
||||
Assert.Equal("tok-encrypted", reasoningMessage.EncryptedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllSixMessageTypes_SerializeAsPolymorphicArray_Correctly()
|
||||
{
|
||||
// Arrange
|
||||
AGUIMessage[] messages =
|
||||
@@ -972,7 +1084,8 @@ public sealed class AGUIJsonSerializerContextTests
|
||||
new AGUIDeveloperMessage { Id = "2", Content = "Developer message" },
|
||||
new AGUIUserMessage { Id = "3", Content = "User message" },
|
||||
new AGUIAssistantMessage { Id = "4", Content = "Assistant message" },
|
||||
new AGUIToolMessage { Id = "5", ToolCallId = "call_1", Content = "{\"result\":\"success\"}" }
|
||||
new AGUIToolMessage { Id = "5", ToolCallId = "call_1", Content = "{\"result\":\"success\"}" },
|
||||
new AGUIReasoningMessage { Id = "6", Content = "Reasoning message", EncryptedValue = "tok-123" }
|
||||
];
|
||||
|
||||
// Act
|
||||
@@ -981,12 +1094,13 @@ public sealed class AGUIJsonSerializerContextTests
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(5, deserialized.Length);
|
||||
Assert.Equal(6, deserialized.Length);
|
||||
Assert.IsType<AGUISystemMessage>(deserialized[0]);
|
||||
Assert.IsType<AGUIDeveloperMessage>(deserialized[1]);
|
||||
Assert.IsType<AGUIUserMessage>(deserialized[2]);
|
||||
Assert.IsType<AGUIAssistantMessage>(deserialized[3]);
|
||||
Assert.IsType<AGUIToolMessage>(deserialized[4]);
|
||||
Assert.IsType<AGUIReasoningMessage>(deserialized[5]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1111,4 +1225,149 @@ public sealed class AGUIJsonSerializerContextTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reasoning Event Serialization Tests
|
||||
|
||||
[Fact]
|
||||
public void ReasoningStartEvent_Serializes_WithCorrectTypeDiscriminator()
|
||||
{
|
||||
// Arrange
|
||||
ReasoningStartEvent evt = new() { MessageId = "reason1" };
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(evt, AGUIJsonSerializerContext.Default.ReasoningStartEvent);
|
||||
JsonElement jsonElement = JsonElement.Parse(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AGUIEventTypes.ReasoningStart, jsonElement.GetProperty("type").GetString());
|
||||
Assert.Equal("reason1", jsonElement.GetProperty("messageId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReasoningMessageStartEvent_Serializes_WithRoleReasoningAndMessageId()
|
||||
{
|
||||
// Arrange
|
||||
ReasoningMessageStartEvent evt = new() { MessageId = "reason1" };
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(evt, AGUIJsonSerializerContext.Default.ReasoningMessageStartEvent);
|
||||
JsonElement jsonElement = JsonElement.Parse(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AGUIEventTypes.ReasoningMessageStart, jsonElement.GetProperty("type").GetString());
|
||||
Assert.Equal("reason1", jsonElement.GetProperty("messageId").GetString());
|
||||
Assert.Equal("reasoning", jsonElement.GetProperty("role").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReasoningMessageContentEvent_Serializes_WithDeltaAndMessageId()
|
||||
{
|
||||
// Arrange
|
||||
ReasoningMessageContentEvent evt = new() { MessageId = "reason1", Delta = "I am thinking" };
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(evt, AGUIJsonSerializerContext.Default.ReasoningMessageContentEvent);
|
||||
JsonElement jsonElement = JsonElement.Parse(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AGUIEventTypes.ReasoningMessageContent, jsonElement.GetProperty("type").GetString());
|
||||
Assert.Equal("reason1", jsonElement.GetProperty("messageId").GetString());
|
||||
Assert.Equal("I am thinking", jsonElement.GetProperty("delta").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReasoningMessageEndEvent_Serializes_WithMessageId()
|
||||
{
|
||||
// Arrange
|
||||
ReasoningMessageEndEvent evt = new() { MessageId = "reason1" };
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(evt, AGUIJsonSerializerContext.Default.ReasoningMessageEndEvent);
|
||||
JsonElement jsonElement = JsonElement.Parse(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AGUIEventTypes.ReasoningMessageEnd, jsonElement.GetProperty("type").GetString());
|
||||
Assert.Equal("reason1", jsonElement.GetProperty("messageId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReasoningEndEvent_Serializes_WithMessageId()
|
||||
{
|
||||
// Arrange
|
||||
ReasoningEndEvent evt = new() { MessageId = "reason1" };
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(evt, AGUIJsonSerializerContext.Default.ReasoningEndEvent);
|
||||
JsonElement jsonElement = JsonElement.Parse(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AGUIEventTypes.ReasoningEnd, jsonElement.GetProperty("type").GetString());
|
||||
Assert.Equal("reason1", jsonElement.GetProperty("messageId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReasoningMessageChunkEvent_Serializes_WithDeltaAndMessageId()
|
||||
{
|
||||
// Arrange
|
||||
ReasoningMessageChunkEvent evt = new() { MessageId = "reason1", Delta = "chunk" };
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(evt, AGUIJsonSerializerContext.Default.ReasoningMessageChunkEvent);
|
||||
JsonElement jsonElement = JsonElement.Parse(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AGUIEventTypes.ReasoningMessageChunk, jsonElement.GetProperty("type").GetString());
|
||||
Assert.Equal("reason1", jsonElement.GetProperty("messageId").GetString());
|
||||
Assert.Equal("chunk", jsonElement.GetProperty("delta").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReasoningEncryptedValueEvent_Serializes_WithAllFields()
|
||||
{
|
||||
// Arrange
|
||||
ReasoningEncryptedValueEvent evt = new() { EntityId = "reason1", EncryptedValue = "tok-abc123" };
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(evt, AGUIJsonSerializerContext.Default.ReasoningEncryptedValueEvent);
|
||||
JsonElement jsonElement = JsonElement.Parse(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AGUIEventTypes.ReasoningEncryptedValue, jsonElement.GetProperty("type").GetString());
|
||||
Assert.Equal("reason1", jsonElement.GetProperty("entityId").GetString());
|
||||
Assert.Equal("tok-abc123", jsonElement.GetProperty("encryptedValue").GetString());
|
||||
Assert.Equal("message", jsonElement.GetProperty("subtype").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllReasoningEventTypes_DeserializeViaBaseEventConverter_ToCorrectTypes()
|
||||
{
|
||||
// Arrange
|
||||
BaseEvent[] events =
|
||||
[
|
||||
new ReasoningStartEvent { MessageId = "r1" },
|
||||
new ReasoningMessageStartEvent { MessageId = "r1" },
|
||||
new ReasoningMessageContentEvent { MessageId = "r1", Delta = "thinking" },
|
||||
new ReasoningMessageEndEvent { MessageId = "r1" },
|
||||
new ReasoningEndEvent { MessageId = "r1" },
|
||||
new ReasoningMessageChunkEvent { MessageId = "r1", Delta = "chunk" },
|
||||
new ReasoningEncryptedValueEvent { EntityId = "r1", EncryptedValue = "tok" }
|
||||
];
|
||||
|
||||
// Act
|
||||
string json = JsonSerializer.Serialize(events, AGUIJsonSerializerContext.Default.Options);
|
||||
var deserialized = JsonSerializer.Deserialize<BaseEvent[]>(json, AGUIJsonSerializerContext.Default.Options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(7, deserialized.Length);
|
||||
Assert.IsType<ReasoningStartEvent>(deserialized[0]);
|
||||
Assert.IsType<ReasoningMessageStartEvent>(deserialized[1]);
|
||||
Assert.IsType<ReasoningMessageContentEvent>(deserialized[2]);
|
||||
Assert.IsType<ReasoningMessageEndEvent>(deserialized[3]);
|
||||
Assert.IsType<ReasoningEndEvent>(deserialized[4]);
|
||||
Assert.IsType<ReasoningMessageChunkEvent>(deserialized[5]);
|
||||
Assert.IsType<ReasoningEncryptedValueEvent>(deserialized[6]);
|
||||
}
|
||||
|
||||
#endregion Reasoning Event Serialization Tests
|
||||
}
|
||||
|
||||
+465
@@ -777,4 +777,469 @@ public sealed class ChatResponseUpdateAGUIExtensionsTests
|
||||
}
|
||||
|
||||
#endregion State Delta Tests
|
||||
|
||||
#region Reasoning Tests
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithReasoningMessageEndForWrongMessageId_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new ReasoningMessageStartEvent { MessageId = "reason1" },
|
||||
new ReasoningMessageContentEvent { MessageId = "reason1", Delta = "thinking..." },
|
||||
new ReasoningMessageEndEvent { MessageId = "reason2" } // Wrong message ID
|
||||
];
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await foreach (var _ in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
// Consume stream to trigger exception
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithReasoningContent_EmitsCorrectReasoningEventSequenceAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("I need to think about this")]) { MessageId = "reason1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.IsType<RunStartedEvent>(outputEvents[0]);
|
||||
var reasoningStart = Assert.IsType<ReasoningStartEvent>(outputEvents[1]);
|
||||
var reasoningId = reasoningStart.MessageId;
|
||||
Assert.NotEqual("reason1", reasoningId);
|
||||
var reasoningMessageStart = Assert.IsType<ReasoningMessageStartEvent>(outputEvents[2]);
|
||||
var reasoningMessageId = reasoningMessageStart.MessageId;
|
||||
Assert.NotEqual(reasoningId, reasoningMessageId);
|
||||
var reasoningContent = Assert.IsType<ReasoningMessageContentEvent>(outputEvents[3]);
|
||||
Assert.Equal(reasoningMessageId, reasoningContent.MessageId);
|
||||
Assert.Equal("I need to think about this", reasoningContent.Delta);
|
||||
var reasoningMessageEnd = Assert.IsType<ReasoningMessageEndEvent>(outputEvents[4]);
|
||||
Assert.Equal(reasoningMessageId, reasoningMessageEnd.MessageId);
|
||||
var reasoningEnd = Assert.IsType<ReasoningEndEvent>(outputEvents[5]);
|
||||
Assert.Equal(reasoningId, reasoningEnd.MessageId);
|
||||
Assert.IsType<RunFinishedEvent>(outputEvents[6]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithMultipleReasoningDeltas_EmitsContentEventPerDeltaAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("First")]) { MessageId = "reason1" },
|
||||
new(ChatRole.Assistant, [new TextReasoningContent(" step")]) { MessageId = "reason1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var contentEvents = outputEvents.OfType<ReasoningMessageContentEvent>().ToList();
|
||||
Assert.Equal(2, contentEvents.Count);
|
||||
Assert.Equal("First", contentEvents[0].Delta);
|
||||
Assert.Equal(" step", contentEvents[1].Delta);
|
||||
|
||||
// Only one START/END pair
|
||||
Assert.Single(outputEvents.OfType<ReasoningStartEvent>());
|
||||
Assert.Single(outputEvents.OfType<ReasoningMessageStartEvent>());
|
||||
Assert.Single(outputEvents.OfType<ReasoningMessageEndEvent>());
|
||||
Assert.Single(outputEvents.OfType<ReasoningEndEvent>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithReasoningAndProtectedData_EmitsEncryptedValueEventAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("thinking") { ProtectedData = "encrypted-abc" }]) { MessageId = "reason1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var reasoningMessageId = outputEvents.OfType<ReasoningMessageStartEvent>().Single().MessageId;
|
||||
Assert.NotEqual("reason1", reasoningMessageId);
|
||||
var encryptedEvent = outputEvents.OfType<ReasoningEncryptedValueEvent>().Single();
|
||||
Assert.Equal(reasoningMessageId, encryptedEvent.EntityId);
|
||||
Assert.Equal("encrypted-abc", encryptedEvent.EncryptedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithReasoningFollowedByText_EmitsBothEventSequencesAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("thinking")]) { MessageId = "reason1" },
|
||||
new(ChatRole.Assistant, [new TextContent("Hello")]) { MessageId = "msg1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains(outputEvents, e => e is ReasoningStartEvent);
|
||||
Assert.Contains(outputEvents, e => e is ReasoningMessageContentEvent);
|
||||
Assert.Contains(outputEvents, e => e is ReasoningEndEvent);
|
||||
Assert.Contains(outputEvents, e => e is TextMessageStartEvent);
|
||||
Assert.Contains(outputEvents, e => e is TextMessageContentEvent);
|
||||
Assert.Contains(outputEvents, e => e is TextMessageEndEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithReasoningAndTextSharingSameMessageId_EmitsDistinctEventIdsAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("thinking")]) { MessageId = "shared1" },
|
||||
new(ChatRole.Assistant, [new TextContent("Hello")]) { MessageId = "shared1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var reasoningId = outputEvents.OfType<ReasoningStartEvent>().Single().MessageId;
|
||||
var reasoningMessageId = outputEvents.OfType<ReasoningMessageStartEvent>().Single().MessageId;
|
||||
var textMessageId = outputEvents.OfType<TextMessageStartEvent>().Single().MessageId;
|
||||
Assert.NotEqual(reasoningId, reasoningMessageId);
|
||||
Assert.NotEqual(reasoningId, textMessageId);
|
||||
Assert.NotEqual(reasoningMessageId, textMessageId);
|
||||
Assert.Equal("shared1", textMessageId);
|
||||
Assert.All(outputEvents.OfType<ReasoningMessageContentEvent>(), e => Assert.Equal(reasoningMessageId, e.MessageId));
|
||||
Assert.Equal(reasoningMessageId, outputEvents.OfType<ReasoningMessageEndEvent>().Single().MessageId);
|
||||
Assert.Equal(reasoningId, outputEvents.OfType<ReasoningEndEvent>().Single().MessageId);
|
||||
Assert.All(outputEvents.OfType<TextMessageContentEvent>(), e => Assert.Equal("shared1", e.MessageId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithReasoningThenTextSharingSameMessageId_ClosesReasoningBlockBeforeTextStartAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("thinking")]) { MessageId = "shared1" },
|
||||
new(ChatRole.Assistant, [new TextContent("Hello")]) { MessageId = "shared1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
int reasoningMessageEndIndex = outputEvents.FindIndex(e => e is ReasoningMessageEndEvent);
|
||||
int reasoningEndIndex = outputEvents.FindIndex(e => e is ReasoningEndEvent);
|
||||
int textMessageStartIndex = outputEvents.FindIndex(e => e is TextMessageStartEvent);
|
||||
Assert.True(reasoningMessageEndIndex < textMessageStartIndex);
|
||||
Assert.True(reasoningEndIndex < textMessageStartIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithReasoningThenToolCallSharingSameMessageId_ClosesReasoningBlockBeforeToolCallStartAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("thinking about which tool to use")]) { MessageId = "shared1" },
|
||||
new(ChatRole.Assistant, [new FunctionCallContent("call-1", "GetWeather", new Dictionary<string, object?> { ["location"] = "Seattle" })]) { MessageId = "shared1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
int reasoningEndIndex = outputEvents.FindIndex(e => e is ReasoningEndEvent);
|
||||
int toolCallStartIndex = outputEvents.FindIndex(e => e is ToolCallStartEvent);
|
||||
Assert.True(reasoningEndIndex < toolCallStartIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithReasoningThenToolResultSharingSameMessageId_ClosesReasoningBlockBeforeToolResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("reflecting on result")]) { MessageId = "shared1" },
|
||||
new(ChatRole.Tool, [new FunctionResultContent("call-1", "72F and sunny")]) { MessageId = "shared1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
int reasoningEndIndex = outputEvents.FindIndex(e => e is ReasoningEndEvent);
|
||||
int toolCallResultIndex = outputEvents.FindIndex(e => e is ToolCallResultEvent);
|
||||
Assert.True(reasoningEndIndex < toolCallResultIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithReasoningMessageSequence_ProducesTextReasoningContentPerDeltaAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new ReasoningStartEvent { MessageId = "reason1" },
|
||||
new ReasoningMessageStartEvent { MessageId = "reason1" },
|
||||
new ReasoningMessageContentEvent { MessageId = "reason1", Delta = "First thought" },
|
||||
new ReasoningMessageContentEvent { MessageId = "reason1", Delta = " and more" },
|
||||
new ReasoningMessageEndEvent { MessageId = "reason1" },
|
||||
new ReasoningEndEvent { MessageId = "reason1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, updates.Count);
|
||||
Assert.All(updates, u => Assert.Equal(ChatRole.Assistant, u.Role));
|
||||
Assert.All(updates, u => Assert.Equal("reason1", u.MessageId));
|
||||
var firstContent = Assert.IsType<TextReasoningContent>(updates[0].Contents[0]);
|
||||
Assert.Equal("First thought", firstContent.Text);
|
||||
var secondContent = Assert.IsType<TextReasoningContent>(updates[1].Contents[0]);
|
||||
Assert.Equal(" and more", secondContent.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithReasoningStartAndEndEvents_DoNotProduceUpdatesAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new ReasoningStartEvent { MessageId = "reason1" },
|
||||
new ReasoningEndEvent { MessageId = "reason1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Empty(updates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithReasoningEncryptedValueEvent_ProducesTextReasoningContentWithProtectedDataAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new ReasoningEncryptedValueEvent { EntityId = "reason1", EncryptedValue = "secret-token" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Single(updates);
|
||||
Assert.Equal(ChatRole.Assistant, updates[0].Role);
|
||||
Assert.Equal("reason1", updates[0].MessageId);
|
||||
var content = Assert.IsType<TextReasoningContent>(updates[0].Contents[0]);
|
||||
Assert.Equal("secret-token", content.ProtectedData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithReasoningMessageChunks_ProducesTextReasoningContentPerChunkAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new ReasoningMessageChunkEvent { MessageId = "reason1", Delta = "chunk one" },
|
||||
new ReasoningMessageChunkEvent { MessageId = "reason1", Delta = " chunk two" },
|
||||
new ReasoningMessageChunkEvent { MessageId = "reason1", Delta = "" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, updates.Count);
|
||||
Assert.All(updates, u => Assert.Equal(ChatRole.Assistant, u.Role));
|
||||
var firstContent = Assert.IsType<TextReasoningContent>(updates[0].Contents[0]);
|
||||
Assert.Equal("chunk one", firstContent.Text);
|
||||
var secondContent = Assert.IsType<TextReasoningContent>(updates[1].Contents[0]);
|
||||
Assert.Equal(" chunk two", secondContent.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithReasoningMessageChunkEmptyDelta_ProducesNoUpdateAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new ReasoningMessageChunkEvent { MessageId = "reason1", Delta = "" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (ChatResponseUpdate update in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Empty(updates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithReasoningMessageStartWhileMessageInProgress_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new ReasoningMessageStartEvent { MessageId = "reason1" },
|
||||
new ReasoningMessageStartEvent { MessageId = "reason2" } // Overlapping start
|
||||
];
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await foreach (var _ in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
// Consume stream to trigger exception
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsChatResponseUpdatesAsync_WithReasoningMessageEndWithoutStart_ThrowsInvalidOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new ReasoningMessageEndEvent { MessageId = "reason1" } // End without start
|
||||
];
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await foreach (var _ in events.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
// Consume stream to trigger exception
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithProtectedDataOnly_EmitsEncryptedValueEventWithoutContentDeltaAsync()
|
||||
{
|
||||
// Arrange — TextReasoningContent with empty text but non-empty ProtectedData
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("") { ProtectedData = "encrypted-only" }]) { MessageId = "reason1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> outputEvents = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
outputEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains(outputEvents, e => e is ReasoningStartEvent);
|
||||
Assert.Contains(outputEvents, e => e is ReasoningMessageStartEvent);
|
||||
Assert.DoesNotContain(outputEvents, e => e is ReasoningMessageContentEvent);
|
||||
var reasoningMessageId = outputEvents.OfType<ReasoningMessageStartEvent>().Single().MessageId;
|
||||
Assert.NotEqual("reason1", reasoningMessageId);
|
||||
var encryptedEvent = outputEvents.OfType<ReasoningEncryptedValueEvent>().Single();
|
||||
Assert.Equal(reasoningMessageId, encryptedEvent.EntityId);
|
||||
Assert.Equal("encrypted-only", encryptedEvent.EncryptedValue);
|
||||
Assert.Contains(outputEvents, e => e is ReasoningMessageEndEvent);
|
||||
Assert.Contains(outputEvents, e => e is ReasoningEndEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReasoningContent_RoundTrip_OutboundThenInbound_PreservesTextAndProtectedDataAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatResponseUpdate> outboundUpdates =
|
||||
[
|
||||
new(ChatRole.Assistant, [new TextReasoningContent("I'm thinking") { ProtectedData = "enc-value" }]) { MessageId = "reason1" }
|
||||
];
|
||||
|
||||
// Act - outbound: ChatResponseUpdate → AGUI events
|
||||
List<BaseEvent> aguilEvents = [];
|
||||
await foreach (BaseEvent evt in outboundUpdates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync("thread1", "run1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
aguilEvents.Add(evt);
|
||||
}
|
||||
|
||||
// Act - inbound: AGUI events → ChatResponseUpdate
|
||||
List<ChatResponseUpdate> inboundUpdates = [];
|
||||
await foreach (ChatResponseUpdate update in aguilEvents.ToAsyncEnumerableAsync().AsChatResponseUpdatesAsync(AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
inboundUpdates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
var reasoningContents = inboundUpdates
|
||||
.SelectMany(u => u.Contents)
|
||||
.OfType<TextReasoningContent>()
|
||||
.ToList();
|
||||
|
||||
Assert.Contains(reasoningContents, c => c.Text == "I'm thinking");
|
||||
Assert.Contains(reasoningContents, c => c.ProtectedData == "enc-value");
|
||||
}
|
||||
|
||||
#endregion Reasoning Tests
|
||||
}
|
||||
|
||||
+5
-7
@@ -14,7 +14,7 @@ public class GitHubCopilotAgentTests
|
||||
private const string SkipReason = "Integration tests require GitHub Copilot CLI installed. For local execution only.";
|
||||
|
||||
private static Task<PermissionRequestResult> OnPermissionRequestAsync(PermissionRequest request, PermissionInvocation invocation)
|
||||
=> Task.FromResult(new PermissionRequestResult { Kind = "approved" });
|
||||
=> Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved });
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithSimplePrompt_ReturnsResponseAsync()
|
||||
@@ -201,11 +201,10 @@ public class GitHubCopilotAgentTests
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
OnPermissionRequest = OnPermissionRequestAsync,
|
||||
McpServers = new Dictionary<string, object>
|
||||
McpServers = new Dictionary<string, McpServerConfig>
|
||||
{
|
||||
["filesystem"] = new McpLocalServerConfig
|
||||
["filesystem"] = new McpStdioServerConfig
|
||||
{
|
||||
Type = "stdio",
|
||||
Command = "npx",
|
||||
Args = ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
||||
Tools = ["*"],
|
||||
@@ -234,11 +233,10 @@ public class GitHubCopilotAgentTests
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
OnPermissionRequest = OnPermissionRequestAsync,
|
||||
McpServers = new Dictionary<string, object>
|
||||
McpServers = new Dictionary<string, McpServerConfig>
|
||||
{
|
||||
["microsoft-learn"] = new McpRemoteServerConfig
|
||||
["microsoft-learn"] = new McpHttpServerConfig
|
||||
{
|
||||
Type = "http",
|
||||
Url = "https://learn.microsoft.com/api/mcp",
|
||||
Tools = ["*"],
|
||||
},
|
||||
|
||||
+2
-2
@@ -111,7 +111,7 @@ public sealed class GitHubCopilotAgentTests
|
||||
var systemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = "Be helpful" };
|
||||
PermissionRequestHandler permissionHandler = (_, _) => Task.FromResult(new PermissionRequestResult());
|
||||
UserInputHandler userInputHandler = (_, _) => Task.FromResult(new UserInputResponse { Answer = "input" });
|
||||
var mcpServers = new Dictionary<string, object> { ["server1"] = new McpLocalServerConfig() };
|
||||
var mcpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig() };
|
||||
|
||||
var source = new SessionConfig
|
||||
{
|
||||
@@ -162,7 +162,7 @@ public sealed class GitHubCopilotAgentTests
|
||||
var systemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = "Be helpful" };
|
||||
PermissionRequestHandler permissionHandler = (_, _) => Task.FromResult(new PermissionRequestResult());
|
||||
UserInputHandler userInputHandler = (_, _) => Task.FromResult(new UserInputResponse { Answer = "input" });
|
||||
var mcpServers = new Dictionary<string, object> { ["server1"] = new McpLocalServerConfig() };
|
||||
var mcpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig() };
|
||||
|
||||
var source = new SessionConfig
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user