mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-14 02:09:22 -04:00
refactor: IProjectScanner port; ContextManager QtC-free
This commit is contained in:
@@ -4,6 +4,8 @@ add_executable(QodeAssistTest
|
||||
CodeHandlerTest.cpp
|
||||
DocumentContextReaderTest.cpp
|
||||
LLMSuggestionTest.cpp
|
||||
JsonPromptTemplateTest.cpp
|
||||
ResponseRouterTest.cpp
|
||||
# LLMClientInterfaceTests.cpp
|
||||
unittest_main.cpp
|
||||
)
|
||||
@@ -18,6 +20,9 @@ target_link_libraries(QodeAssistTest PRIVATE
|
||||
Context
|
||||
Common
|
||||
LLMQore
|
||||
Templates
|
||||
Agents
|
||||
Session
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistTest PRIVATE ${CMAKE_SOURCE_DIR})
|
||||
|
||||
129
test/JsonPromptTemplateTest.cpp
Normal file
129
test/JsonPromptTemplateTest.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
// 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 <gtest/gtest.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
#include <ContextData.hpp>
|
||||
#include <JsonPromptTemplate.hpp>
|
||||
|
||||
using QodeAssist::AgentConfig;
|
||||
using QodeAssist::Templates::ContextData;
|
||||
using QodeAssist::Templates::JsonPromptTemplate;
|
||||
|
||||
namespace {
|
||||
|
||||
AgentConfig makeConfig(const QJsonObject &body)
|
||||
{
|
||||
AgentConfig cfg;
|
||||
cfg.name = QStringLiteral("test-agent");
|
||||
cfg.body = body;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const QString kUserMessages
|
||||
= QStringLiteral("[ { \"role\": \"user\", \"content\": {{ tojson(ctx.prefix) }} } ]");
|
||||
|
||||
const QString kSystemField = QStringLiteral(
|
||||
"{% if existsIn(ctx, \"system_prompt\") %}{{ tojson(ctx.system_prompt) }}{% endif %}");
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(JsonPromptTemplateTest, RendersJinjaSplicesAndKeepsLiterals)
|
||||
{
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||
{"max_tokens", 128},
|
||||
{"temperature", 0.5},
|
||||
{"stream", true},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
ctx.prefix = QStringLiteral("hello world");
|
||||
|
||||
QJsonObject request;
|
||||
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||
|
||||
EXPECT_EQ(request.value("max_tokens").toInt(), 128);
|
||||
EXPECT_DOUBLE_EQ(request.value("temperature").toDouble(), 0.5);
|
||||
EXPECT_TRUE(request.value("stream").toBool());
|
||||
|
||||
const QJsonArray messages = request.value("messages").toArray();
|
||||
ASSERT_EQ(messages.size(), 1);
|
||||
EXPECT_EQ(
|
||||
messages.at(0).toObject().value("content").toString(), QStringLiteral("hello world"));
|
||||
}
|
||||
|
||||
TEST(JsonPromptTemplateTest, DropsKeyWhenJinjaRendersEmpty)
|
||||
{
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||
{"system", kSystemField},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
ctx.prefix = QStringLiteral("hi");
|
||||
|
||||
QJsonObject request;
|
||||
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||
|
||||
EXPECT_FALSE(request.contains(QStringLiteral("system")));
|
||||
EXPECT_TRUE(request.contains(QStringLiteral("messages")));
|
||||
}
|
||||
|
||||
TEST(JsonPromptTemplateTest, RendersSystemPromptWhenPresent)
|
||||
{
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||
{"system", kSystemField},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
ctx.prefix = QStringLiteral("hi");
|
||||
ctx.systemPrompt = QStringLiteral("You are a helpful assistant.");
|
||||
|
||||
QJsonObject request;
|
||||
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||
|
||||
EXPECT_EQ(
|
||||
request.value("system").toString(), QStringLiteral("You are a helpful assistant."));
|
||||
}
|
||||
|
||||
TEST(JsonPromptTemplateTest, PreservesNestedLiteralObjects)
|
||||
{
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||
{"thinking", QJsonObject{{"type", "adaptive"}, {"budget", 8192}}},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
ctx.prefix = QStringLiteral("x");
|
||||
|
||||
QJsonObject request;
|
||||
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||
|
||||
const QJsonObject thinking = request.value("thinking").toObject();
|
||||
EXPECT_EQ(thinking.value("type").toString(), QStringLiteral("adaptive"));
|
||||
EXPECT_EQ(thinking.value("budget").toInt(), 8192);
|
||||
}
|
||||
|
||||
TEST(JsonPromptTemplateTest, RejectsBodyThatRendersInvalidJsonAtLoad)
|
||||
{
|
||||
QString error;
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(
|
||||
makeConfig(QJsonObject{
|
||||
{"messages", QStringLiteral("[ {{ tojson(ctx.prefix) }}")},
|
||||
}),
|
||||
&error);
|
||||
|
||||
EXPECT_EQ(tmpl, nullptr);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
}
|
||||
159
test/ResponseRouterTest.cpp
Normal file
159
test/ResponseRouterTest.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
// 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 <gtest/gtest.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkRequest>
|
||||
#include <QVector>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ToolResult.hpp>
|
||||
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <ErrorInfo.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <ResponseEvent.hpp>
|
||||
#include <ResponseRouter.hpp>
|
||||
|
||||
using namespace QodeAssist;
|
||||
|
||||
namespace {
|
||||
|
||||
class FakeClient : public LLMQore::BaseClient
|
||||
{
|
||||
public:
|
||||
using LLMQore::BaseClient::BaseClient;
|
||||
|
||||
void fireChunk(const QString &id, const QString &chunk) { emit chunkReceived(id, chunk); }
|
||||
void fireThinking(const QString &id, const QString &thinking, const QString &signature)
|
||||
{
|
||||
emit thinkingBlockReceived(id, thinking, signature);
|
||||
}
|
||||
void fireToolStarted(
|
||||
const QString &id, const QString &toolId, const QString &name, const QJsonObject &args)
|
||||
{
|
||||
emit toolStarted(id, toolId, name, args);
|
||||
}
|
||||
void fireToolResult(
|
||||
const QString &id, const QString &toolId, const QString &name, const QString &result)
|
||||
{
|
||||
emit toolResultReady(id, toolId, name, result);
|
||||
}
|
||||
void fireFinalized(const QString &id, const LLMQore::CompletionInfo &info)
|
||||
{
|
||||
emit requestFinalized(id, info);
|
||||
}
|
||||
void fireFailed(const QString &id, const QString &error) { emit requestFailed(id, error); }
|
||||
|
||||
protected:
|
||||
LLMQore::RequestID sendMessage(
|
||||
const QJsonObject &, const QString &, LLMQore::RequestMode) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
LLMQore::RequestID ask(const QString &, LLMQore::RequestMode) override { return {}; }
|
||||
QFuture<QList<QString>> listModels() override { return {}; }
|
||||
LLMQore::ToolSchemaFormat toolSchemaFormat() const override
|
||||
{
|
||||
return LLMQore::ToolSchemaFormat::Claude;
|
||||
}
|
||||
void processData(const LLMQore::RequestID &, const QByteArray &) override {}
|
||||
void processBufferedResponse(const LLMQore::RequestID &, const QByteArray &) override {}
|
||||
QNetworkRequest prepareNetworkRequest(const QUrl &) const override { return {}; }
|
||||
LLMQore::BaseMessage *messageForRequest(const LLMQore::RequestID &) const override
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
void cleanupDerivedData(const LLMQore::RequestID &) override {}
|
||||
QJsonObject buildContinuationPayload(
|
||||
const QJsonObject &,
|
||||
LLMQore::BaseMessage *,
|
||||
const QHash<QString, LLMQore::ToolResult> &) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ResponseRouterTest, BuildsAssistantTurnAndEmitsEvents)
|
||||
{
|
||||
FakeClient client;
|
||||
ConversationHistory history;
|
||||
ResponseRouter router(&client, &history);
|
||||
|
||||
QVector<ResponseEvent::Kind> kinds;
|
||||
QObject::connect(&router, &ResponseRouter::event, &router, [&kinds](const ResponseEvent &ev) {
|
||||
kinds.append(ev.kind());
|
||||
});
|
||||
|
||||
const QString id = QStringLiteral("req-1");
|
||||
router.beginRequest(id);
|
||||
client.fireThinking(id, QStringLiteral("pondering"), QStringLiteral("sig"));
|
||||
client.fireChunk(id, QStringLiteral("Hello"));
|
||||
client.fireChunk(id, QStringLiteral(" world"));
|
||||
client.fireToolStarted(
|
||||
id, QStringLiteral("t1"), QStringLiteral("read_file"), QJsonObject{{"path", "a.txt"}});
|
||||
client.fireToolResult(
|
||||
id, QStringLiteral("t1"), QStringLiteral("read_file"), QStringLiteral("contents"));
|
||||
|
||||
LLMQore::CompletionInfo info;
|
||||
info.stopReason = QStringLiteral("end_turn");
|
||||
info.usage = LLMQore::TokenUsage{12, 34, 0, 0};
|
||||
client.fireFinalized(id, info);
|
||||
|
||||
ASSERT_EQ(history.size(), 2);
|
||||
|
||||
const Message &assistant = history.messages()[0];
|
||||
EXPECT_EQ(assistant.role(), Message::Role::Assistant);
|
||||
EXPECT_EQ(assistant.id(), id);
|
||||
EXPECT_EQ(assistant.text(), QStringLiteral("Hello world"));
|
||||
EXPECT_TRUE(assistant.hasToolUse());
|
||||
|
||||
const Message &toolResult = history.messages()[1];
|
||||
EXPECT_EQ(toolResult.role(), Message::Role::User);
|
||||
|
||||
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::ThinkingDelta));
|
||||
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::TextDelta));
|
||||
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::ToolResult));
|
||||
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::Usage));
|
||||
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::MessageStop));
|
||||
}
|
||||
|
||||
TEST(ResponseRouterTest, CategorizesAuthError)
|
||||
{
|
||||
FakeClient client;
|
||||
ConversationHistory history;
|
||||
ResponseRouter router(&client, &history);
|
||||
|
||||
std::optional<ResponseEvents::Error> captured;
|
||||
QObject::connect(&router, &ResponseRouter::event, &router, [&captured](const ResponseEvent &ev) {
|
||||
if (ev.kind() == ResponseEvent::Kind::Error)
|
||||
captured = *ev.as<ResponseEvents::Error>();
|
||||
});
|
||||
|
||||
router.beginRequest(QStringLiteral("req-2"));
|
||||
client.fireFailed(
|
||||
QStringLiteral("req-2"), QStringLiteral("HTTP 401 Unauthorized: invalid api key"));
|
||||
|
||||
ASSERT_TRUE(captured.has_value());
|
||||
EXPECT_EQ(captured->category, ErrorCategory::Auth);
|
||||
}
|
||||
|
||||
TEST(ResponseRouterTest, IgnoresEventsForInactiveRequest)
|
||||
{
|
||||
FakeClient client;
|
||||
ConversationHistory history;
|
||||
ResponseRouter router(&client, &history);
|
||||
|
||||
router.beginRequest(QStringLiteral("req-3"));
|
||||
client.fireChunk(QStringLiteral("OTHER"), QStringLiteral("ignored"));
|
||||
|
||||
EXPECT_TRUE(history.isEmpty());
|
||||
}
|
||||
Reference in New Issue
Block a user