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)!; // ═══════════════════════════════════════════════════════════ // Standardformat // ═══════════════════════════════════════════════════════════ [Fact] public void Ohne_Breakpoint_bleibt_der_Inhalt_ein_einfacher_String() { var json = Serialize(ChatMessage.User("Hallo")); json.ShouldBe("""{"role":"user","content":"Hallo"}"""); } [Fact] public void Nullwerte_werden_weggelassen() { var json = Serialize(ChatMessage.Assistant("Antwort")); json.ShouldNotContain("tool_calls"); json.ShouldNotContain("tool_call_id"); } [Fact] public void ToolAntwort_traegt_ihre_ToolCallId() { var json = Serialize(ChatMessage.ToolResponse("call_42", "Ergebnis")); json.ShouldBe("""{"role":"tool","content":"Ergebnis","tool_call_id":"call_42"}"""); } [Fact] public void AssistantMitToolCalls_serialisiert_die_Aufrufe() { 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 Mit_Breakpoint_wird_der_Inhalt_zum_Block_mit_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 Der_Breakpoint_selbst_wird_nicht_als_Feld_serialisiert() { var msg = ChatMessage.User("Text"); msg.CacheBreakpoint = true; Serialize(msg).ShouldNotContain("CacheBreakpoint", Case.Insensitive); } // ═══════════════════════════════════════════════════════════ // Deserialisierung: beide Formate // ═══════════════════════════════════════════════════════════ [Fact] public void Inhalt_als_String_wird_gelesen() { var msg = Deserialize("""{"role":"assistant","content":"Antwort"}"""); msg.Role.ShouldBe("assistant"); msg.Content.ShouldBe("Antwort"); } [Fact] public void Inhalt_als_Blockarray_wird_gelesen() { // So sieht ein persistierter Kontext aus, der mit Breakpoint geschrieben wurde. var msg = Deserialize( """{"role":"system","content":[{"type":"text","text":"Prompt","cache_control":{"type":"ephemeral"}}]}"""); msg.Content.ShouldBe("Prompt"); } [Fact] public void Mehrere_Textbloecke_werden_zusammengefuehrt() { 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 Fehlender_Inhalt_wird_zu_null() { var msg = Deserialize("""{"role":"assistant","tool_calls":[]}"""); msg.Content.ShouldBeNull(); } [Fact] public void Unbekannte_Felder_stoeren_nicht() { var msg = Deserialize("""{"role":"user","content":"Text","reasoning":"…","annotations":[1,2]}"""); msg.Content.ShouldBe("Text"); } [Fact] public void RoundTrip_erhaelt_alle_Felder() { 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_erhaelt_Umlaute_und_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"); } }