#include "TestHarness.h" #include "src/ai/ConversationStore.h" #include #include #include #include #include #include #include #include #include #include namespace { ChatMessage message(const QString &role, const QString &content, int minute) { return { role, content, QDateTime(QDate(2026, 7, 17), QTime(10, minute)), QStringLiteral("openai"), QStringLiteral("test-model"), }; } bool writeBytes(const QString &filePath, const QByteArray &bytes) { QFile file(filePath); return file.open(QIODevice::WriteOnly | QIODevice::Truncate) && file.write(bytes) == bytes.size(); } } QA_TEST(ConversationStore, SavesLoadsAndLimitsEvenHistory) { QTemporaryDir directory; QA_EXPECT(directory.isValid()); const QString filePath = directory.filePath(QStringLiteral("conversation_history.json")); const ConversationStore store(filePath); const QVector messages = { message(QStringLiteral("user"), QStringLiteral("one"), 0), message(QStringLiteral("assistant"), QStringLiteral("two"), 1), message(QStringLiteral("user"), QStringLiteral("three"), 2), message(QStringLiteral("assistant"), QStringLiteral("four"), 3), }; QA_EXPECT(store.save(messages, 4)); const QVector loaded = store.load(3); QA_EXPECT_EQ(loaded.size(), 2); QA_EXPECT_EQ(loaded.first().content, QStringLiteral("three")); QA_EXPECT_EQ(loaded.last().model, QStringLiteral("test-model")); } QA_TEST(ConversationStore, AvoidsDuplicatingOverlappingMessages) { QTemporaryDir directory; QA_EXPECT(directory.isValid()); const ConversationStore store(directory.filePath(QStringLiteral("history.json"))); const QVector firstPair = { message(QStringLiteral("user"), QStringLiteral("hello"), 0), message(QStringLiteral("assistant"), QStringLiteral("world"), 1), }; QA_EXPECT(store.save(firstPair, 10)); QA_EXPECT(store.save(firstPair, 10)); QA_EXPECT_EQ(store.load(10).size(), 2); } QA_TEST(ConversationStore, LoadsLegacyCreatedAtField) { QTemporaryDir directory; QA_EXPECT(directory.isValid()); const QString filePath = directory.filePath(QStringLiteral("legacy.json")); QJsonArray messages; messages.append(QJsonObject{ {QStringLiteral("role"), QStringLiteral("user")}, {QStringLiteral("content"), QStringLiteral("legacy-user")}, {QStringLiteral("createdAt"), QStringLiteral("2026-07-17T10:00:00")}, }); messages.append(QJsonObject{ {QStringLiteral("role"), QStringLiteral("assistant")}, {QStringLiteral("content"), QStringLiteral("legacy-assistant")}, {QStringLiteral("createdAt"), QStringLiteral("2026-07-17T10:00:01")}, }); QA_EXPECT(writeBytes(filePath, QJsonDocument(QJsonObject{{QStringLiteral("messages"), messages}}).toJson())); const QVector loaded = ConversationStore(filePath).load(10); QA_EXPECT_EQ(loaded.size(), 2); QA_EXPECT(loaded.first().timestamp.isValid()); QA_EXPECT(loaded.first().provider.isEmpty()); } QA_TEST(ConversationStore, BacksUpBrokenJsonAndClearsHistory) { QTemporaryDir directory; QA_EXPECT(directory.isValid()); const QString filePath = directory.filePath(QStringLiteral("conversation_history.json")); QA_EXPECT(writeBytes(filePath, QByteArrayLiteral("{broken"))); QString errorMessage; const ConversationStore store(filePath); QA_EXPECT(store.load(10, &errorMessage).isEmpty()); QA_EXPECT(!errorMessage.isEmpty()); QA_EXPECT(!QFile::exists(filePath)); QA_EXPECT_EQ( QDir(directory.path()).entryList({QStringLiteral("conversation_history.broken.*.json")}, QDir::Files).size(), 1); QA_EXPECT(store.save({ message(QStringLiteral("user"), QStringLiteral("hello"), 0), message(QStringLiteral("assistant"), QStringLiteral("world"), 1), }, 10)); QA_EXPECT(store.clear()); QA_EXPECT(!QFile::exists(filePath)); }