// Copyright (C) 2024-2026 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later // Additional attribution terms under GPLv3 §7(b) apply — see LICENSE #include #include #include #include #include #include #include using namespace QodeAssist; namespace { // Round-trips a message through JSON and back, returning the re-serialized // form so it can be compared against the original serialization. Any field // dropped or mangled by fromJson/toJson surfaces as a JSON mismatch. QJsonObject reserialize(const Message &message) { bool ok = false; const QJsonObject json = MessageSerializer::toJson(message); Message restored = MessageSerializer::fromJson(json, &ok); EXPECT_TRUE(ok); return MessageSerializer::toJson(restored); } } // namespace TEST(MessageSerializerTest, RoleAndIdRoundtrip) { Message m(Message::Role::Assistant, QStringLiteral("msg-7")); m.appendBlock(std::make_unique(QStringLiteral("hi"))); const QJsonObject json = MessageSerializer::toJson(m); EXPECT_EQ(json.value("role").toString(), QStringLiteral("assistant")); EXPECT_EQ(json.value("id").toString(), QStringLiteral("msg-7")); EXPECT_EQ(reserialize(m), json); } TEST(MessageSerializerTest, EmptyIdIsOmitted) { Message m(Message::Role::User); m.appendBlock(std::make_unique(QStringLiteral("x"))); const QJsonObject json = MessageSerializer::toJson(m); EXPECT_FALSE(json.contains(QStringLiteral("id"))); EXPECT_EQ(json.value("role").toString(), QStringLiteral("user")); } TEST(MessageSerializerTest, SystemRoleRoundtrip) { Message m(Message::Role::System); m.appendBlock(std::make_unique(QStringLiteral("rules"))); EXPECT_EQ(MessageSerializer::toJson(m).value("role").toString(), QStringLiteral("system")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, ThinkingBlockPreservesSignature) { Message m(Message::Role::Assistant); m.appendBlock( std::make_unique(QStringLiteral("draft"), QStringLiteral("sig"))); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("thinking")); EXPECT_EQ(block.value("thinking").toString(), QStringLiteral("draft")); EXPECT_EQ(block.value("signature").toString(), QStringLiteral("sig")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, RedactedThinkingRoundtrip) { Message m(Message::Role::Assistant); m.appendBlock(std::make_unique(QStringLiteral("blob"))); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("redacted_thinking")); EXPECT_EQ(block.value("signature").toString(), QStringLiteral("blob")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, ImageBase64Roundtrip) { Message m(Message::Role::User); m.appendBlock(std::make_unique( QStringLiteral("ZGF0YQ=="), QStringLiteral("image/png"), LLMQore::ImageContent::ImageSourceType::Base64)); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("image")); EXPECT_EQ(block.value("sourceType").toString(), QStringLiteral("base64")); EXPECT_EQ(block.value("mediaType").toString(), QStringLiteral("image/png")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, ImageUrlSourceTypeRoundtrip) { Message m(Message::Role::User); m.appendBlock(std::make_unique( QStringLiteral("https://example.com/a.png"), QStringLiteral("image/png"), LLMQore::ImageContent::ImageSourceType::Url)); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("sourceType").toString(), QStringLiteral("url")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, ToolUseRoundtrip) { Message m(Message::Role::Assistant); m.appendBlock(std::make_unique( QStringLiteral("tu1"), QStringLiteral("read_file"), QJsonObject{{"path", "a.cpp"}})); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("tool_use")); EXPECT_EQ(block.value("id").toString(), QStringLiteral("tu1")); EXPECT_EQ(block.value("name").toString(), QStringLiteral("read_file")); EXPECT_EQ(block.value("input").toObject().value("path").toString(), QStringLiteral("a.cpp")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, ToolResultRoundtrip) { Message m(Message::Role::User); m.appendBlock( std::make_unique(QStringLiteral("tu1"), QStringLiteral("body"))); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("tool_result")); EXPECT_EQ(block.value("toolUseId").toString(), QStringLiteral("tu1")); EXPECT_EQ(block.value("result").toString(), QStringLiteral("body")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, StoredImageRoundtrip) { Message m(Message::Role::User); m.appendBlock(std::make_unique( QStringLiteral("shot.png"), QStringLiteral("stored/shot"), QStringLiteral("image/png"))); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("stored_image")); EXPECT_EQ(block.value("fileName").toString(), QStringLiteral("shot.png")); EXPECT_EQ(block.value("storedPath").toString(), QStringLiteral("stored/shot")); EXPECT_EQ(block.value("mediaType").toString(), QStringLiteral("image/png")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, StoredAttachmentRoundtrip) { Message m(Message::Role::User); m.appendBlock(std::make_unique( QStringLiteral("notes.txt"), QStringLiteral("stored/notes"))); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("stored_attachment")); EXPECT_EQ(block.value("fileName").toString(), QStringLiteral("notes.txt")); EXPECT_EQ(block.value("storedPath").toString(), QStringLiteral("stored/notes")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, SkillInvocationRoundtrip) { Message m(Message::Role::User); m.appendBlock(std::make_unique( QStringLiteral("review"), QStringLiteral("Review the code."))); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("skill_invocation")); EXPECT_EQ(block.value("skillName").toString(), QStringLiteral("review")); EXPECT_EQ(block.value("body").toString(), QStringLiteral("Review the code.")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, FileEditRoundtripWithStatusAndMessage) { Message m(Message::Role::Assistant); m.appendBlock(std::make_unique( QStringLiteral("e1"), QStringLiteral("a.cpp"), QStringLiteral("old"), QStringLiteral("new"), FileEditContent::Status::Applied, QStringLiteral("done"))); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("type").toString(), QStringLiteral("file_edit")); EXPECT_EQ(block.value("editId").toString(), QStringLiteral("e1")); EXPECT_EQ(block.value("filePath").toString(), QStringLiteral("a.cpp")); EXPECT_EQ(block.value("oldContent").toString(), QStringLiteral("old")); EXPECT_EQ(block.value("newContent").toString(), QStringLiteral("new")); EXPECT_EQ(block.value("status").toString(), QStringLiteral("applied")); EXPECT_EQ(block.value("statusMessage").toString(), QStringLiteral("done")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, FileEditOmitsEmptyStatusMessageAndDefaultsToPending) { Message m(Message::Role::Assistant); m.appendBlock(std::make_unique( QStringLiteral("e1"), QStringLiteral("a.cpp"), QStringLiteral("old"), QStringLiteral("new"))); const QJsonObject block = MessageSerializer::toJson(m).value("blocks").toArray().first().toObject(); EXPECT_EQ(block.value("status").toString(), QStringLiteral("pending")); EXPECT_FALSE(block.contains(QStringLiteral("statusMessage"))); } TEST(MessageSerializerTest, MultipleBlocksPreserveOrder) { Message m(Message::Role::Assistant); m.appendBlock(std::make_unique(QStringLiteral("calling"))); m.appendBlock(std::make_unique( QStringLiteral("tu1"), QStringLiteral("read_file"), QJsonObject())); const QJsonArray blocks = MessageSerializer::toJson(m).value("blocks").toArray(); ASSERT_EQ(blocks.size(), 2); EXPECT_EQ(blocks[0].toObject().value("type").toString(), QStringLiteral("text")); EXPECT_EQ(blocks[1].toObject().value("type").toString(), QStringLiteral("tool_use")); EXPECT_EQ(reserialize(m), MessageSerializer::toJson(m)); } TEST(MessageSerializerTest, UnknownRoleFailsDeserialization) { QJsonObject json; json["role"] = QStringLiteral("operator"); json["blocks"] = QJsonArray{}; bool ok = true; const Message m = MessageSerializer::fromJson(json, &ok); EXPECT_FALSE(ok); EXPECT_TRUE(m.blocks().empty()); } TEST(MessageSerializerTest, EmptyBlocksDeserializeOk) { QJsonObject json; json["role"] = QStringLiteral("user"); json["blocks"] = QJsonArray{}; bool ok = false; const Message m = MessageSerializer::fromJson(json, &ok); EXPECT_TRUE(ok); EXPECT_TRUE(m.blocks().empty()); } TEST(MessageSerializerTest, AllUnknownBlocksFailDeserialization) { QJsonObject json; json["role"] = QStringLiteral("assistant"); json["blocks"] = QJsonArray{QJsonObject{{"type", "future_block"}}}; bool ok = true; const Message m = MessageSerializer::fromJson(json, &ok); EXPECT_FALSE(ok); EXPECT_TRUE(m.blocks().empty()); } TEST(MessageSerializerTest, UnknownBlocksSkippedButKnownKept) { QJsonObject json; json["role"] = QStringLiteral("assistant"); json["blocks"] = QJsonArray{ QJsonObject{{"type", "future_block"}}, QJsonObject{{"type", "text"}, {"text", "kept"}}}; bool ok = false; const Message m = MessageSerializer::fromJson(json, &ok); EXPECT_TRUE(ok); ASSERT_EQ(m.blocks().size(), 1u); EXPECT_EQ(m.text(), QStringLiteral("kept")); }