Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt). Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e): Properties, Record-Parameter, Enum-Member sowie die Parameter der Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.1 KiB
C#
150 lines
5.1 KiB
C#
using System.Text.Json;
|
|
using ClawdDotNet.Core.Api.Models;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Api;
|
|
|
|
public sealed class ChatMessageSerializationTests
|
|
{
|
|
private static string Serialize(ChatMessage m) => JsonSerializer.Serialize(m);
|
|
private static ChatMessage Deserialize(string json) => JsonSerializer.Deserialize<ChatMessage>(json)!;
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Standard format
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Without_breakpoint_the_content_stays_a_plain_string()
|
|
{
|
|
var json = Serialize(ChatMessage.User("Hallo"));
|
|
|
|
json.ShouldBe("""{"role":"user","content":"Hallo"}""");
|
|
}
|
|
|
|
[Fact]
|
|
public void Null_values_are_omitted()
|
|
{
|
|
var json = Serialize(ChatMessage.Assistant("Antwort"));
|
|
|
|
json.ShouldNotContain("tool_calls");
|
|
json.ShouldNotContain("tool_call_id");
|
|
}
|
|
|
|
[Fact]
|
|
public void Tool_answer_carries_its_ToolCallId()
|
|
{
|
|
var json = Serialize(ChatMessage.ToolResponse("call_42", "Ergebnis"));
|
|
|
|
json.ShouldBe("""{"role":"tool","content":"Ergebnis","tool_call_id":"call_42"}""");
|
|
}
|
|
|
|
[Fact]
|
|
public void AssistantWithToolCalls_serialises_the_calls()
|
|
{
|
|
var msg = ChatMessage.AssistantWithToolCalls([
|
|
new ToolCall { Id = "c1", Function = new ToolCallFunction { Name = "FileRW", Arguments = "{}" } }
|
|
]);
|
|
|
|
var json = Serialize(msg);
|
|
|
|
json.ShouldContain("tool_calls");
|
|
json.ShouldContain("FileRW");
|
|
json.ShouldNotContain("\"content\"", Case.Sensitive);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Cache-Breakpoint
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void With_breakpoint_the_content_becomes_a_block_with_cache_control()
|
|
{
|
|
var msg = ChatMessage.System("Du bist ein Agent.");
|
|
msg.CacheBreakpoint = true;
|
|
|
|
var json = Serialize(msg);
|
|
|
|
json.ShouldBe(
|
|
"""{"role":"system","content":[{"type":"text","text":"Du bist ein Agent.","cache_control":{"type":"ephemeral"}}]}""");
|
|
}
|
|
|
|
[Fact]
|
|
public void The_breakpoint_itself_is_not_serialised_as_a_field()
|
|
{
|
|
var msg = ChatMessage.User("Text");
|
|
msg.CacheBreakpoint = true;
|
|
|
|
Serialize(msg).ShouldNotContain("CacheBreakpoint", Case.Insensitive);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Deserialization: both formats
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Content_as_string_is_read()
|
|
{
|
|
var msg = Deserialize("""{"role":"assistant","content":"Antwort"}""");
|
|
|
|
msg.Role.ShouldBe("assistant");
|
|
msg.Content.ShouldBe("Antwort");
|
|
}
|
|
|
|
[Fact]
|
|
public void Content_as_block_array_is_read()
|
|
{
|
|
// This is what a persisted context written with a breakpoint looks like.
|
|
var msg = Deserialize(
|
|
"""{"role":"system","content":[{"type":"text","text":"Prompt","cache_control":{"type":"ephemeral"}}]}""");
|
|
|
|
msg.Content.ShouldBe("Prompt");
|
|
}
|
|
|
|
[Fact]
|
|
public void Several_text_blocks_are_merged()
|
|
{
|
|
var msg = Deserialize(
|
|
"""{"role":"user","content":[{"type":"text","text":"Teil A"},{"type":"text","text":" Teil B"}]}""");
|
|
|
|
msg.Content.ShouldBe("Teil A Teil B");
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_content_becomes_null()
|
|
{
|
|
var msg = Deserialize("""{"role":"assistant","tool_calls":[]}""");
|
|
|
|
msg.Content.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Unknown_fields_do_no_harm()
|
|
{
|
|
var msg = Deserialize("""{"role":"user","content":"Text","reasoning":"…","annotations":[1,2]}""");
|
|
|
|
msg.Content.ShouldBe("Text");
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundTrip_keeps_all_fields()
|
|
{
|
|
var original = ChatMessage.ToolResponse("call_7", "Das Ergebnis");
|
|
|
|
var restored = Deserialize(Serialize(original));
|
|
|
|
restored.Role.ShouldBe("tool");
|
|
restored.Content.ShouldBe("Das Ergebnis");
|
|
restored.ToolCallId.ShouldBe("call_7");
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundTrip_keeps_umlauts_and_emoji()
|
|
{
|
|
var original = ChatMessage.User("Grüße aus München 🦀 — größer & schöner");
|
|
|
|
var restored = Deserialize(Serialize(original));
|
|
|
|
restored.Content.ShouldBe("Grüße aus München 🦀 — größer & schöner");
|
|
}
|
|
}
|