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(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"); } }