mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-08 14:09:14 -04:00
fix: Found and fix review mistakes
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
@@ -25,13 +26,14 @@ void writeFile(const QString &dir, const QString &name, const QByteArray &conten
|
||||
|
||||
QByteArray minimalAgent(const QByteArray &name, const QByteArray &extra = {})
|
||||
{
|
||||
return "name = \"" + name + "\"\n"
|
||||
"provider_instance = \"P\"\n"
|
||||
"model = \"m\"\n"
|
||||
"endpoint = \"/e\"\n"
|
||||
+ extra +
|
||||
"[body]\n"
|
||||
"stream = true\n";
|
||||
return "name = \"" + name
|
||||
+ "\"\n"
|
||||
"provider_instance = \"P\"\n"
|
||||
"model = \"m\"\n"
|
||||
"endpoint = \"/e\"\n"
|
||||
+ extra
|
||||
+ "[body]\n"
|
||||
"stream = true\n";
|
||||
}
|
||||
|
||||
const AgentConfig *findConfig(const AgentLoader::LoadResult &result, const QString &name)
|
||||
@@ -58,11 +60,14 @@ TEST(AgentLoaderTest, WarnsOnUnknownTopLevelAndMatchKeys)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
writeFile(dir.path(), "a.toml",
|
||||
minimalAgent("A",
|
||||
"enable_thinkin = true\n"
|
||||
"[match]\n"
|
||||
"file_pattern = [\"*.cpp\"]\n"));
|
||||
writeFile(
|
||||
dir.path(),
|
||||
"a.toml",
|
||||
minimalAgent(
|
||||
"A",
|
||||
"enable_thinkin = true\n"
|
||||
"[match]\n"
|
||||
"file_pattern = [\"*.cpp\"]\n"));
|
||||
|
||||
const auto result = AgentLoader::load(QString(), dir.path());
|
||||
EXPECT_TRUE(result.errors.isEmpty()) << result.errors.join("; ").toStdString();
|
||||
@@ -124,23 +129,29 @@ TEST(AgentLoaderTest, UserAgentExtendsBundledProviderBase)
|
||||
QTemporaryDir user;
|
||||
ASSERT_TRUE(bundled.isValid());
|
||||
ASSERT_TRUE(user.isValid());
|
||||
writeFile(bundled.path(), "base.toml",
|
||||
"name = \"Provider Base\"\n"
|
||||
"abstract = true\n"
|
||||
"provider_instance = \"P\"\n"
|
||||
"endpoint = \"/e\"\n"
|
||||
"[body]\n"
|
||||
"stream = true\n");
|
||||
writeFile(bundled.path(), "a.toml",
|
||||
"name = \"A\"\n"
|
||||
"extends = \"Provider Base\"\n"
|
||||
"model = \"stock-model\"\n");
|
||||
writeFile(user.path(), "mine.toml",
|
||||
"name = \"My A\"\n"
|
||||
"extends = \"Provider Base\"\n"
|
||||
"model = \"my-model\"\n"
|
||||
"[body]\n"
|
||||
"temperature = 0.2\n");
|
||||
writeFile(
|
||||
bundled.path(),
|
||||
"base.toml",
|
||||
"name = \"Provider Base\"\n"
|
||||
"abstract = true\n"
|
||||
"provider_instance = \"P\"\n"
|
||||
"endpoint = \"/e\"\n"
|
||||
"[body]\n"
|
||||
"stream = true\n");
|
||||
writeFile(
|
||||
bundled.path(),
|
||||
"a.toml",
|
||||
"name = \"A\"\n"
|
||||
"extends = \"Provider Base\"\n"
|
||||
"model = \"stock-model\"\n");
|
||||
writeFile(
|
||||
user.path(),
|
||||
"mine.toml",
|
||||
"name = \"My A\"\n"
|
||||
"extends = \"Provider Base\"\n"
|
||||
"model = \"my-model\"\n"
|
||||
"[body]\n"
|
||||
"temperature = 0.2\n");
|
||||
|
||||
const auto result = AgentLoader::load(bundled.path(), user.path());
|
||||
EXPECT_TRUE(result.errors.isEmpty()) << result.errors.join("; ").toStdString();
|
||||
@@ -163,7 +174,8 @@ TEST(AgentLoaderTest, ExtendsUnknownParentErrorNamesChild)
|
||||
writeFile(dir.path(), "child.toml", minimalAgent("Child", "extends = \"NoSuchBase\"\n"));
|
||||
|
||||
const auto result = AgentLoader::load(QString(), dir.path());
|
||||
EXPECT_TRUE(anyContains(result.errors, QStringLiteral("'Child' extends unknown agent 'NoSuchBase'")));
|
||||
EXPECT_TRUE(
|
||||
anyContains(result.errors, QStringLiteral("'Child' extends unknown agent 'NoSuchBase'")));
|
||||
EXPECT_EQ(findConfig(result, QStringLiteral("Child")), nullptr);
|
||||
}
|
||||
|
||||
@@ -182,3 +194,91 @@ TEST(AgentLoaderTest, ParseFileReportsWarningsForOwnFileOnly)
|
||||
EXPECT_TRUE(anyContains(warnings, QStringLiteral("another_bogus")));
|
||||
EXPECT_FALSE(anyContains(warnings, QStringLiteral("bogus_key")));
|
||||
}
|
||||
|
||||
TEST(AgentLoaderTest, CyclicExtendsRejectsAllInvolvedAgents)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
writeFile(dir.path(), "a.toml", minimalAgent("A", "extends = \"B\"\n"));
|
||||
writeFile(dir.path(), "b.toml", minimalAgent("B", "extends = \"A\"\n"));
|
||||
|
||||
const auto result = AgentLoader::load(QString(), dir.path());
|
||||
EXPECT_TRUE(anyContains(result.errors, QStringLiteral("Cyclic 'extends'")));
|
||||
EXPECT_EQ(findConfig(result, QStringLiteral("A")), nullptr);
|
||||
EXPECT_EQ(findConfig(result, QStringLiteral("B")), nullptr);
|
||||
}
|
||||
|
||||
TEST(AgentLoaderTest, SelfExtendsIsRejected)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
writeFile(dir.path(), "a.toml", minimalAgent("A", "extends = \"A\"\n"));
|
||||
|
||||
const auto result = AgentLoader::load(QString(), dir.path());
|
||||
EXPECT_TRUE(anyContains(result.errors, QStringLiteral("Cyclic 'extends'")));
|
||||
EXPECT_EQ(findConfig(result, QStringLiteral("A")), nullptr);
|
||||
}
|
||||
|
||||
TEST(AgentLoaderTest, ExtendsChainTooDeepRejectsChain)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const int chainLength = 40;
|
||||
writeFile(dir.path(), "agent0.toml", minimalAgent("Agent0"));
|
||||
for (int i = 1; i <= chainLength; ++i) {
|
||||
writeFile(
|
||||
dir.path(),
|
||||
QStringLiteral("agent%1.toml").arg(i),
|
||||
minimalAgent(
|
||||
QStringLiteral("Agent%1").arg(i).toUtf8(),
|
||||
QStringLiteral("extends = \"Agent%1\"\n").arg(i - 1).toUtf8()));
|
||||
}
|
||||
|
||||
const auto result = AgentLoader::load(QString(), dir.path());
|
||||
EXPECT_TRUE(anyContains(result.errors, QStringLiteral("too deep")));
|
||||
EXPECT_EQ(findConfig(result, QStringLiteral("Agent%1").arg(chainLength)), nullptr);
|
||||
EXPECT_NE(findConfig(result, QStringLiteral("Agent0")), nullptr);
|
||||
}
|
||||
|
||||
TEST(AgentLoaderTest, ParseFileRejectsBundledAgentShadow)
|
||||
{
|
||||
QTemporaryDir bundled;
|
||||
QTemporaryDir user;
|
||||
ASSERT_TRUE(bundled.isValid());
|
||||
ASSERT_TRUE(user.isValid());
|
||||
writeFile(bundled.path(), "a.toml", minimalAgent("A", "description = \"base\"\n"));
|
||||
writeFile(user.path(), "shadow.toml", minimalAgent("A", "description = \"mine\"\n"));
|
||||
|
||||
QString error;
|
||||
const auto cfg = AgentLoader::parseFile(
|
||||
user.path() + QStringLiteral("/shadow.toml"), bundled.path(), &error);
|
||||
EXPECT_FALSE(cfg.has_value());
|
||||
EXPECT_TRUE(error.contains(QStringLiteral("cannot be replaced"))) << error.toStdString();
|
||||
}
|
||||
|
||||
TEST(AgentLoaderTest, ChildArrayReplacesParentArray)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
writeFile(
|
||||
dir.path(),
|
||||
"parent.toml",
|
||||
minimalAgent("Parent", "tags = [\"a\", \"b\"]\n") + "stop = [\"one\", \"two\"]\n");
|
||||
writeFile(
|
||||
dir.path(),
|
||||
"child.toml",
|
||||
"name = \"Child\"\n"
|
||||
"extends = \"Parent\"\n"
|
||||
"tags = [\"c\"]\n"
|
||||
"[body]\n"
|
||||
"stop = [\"three\"]\n");
|
||||
|
||||
const auto result = AgentLoader::load(QString(), dir.path());
|
||||
EXPECT_TRUE(result.errors.isEmpty()) << result.errors.join("; ").toStdString();
|
||||
const AgentConfig *child = findConfig(result, QStringLiteral("Child"));
|
||||
ASSERT_NE(child, nullptr);
|
||||
EXPECT_EQ(child->tags, QStringList{QStringLiteral("c")});
|
||||
const auto stops = child->body.value(QStringLiteral("stop")).toArray();
|
||||
ASSERT_EQ(stops.size(), 1);
|
||||
EXPECT_EQ(stops.first().toString(), QStringLiteral("three"));
|
||||
}
|
||||
|
||||
@@ -99,6 +99,5 @@ TEST(AgentRouterTest, UnconstrainedDimensionDoesNotBlock)
|
||||
AgentConfig::Match m;
|
||||
m.projectNames = {QStringLiteral("P")};
|
||||
|
||||
// file path is irrelevant because no file/path patterns are set
|
||||
EXPECT_TRUE(matches(m, ctx(QString(), QStringLiteral("P"))));
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ TEST(BundledAgentsTest, AllBundledAgentsLoadResolveAndRender)
|
||||
const AgentLoader::LoadResult result = AgentLoader::load(QStringLiteral(":/agents"), QString());
|
||||
|
||||
EXPECT_TRUE(result.errors.isEmpty())
|
||||
<< "bundled agent load errors: "
|
||||
<< result.errors.join(QStringLiteral("; ")).toStdString();
|
||||
<< "bundled agent load errors: " << result.errors.join(QStringLiteral("; ")).toStdString();
|
||||
|
||||
EXPECT_TRUE(result.warnings.isEmpty())
|
||||
<< "bundled agent load warnings: "
|
||||
@@ -45,8 +44,7 @@ TEST(BundledAgentsTest, AllBundledSystemPromptsResolveQrcResources)
|
||||
Q_INIT_RESOURCE(agents);
|
||||
|
||||
const AgentLoader::LoadResult result = AgentLoader::load(QStringLiteral(":/agents"), QString());
|
||||
ASSERT_TRUE(result.errors.isEmpty())
|
||||
<< result.errors.join(QStringLiteral("; ")).toStdString();
|
||||
ASSERT_TRUE(result.errors.isEmpty()) << result.errors.join(QStringLiteral("; ")).toStdString();
|
||||
ASSERT_FALSE(result.configs.empty());
|
||||
|
||||
const QStringList languages = {QString(), QStringLiteral("qml"), QStringLiteral("c-like")};
|
||||
@@ -64,8 +62,8 @@ TEST(BundledAgentsTest, AllBundledSystemPromptsResolveQrcResources)
|
||||
const QString rendered = render(cfg.systemPrompt, bindings, &error);
|
||||
|
||||
EXPECT_TRUE(error.isEmpty())
|
||||
<< "agent '" << cfg.name.toStdString() << "' (language='"
|
||||
<< lang.toStdString() << "') system_prompt render error: " << error.toStdString();
|
||||
<< "agent '" << cfg.name.toStdString() << "' (language='" << lang.toStdString()
|
||||
<< "') system_prompt render error: " << error.toStdString();
|
||||
EXPECT_FALSE(rendered.trimmed().isEmpty())
|
||||
<< "agent '" << cfg.name.toStdString() << "' (language='" << lang.toStdString()
|
||||
<< "') system_prompt rendered empty — a read_file(\":/...\") path is likely broken";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
add_executable(QodeAssistTest
|
||||
../CodeHandler.cpp
|
||||
../LLMSuggestion.cpp
|
||||
../sources/settings/PipelinesConfig.cpp
|
||||
CodeHandlerTest.cpp
|
||||
DocumentContextReaderTest.cpp
|
||||
EnvBlockFormatterTest.cpp
|
||||
@@ -16,9 +17,9 @@ add_executable(QodeAssistTest
|
||||
ContextRendererTest.cpp
|
||||
ErrorInfoTest.cpp
|
||||
MessageSerializerTest.cpp
|
||||
PipelinesConfigTest.cpp
|
||||
ResponseCleanerTest.cpp
|
||||
SystemPromptBuilderTest.cpp
|
||||
# LLMClientInterfaceTests.cpp
|
||||
unittest_main.cpp
|
||||
)
|
||||
|
||||
@@ -35,9 +36,17 @@ target_link_libraries(QodeAssistTest PRIVATE
|
||||
Templates
|
||||
Agents
|
||||
Session
|
||||
TomlSerializer
|
||||
tomlplusplus::tomlplusplus
|
||||
QodeAssistLogger
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistTest PRIVATE ${CMAKE_SOURCE_DIR})
|
||||
target_include_directories(QodeAssistTest PRIVATE
|
||||
${CMAKE_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/sources/settings
|
||||
${CMAKE_SOURCE_DIR}/sources/tomlSerializer
|
||||
${CMAKE_SOURCE_DIR}/logger
|
||||
)
|
||||
|
||||
target_compile_definitions(QodeAssistTest PRIVATE CMAKE_CURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
|
||||
@@ -226,8 +226,7 @@ TEST(ClaudeCacheControlTest, EmptyBreakpointListMarksEveryDimension)
|
||||
apply(request, false, QStringList());
|
||||
|
||||
EXPECT_TRUE(request.value("system").isArray());
|
||||
EXPECT_TRUE(
|
||||
request.value("tools").toArray().last().toObject().contains("cache_control"));
|
||||
EXPECT_TRUE(request.value("tools").toArray().last().toObject().contains("cache_control"));
|
||||
const QJsonArray msgs = request.value("messages").toArray();
|
||||
EXPECT_TRUE(msgs[0].toObject().value("content").isArray());
|
||||
}
|
||||
|
||||
@@ -27,9 +27,7 @@ Message textMessage(Message::Role role, const QString &text)
|
||||
|
||||
ContextAssembler::ContentLoader base64Loader(const QString &content)
|
||||
{
|
||||
return [content](const QString &) {
|
||||
return QString::fromUtf8(content.toUtf8().toBase64());
|
||||
};
|
||||
return [content](const QString &) { return QString::fromUtf8(content.toUtf8().toBase64()); };
|
||||
}
|
||||
|
||||
ContextAssembler::ContentLoader emptyLoader()
|
||||
@@ -145,8 +143,7 @@ TEST(ContextAssemblerTest, OrphanToolUseIsDropped)
|
||||
std::vector<Message> history;
|
||||
Message m(Message::Role::Assistant);
|
||||
m.appendBlock(std::make_unique<LLMQore::TextContent>("calling"));
|
||||
m.appendBlock(
|
||||
std::make_unique<LLMQore::ToolUseContent>("tu1", "read_file", QJsonObject()));
|
||||
m.appendBlock(std::make_unique<LLMQore::ToolUseContent>("tu1", "read_file", QJsonObject()));
|
||||
history.push_back(std::move(m));
|
||||
|
||||
ContextAssembler::Manifest manifest;
|
||||
@@ -179,8 +176,8 @@ TEST(ContextAssemblerTest, PairedToolUseAndResultAreKept)
|
||||
{
|
||||
std::vector<Message> history;
|
||||
Message use(Message::Role::Assistant);
|
||||
use.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||
"tu1", "read_file", QJsonObject{{"path", "a.cpp"}}));
|
||||
use.appendBlock(
|
||||
std::make_unique<LLMQore::ToolUseContent>("tu1", "read_file", QJsonObject{{"path", "a.cpp"}}));
|
||||
history.push_back(std::move(use));
|
||||
Message result(Message::Role::User);
|
||||
result.appendBlock(std::make_unique<LLMQore::ToolResultContent>("tu1", "contents"));
|
||||
@@ -205,8 +202,7 @@ TEST(ContextAssemblerTest, AttachmentMaterializedThroughLoader)
|
||||
m.appendBlock(std::make_unique<StoredAttachmentContent>("notes.txt", "stored/notes"));
|
||||
history.push_back(std::move(m));
|
||||
|
||||
const auto ctx
|
||||
= ContextAssembler::assemble(history, QString(), base64Loader("file body"));
|
||||
const auto ctx = ContextAssembler::assemble(history, QString(), base64Loader("file body"));
|
||||
|
||||
ASSERT_TRUE(ctx.history.has_value());
|
||||
const auto &block = ctx.history->first().blocks.first();
|
||||
@@ -235,8 +231,7 @@ TEST(ContextAssemblerTest, StoredImageMaterializedThroughLoader)
|
||||
{
|
||||
std::vector<Message> history;
|
||||
Message m(Message::Role::User);
|
||||
m.appendBlock(
|
||||
std::make_unique<StoredImageContent>("shot.png", "stored/shot", "image/png"));
|
||||
m.appendBlock(std::make_unique<StoredImageContent>("shot.png", "stored/shot", "image/png"));
|
||||
history.push_back(std::move(m));
|
||||
|
||||
ContextAssembler::Manifest manifest;
|
||||
@@ -255,8 +250,7 @@ TEST(ContextAssemblerTest, MissingImageWithNullLoaderGetsPlaceholder)
|
||||
{
|
||||
std::vector<Message> history;
|
||||
Message m(Message::Role::User);
|
||||
m.appendBlock(
|
||||
std::make_unique<StoredImageContent>("shot.png", "stored/shot", "image/png"));
|
||||
m.appendBlock(std::make_unique<StoredImageContent>("shot.png", "stored/shot", "image/png"));
|
||||
history.push_back(std::move(m));
|
||||
|
||||
ContextAssembler::Manifest manifest;
|
||||
@@ -343,8 +337,7 @@ TEST(ContextAssemblerTest, PinnedAnchorsToTypedMessageNotToolResults)
|
||||
std::vector<Message> history;
|
||||
history.push_back(textMessage(Message::Role::User, "fix the bug"));
|
||||
Message use(Message::Role::Assistant);
|
||||
use.appendBlock(
|
||||
std::make_unique<LLMQore::ToolUseContent>("tu1", "edit_file", QJsonObject()));
|
||||
use.appendBlock(std::make_unique<LLMQore::ToolUseContent>("tu1", "edit_file", QJsonObject()));
|
||||
history.push_back(std::move(use));
|
||||
Message result(Message::Role::User);
|
||||
result.appendBlock(std::make_unique<LLMQore::ToolResultContent>("tu1", "edited"));
|
||||
@@ -366,8 +359,7 @@ TEST(ContextAssemblerTest, PinnedInsertedAfterLeadingToolResults)
|
||||
{
|
||||
std::vector<Message> history;
|
||||
Message use(Message::Role::Assistant);
|
||||
use.appendBlock(
|
||||
std::make_unique<LLMQore::ToolUseContent>("tu1", "edit_file", QJsonObject()));
|
||||
use.appendBlock(std::make_unique<LLMQore::ToolUseContent>("tu1", "edit_file", QJsonObject()));
|
||||
history.push_back(std::move(use));
|
||||
Message result(Message::Role::User);
|
||||
result.appendBlock(std::make_unique<LLMQore::ToolResultContent>("tu1", "edited"));
|
||||
|
||||
@@ -138,6 +138,30 @@ TEST(ContextRendererTest, FileExistsOutsideAllowedRootsThrowsLoudly)
|
||||
EXPECT_TRUE(error.contains(QStringLiteral("file_exists")));
|
||||
}
|
||||
|
||||
TEST(ContextRendererTest, FileExistsWithUnresolvedProjectDirReturnsFalse)
|
||||
{
|
||||
QString error;
|
||||
EXPECT_EQ(
|
||||
render(
|
||||
QStringLiteral(
|
||||
"{% if file_exists(\"${PROJECT_DIR}/x.md\") %}yes{% else %}no{% endif %}"),
|
||||
Bindings{QString(), QString()},
|
||||
&error),
|
||||
QStringLiteral("no"));
|
||||
EXPECT_TRUE(error.isEmpty());
|
||||
}
|
||||
|
||||
TEST(ContextRendererTest, ReadFileWithUnresolvedProjectDirThrowsLoudly)
|
||||
{
|
||||
QString error;
|
||||
const QString out = render(
|
||||
QStringLiteral("{{ read_file(\"${PROJECT_DIR}/x.md\") }}"),
|
||||
Bindings{QString(), QString()},
|
||||
&error);
|
||||
EXPECT_TRUE(out.isEmpty());
|
||||
EXPECT_TRUE(error.contains(QStringLiteral("read_file")));
|
||||
}
|
||||
|
||||
TEST(ContextRendererTest, HeadLinesTakesLeadingLines)
|
||||
{
|
||||
QTemporaryDir proj;
|
||||
@@ -153,8 +177,7 @@ TEST(ContextRendererTest, HeadLinesTakesLeadingLines)
|
||||
TEST(ContextRendererTest, StringHelpers)
|
||||
{
|
||||
const Bindings none{};
|
||||
EXPECT_EQ(
|
||||
render(QStringLiteral("{{ basename(\"/a/b/c.txt\") }}"), none), QStringLiteral("c.txt"));
|
||||
EXPECT_EQ(render(QStringLiteral("{{ basename(\"/a/b/c.txt\") }}"), none), QStringLiteral("c.txt"));
|
||||
EXPECT_EQ(render(QStringLiteral("{{ ext(\"/a/b/c.txt\") }}"), none), QStringLiteral("txt"));
|
||||
EXPECT_EQ(render(QStringLiteral("{{ dirname(\"/a/b/c.txt\") }}"), none), QStringLiteral("/a/b"));
|
||||
EXPECT_EQ(render(QStringLiteral("{{ lower(\"ABC\") }}"), none), QStringLiteral("abc"));
|
||||
@@ -189,7 +212,8 @@ TEST(ContextRendererTest, SelectsCompletionRoleByLanguageFromQrc)
|
||||
|
||||
const QString tpl = QStringLiteral(
|
||||
"{%- if language == \"qml\" %}{{ read_file(\":/roles/code-completion-qml.md\") }}"
|
||||
"{%- else if language == \"c-like\" %}{{ read_file(\":/roles/code-completion-c-like.md\") }}"
|
||||
"{%- else if language == \"c-like\" %}{{ read_file(\":/roles/code-completion-c-like.md\") "
|
||||
"}}"
|
||||
"{%- else %}{{ read_file(\":/roles/code-completion.md\") }}"
|
||||
"{%- endif %}");
|
||||
|
||||
|
||||
@@ -370,24 +370,27 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
|
||||
(QodeAssist::Templates::ContextData{
|
||||
.prefix = "Line 0\nLine 1\nLin",
|
||||
.suffix = "e 2\nLine 3\nLine 4",
|
||||
.fileContext = "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
.fileContext
|
||||
= "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
|
||||
EXPECT_EQ(
|
||||
reader.prepareContext(2, 3, *createSettingsForLines(1, 1)),
|
||||
(QodeAssist::Templates::ContextData{
|
||||
.prefix = "Line 1\nLin",
|
||||
.suffix = "e 2\nLine 3",
|
||||
.fileContext = "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
.fileContext
|
||||
= "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
|
||||
EXPECT_EQ(
|
||||
reader.prepareContext(2, 3, *createSettingsForLines(2, 2)),
|
||||
(QodeAssist::Templates::ContextData{
|
||||
.prefix = "Line 0\nLine 1\nLin",
|
||||
.suffix = "e 2\nLine 3\nLine 4",
|
||||
.fileContext = "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
.fileContext
|
||||
= "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
}
|
||||
|
||||
#include "DocumentContextReaderTest.moc"
|
||||
|
||||
@@ -34,8 +34,7 @@ TEST(EnvBlockFormatterTest, FormatProjectEmptyEnv)
|
||||
|
||||
TEST(EnvBlockFormatterTest, FormatFileWithKnownMime)
|
||||
{
|
||||
const QString out
|
||||
= EnvBlockFormatter::formatFile({"/home/dev/myapp/main.cpp", "text/x-c++src"});
|
||||
const QString out = EnvBlockFormatter::formatFile({"/home/dev/myapp/main.cpp", "text/x-c++src"});
|
||||
|
||||
EXPECT_TRUE(out.startsWith("File information:"));
|
||||
EXPECT_TRUE(out.contains("Language:"));
|
||||
|
||||
@@ -10,7 +10,8 @@ using namespace QodeAssist;
|
||||
|
||||
TEST(ErrorInfoTest, MakeErrorPopulatesFields)
|
||||
{
|
||||
const ErrorInfo e = makeError(ErrorCategory::Tool, QStringLiteral("boom"), QStringLiteral("detail"));
|
||||
const ErrorInfo e
|
||||
= makeError(ErrorCategory::Tool, QStringLiteral("boom"), QStringLiteral("detail"));
|
||||
EXPECT_EQ(e.category, ErrorCategory::Tool);
|
||||
EXPECT_EQ(e.message, QStringLiteral("boom"));
|
||||
EXPECT_EQ(e.providerDetail, QStringLiteral("detail"));
|
||||
@@ -42,7 +43,8 @@ TEST(ErrorInfoTest, CategorizesForbiddenAsAuth)
|
||||
|
||||
TEST(ErrorInfoTest, CategorizesApiKeyAsAuth)
|
||||
{
|
||||
EXPECT_EQ(categorizeProviderError(QStringLiteral("invalid api key supplied")), ErrorCategory::Auth);
|
||||
EXPECT_EQ(
|
||||
categorizeProviderError(QStringLiteral("invalid api key supplied")), ErrorCategory::Auth);
|
||||
}
|
||||
|
||||
TEST(ErrorInfoTest, CategorizesAuthenticationAsAuth)
|
||||
|
||||
@@ -25,8 +25,8 @@ AgentConfig makeConfig(const QJsonObject &body)
|
||||
return cfg;
|
||||
}
|
||||
|
||||
const QString kUserMessages
|
||||
= QStringLiteral("[ { \"role\": \"user\", \"content\": {{ tojson(ctx.prefix) }} } ]");
|
||||
const QString kUserMessages = QStringLiteral(
|
||||
"[ { \"role\": \"user\", \"content\": {{ tojson(ctx.prefix) }} } ]");
|
||||
|
||||
const QString kSystemField = QStringLiteral(
|
||||
"{% if existsIn(ctx, \"system_prompt\") %}{{ tojson(ctx.system_prompt) }}{% endif %}");
|
||||
@@ -35,12 +35,13 @@ const QString kSystemField = QStringLiteral(
|
||||
|
||||
TEST(JsonPromptTemplateTest, RendersJinjaSplicesAndKeepsLiterals)
|
||||
{
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||
{"max_tokens", 128},
|
||||
{"temperature", 0.5},
|
||||
{"stream", true},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(
|
||||
QJsonObject{
|
||||
{"max_tokens", 128},
|
||||
{"temperature", 0.5},
|
||||
{"stream", true},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
@@ -55,16 +56,16 @@ TEST(JsonPromptTemplateTest, RendersJinjaSplicesAndKeepsLiterals)
|
||||
|
||||
const QJsonArray messages = request.value("messages").toArray();
|
||||
ASSERT_EQ(messages.size(), 1);
|
||||
EXPECT_EQ(
|
||||
messages.at(0).toObject().value("content").toString(), QStringLiteral("hello world"));
|
||||
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},
|
||||
}));
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(
|
||||
QJsonObject{
|
||||
{"system", kSystemField},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
@@ -79,10 +80,11 @@ TEST(JsonPromptTemplateTest, DropsKeyWhenJinjaRendersEmpty)
|
||||
|
||||
TEST(JsonPromptTemplateTest, RendersSystemPromptWhenPresent)
|
||||
{
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||
{"system", kSystemField},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(
|
||||
QJsonObject{
|
||||
{"system", kSystemField},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
@@ -92,16 +94,16 @@ TEST(JsonPromptTemplateTest, RendersSystemPromptWhenPresent)
|
||||
QJsonObject request;
|
||||
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||
|
||||
EXPECT_EQ(
|
||||
request.value("system").toString(), QStringLiteral("You are a helpful assistant."));
|
||||
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},
|
||||
}));
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(
|
||||
QJsonObject{
|
||||
{"thinking", QJsonObject{{"type", "adaptive"}, {"budget", 8192}}},
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
@@ -119,11 +121,45 @@ TEST(JsonPromptTemplateTest, RejectsBodyThatRendersInvalidJsonAtLoad)
|
||||
{
|
||||
QString error;
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(
|
||||
makeConfig(QJsonObject{
|
||||
{"messages", QStringLiteral("[ {{ tojson(ctx.prefix) }}")},
|
||||
}),
|
||||
makeConfig(
|
||||
QJsonObject{
|
||||
{"messages", QStringLiteral("[ {{ tojson(ctx.prefix) }}")},
|
||||
}),
|
||||
&error);
|
||||
|
||||
EXPECT_EQ(tmpl, nullptr);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
}
|
||||
|
||||
TEST(JsonPromptTemplateTest, RejectsHandQuotedInterpolationAtLoad)
|
||||
{
|
||||
QString error;
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(
|
||||
makeConfig(
|
||||
QJsonObject{
|
||||
{"messages",
|
||||
QStringLiteral("[ { \"role\": \"user\", \"content\": \"{{ ctx.prefix }}\" } ]")},
|
||||
}),
|
||||
&error);
|
||||
|
||||
EXPECT_EQ(tmpl, nullptr);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
}
|
||||
|
||||
TEST(JsonPromptTemplateTest, RoundTripsQuotesBackslashesAndNewlinesViaTojson)
|
||||
{
|
||||
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(
|
||||
QJsonObject{
|
||||
{"messages", kUserMessages},
|
||||
}));
|
||||
ASSERT_NE(tmpl, nullptr);
|
||||
|
||||
ContextData ctx;
|
||||
ctx.prefix = QStringLiteral("a \"quoted\" back\\slash\nnewline");
|
||||
|
||||
QJsonObject request;
|
||||
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||
const QJsonArray messages = request.value("messages").toArray();
|
||||
ASSERT_EQ(messages.size(), 1);
|
||||
EXPECT_EQ(messages.at(0).toObject().value("content").toString(), *ctx.prefix);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ 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;
|
||||
@@ -92,10 +89,11 @@ TEST(MessageSerializerTest, RedactedThinkingRoundtrip)
|
||||
TEST(MessageSerializerTest, ImageBase64Roundtrip)
|
||||
{
|
||||
Message m(Message::Role::User);
|
||||
m.appendBlock(std::make_unique<LLMQore::ImageContent>(
|
||||
QStringLiteral("ZGF0YQ=="),
|
||||
QStringLiteral("image/png"),
|
||||
LLMQore::ImageContent::ImageSourceType::Base64));
|
||||
m.appendBlock(
|
||||
std::make_unique<LLMQore::ImageContent>(
|
||||
QStringLiteral("ZGF0YQ=="),
|
||||
QStringLiteral("image/png"),
|
||||
LLMQore::ImageContent::ImageSourceType::Base64));
|
||||
|
||||
const QJsonObject block
|
||||
= MessageSerializer::toJson(m).value("blocks").toArray().first().toObject();
|
||||
@@ -109,10 +107,11 @@ TEST(MessageSerializerTest, ImageBase64Roundtrip)
|
||||
TEST(MessageSerializerTest, ImageUrlSourceTypeRoundtrip)
|
||||
{
|
||||
Message m(Message::Role::User);
|
||||
m.appendBlock(std::make_unique<LLMQore::ImageContent>(
|
||||
QStringLiteral("https://example.com/a.png"),
|
||||
QStringLiteral("image/png"),
|
||||
LLMQore::ImageContent::ImageSourceType::Url));
|
||||
m.appendBlock(
|
||||
std::make_unique<LLMQore::ImageContent>(
|
||||
QStringLiteral("https://example.com/a.png"),
|
||||
QStringLiteral("image/png"),
|
||||
LLMQore::ImageContent::ImageSourceType::Url));
|
||||
|
||||
const QJsonObject block
|
||||
= MessageSerializer::toJson(m).value("blocks").toArray().first().toObject();
|
||||
@@ -124,10 +123,9 @@ TEST(MessageSerializerTest, ImageUrlSourceTypeRoundtrip)
|
||||
TEST(MessageSerializerTest, ToolUseRoundtrip)
|
||||
{
|
||||
Message m(Message::Role::Assistant);
|
||||
m.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||
QStringLiteral("tu1"),
|
||||
QStringLiteral("read_file"),
|
||||
QJsonObject{{"path", "a.cpp"}}));
|
||||
m.appendBlock(
|
||||
std::make_unique<LLMQore::ToolUseContent>(
|
||||
QStringLiteral("tu1"), QStringLiteral("read_file"), QJsonObject{{"path", "a.cpp"}}));
|
||||
|
||||
const QJsonObject block
|
||||
= MessageSerializer::toJson(m).value("blocks").toArray().first().toObject();
|
||||
@@ -157,8 +155,9 @@ TEST(MessageSerializerTest, ToolResultRoundtrip)
|
||||
TEST(MessageSerializerTest, StoredImageRoundtrip)
|
||||
{
|
||||
Message m(Message::Role::User);
|
||||
m.appendBlock(std::make_unique<StoredImageContent>(
|
||||
QStringLiteral("shot.png"), QStringLiteral("stored/shot"), QStringLiteral("image/png")));
|
||||
m.appendBlock(
|
||||
std::make_unique<StoredImageContent>(
|
||||
QStringLiteral("shot.png"), QStringLiteral("stored/shot"), QStringLiteral("image/png")));
|
||||
|
||||
const QJsonObject block
|
||||
= MessageSerializer::toJson(m).value("blocks").toArray().first().toObject();
|
||||
@@ -173,8 +172,9 @@ TEST(MessageSerializerTest, StoredImageRoundtrip)
|
||||
TEST(MessageSerializerTest, StoredAttachmentRoundtrip)
|
||||
{
|
||||
Message m(Message::Role::User);
|
||||
m.appendBlock(std::make_unique<StoredAttachmentContent>(
|
||||
QStringLiteral("notes.txt"), QStringLiteral("stored/notes")));
|
||||
m.appendBlock(
|
||||
std::make_unique<StoredAttachmentContent>(
|
||||
QStringLiteral("notes.txt"), QStringLiteral("stored/notes")));
|
||||
|
||||
const QJsonObject block
|
||||
= MessageSerializer::toJson(m).value("blocks").toArray().first().toObject();
|
||||
@@ -188,8 +188,9 @@ TEST(MessageSerializerTest, StoredAttachmentRoundtrip)
|
||||
TEST(MessageSerializerTest, SkillInvocationRoundtrip)
|
||||
{
|
||||
Message m(Message::Role::User);
|
||||
m.appendBlock(std::make_unique<SkillInvocationContent>(
|
||||
QStringLiteral("review"), QStringLiteral("Review the code.")));
|
||||
m.appendBlock(
|
||||
std::make_unique<SkillInvocationContent>(
|
||||
QStringLiteral("review"), QStringLiteral("Review the code.")));
|
||||
|
||||
const QJsonObject block
|
||||
= MessageSerializer::toJson(m).value("blocks").toArray().first().toObject();
|
||||
@@ -203,13 +204,14 @@ TEST(MessageSerializerTest, SkillInvocationRoundtrip)
|
||||
TEST(MessageSerializerTest, FileEditRoundtripWithStatusAndMessage)
|
||||
{
|
||||
Message m(Message::Role::Assistant);
|
||||
m.appendBlock(std::make_unique<FileEditContent>(
|
||||
QStringLiteral("e1"),
|
||||
QStringLiteral("a.cpp"),
|
||||
QStringLiteral("old"),
|
||||
QStringLiteral("new"),
|
||||
FileEditContent::Status::Applied,
|
||||
QStringLiteral("done")));
|
||||
m.appendBlock(
|
||||
std::make_unique<FileEditContent>(
|
||||
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();
|
||||
@@ -227,11 +229,12 @@ TEST(MessageSerializerTest, FileEditRoundtripWithStatusAndMessage)
|
||||
TEST(MessageSerializerTest, FileEditOmitsEmptyStatusMessageAndDefaultsToPending)
|
||||
{
|
||||
Message m(Message::Role::Assistant);
|
||||
m.appendBlock(std::make_unique<FileEditContent>(
|
||||
QStringLiteral("e1"),
|
||||
QStringLiteral("a.cpp"),
|
||||
QStringLiteral("old"),
|
||||
QStringLiteral("new")));
|
||||
m.appendBlock(
|
||||
std::make_unique<FileEditContent>(
|
||||
QStringLiteral("e1"),
|
||||
QStringLiteral("a.cpp"),
|
||||
QStringLiteral("old"),
|
||||
QStringLiteral("new")));
|
||||
|
||||
const QJsonObject block
|
||||
= MessageSerializer::toJson(m).value("blocks").toArray().first().toObject();
|
||||
@@ -243,8 +246,9 @@ TEST(MessageSerializerTest, MultipleBlocksPreserveOrder)
|
||||
{
|
||||
Message m(Message::Role::Assistant);
|
||||
m.appendBlock(std::make_unique<LLMQore::TextContent>(QStringLiteral("calling")));
|
||||
m.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||
QStringLiteral("tu1"), QStringLiteral("read_file"), QJsonObject()));
|
||||
m.appendBlock(
|
||||
std::make_unique<LLMQore::ToolUseContent>(
|
||||
QStringLiteral("tu1"), QStringLiteral("read_file"), QJsonObject()));
|
||||
|
||||
const QJsonArray blocks = MessageSerializer::toJson(m).value("blocks").toArray();
|
||||
ASSERT_EQ(blocks.size(), 2);
|
||||
@@ -295,8 +299,7 @@ TEST(MessageSerializerTest, UnknownBlocksSkippedButKnownKept)
|
||||
QJsonObject json;
|
||||
json["role"] = QStringLiteral("assistant");
|
||||
json["blocks"] = QJsonArray{
|
||||
QJsonObject{{"type", "future_block"}},
|
||||
QJsonObject{{"type", "text"}, {"text", "kept"}}};
|
||||
QJsonObject{{"type", "future_block"}}, QJsonObject{{"type", "text"}, {"text", "kept"}}};
|
||||
|
||||
bool ok = false;
|
||||
const Message m = MessageSerializer::fromJson(json, &ok);
|
||||
|
||||
139
test/PipelinesConfigTest.cpp
Normal file
139
test/PipelinesConfigTest.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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 <QFile>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include <AgentLoader.hpp>
|
||||
#include <PipelinesConfig.hpp>
|
||||
|
||||
using QodeAssist::Agents::AgentLoader;
|
||||
using QodeAssist::Settings::PipelineRosters;
|
||||
using QodeAssist::Settings::PipelinesConfig;
|
||||
using QodeAssist::Settings::PipelinesLoadStatus;
|
||||
|
||||
namespace {
|
||||
|
||||
class PipelinesConfigTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
ASSERT_TRUE(m_dir.isValid());
|
||||
m_path = m_dir.filePath(QStringLiteral("pipelines.toml"));
|
||||
PipelinesConfig::setFilePathForTests(m_path);
|
||||
}
|
||||
|
||||
void TearDown() override { PipelinesConfig::setFilePathForTests(QString()); }
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
QString m_path;
|
||||
};
|
||||
|
||||
void expectEqualRosters(const PipelineRosters &a, const PipelineRosters &b)
|
||||
{
|
||||
EXPECT_EQ(a.codeCompletion, b.codeCompletion);
|
||||
EXPECT_EQ(a.chatAssistant, b.chatAssistant);
|
||||
EXPECT_EQ(a.chatCompression, b.chatCompression);
|
||||
EXPECT_EQ(a.quickRefactor, b.quickRefactor);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(PipelinesConfigTest, DefaultsFillEverySlot)
|
||||
{
|
||||
const PipelineRosters defs = PipelineRosters::defaults();
|
||||
EXPECT_FALSE(defs.codeCompletion.isEmpty());
|
||||
EXPECT_FALSE(defs.chatAssistant.isEmpty());
|
||||
EXPECT_FALSE(defs.chatCompression.isEmpty());
|
||||
EXPECT_FALSE(defs.quickRefactor.isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(PipelinesConfigTest, DefaultsReferenceBundledAgents)
|
||||
{
|
||||
Q_INIT_RESOURCE(agents);
|
||||
|
||||
const AgentLoader::LoadResult result = AgentLoader::load(QStringLiteral(":/agents"), QString());
|
||||
ASSERT_FALSE(result.configs.empty());
|
||||
|
||||
QStringList bundledNames;
|
||||
for (const auto &cfg : result.configs)
|
||||
bundledNames.append(cfg.name);
|
||||
|
||||
const PipelineRosters defs = PipelineRosters::defaults();
|
||||
QStringList referenced = defs.codeCompletion + defs.chatAssistant;
|
||||
referenced << defs.chatCompression << defs.quickRefactor;
|
||||
for (const QString &name : std::as_const(referenced)) {
|
||||
EXPECT_TRUE(bundledNames.contains(name))
|
||||
<< "default roster references unknown agent '" << name.toStdString() << "'";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PipelinesConfigTest, MissingFileYieldsDefaults)
|
||||
{
|
||||
const auto result = PipelinesConfig::load();
|
||||
EXPECT_EQ(result.status, PipelinesLoadStatus::FileMissing);
|
||||
expectEqualRosters(result.rosters, PipelineRosters::defaults());
|
||||
}
|
||||
|
||||
TEST_F(PipelinesConfigTest, SaveLoadRoundTrip)
|
||||
{
|
||||
PipelineRosters rosters;
|
||||
rosters.codeCompletion = {QStringLiteral("Agent A"), QStringLiteral("Agent B")};
|
||||
rosters.chatAssistant = {QStringLiteral("Agent C")};
|
||||
rosters.chatCompression = QStringLiteral("Agent D");
|
||||
rosters.quickRefactor = QString();
|
||||
|
||||
ASSERT_TRUE(PipelinesConfig::save(rosters));
|
||||
|
||||
const auto result = PipelinesConfig::load();
|
||||
EXPECT_EQ(result.status, PipelinesLoadStatus::Ok);
|
||||
expectEqualRosters(result.rosters, rosters);
|
||||
}
|
||||
|
||||
TEST_F(PipelinesConfigTest, ExplicitlyEmptySlotStaysEmpty)
|
||||
{
|
||||
PipelineRosters rosters;
|
||||
ASSERT_TRUE(PipelinesConfig::save(rosters));
|
||||
|
||||
const auto result = PipelinesConfig::load();
|
||||
EXPECT_EQ(result.status, PipelinesLoadStatus::Ok);
|
||||
EXPECT_TRUE(result.rosters.codeCompletion.isEmpty());
|
||||
EXPECT_TRUE(result.rosters.chatAssistant.isEmpty());
|
||||
EXPECT_TRUE(result.rosters.chatCompression.isEmpty());
|
||||
EXPECT_TRUE(result.rosters.quickRefactor.isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(PipelinesConfigTest, LegacyArrayCollapsesToFirstName)
|
||||
{
|
||||
QFile f(m_path);
|
||||
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||
f.write(
|
||||
"[pipelines]\n"
|
||||
"code_completion = [\"CC\"]\n"
|
||||
"chat_assistant = [\"CA\"]\n"
|
||||
"chat_compression = [\"First\", \"Second\"]\n"
|
||||
"quick_refactor = [\"Only\"]\n");
|
||||
f.close();
|
||||
|
||||
const auto result = PipelinesConfig::load();
|
||||
EXPECT_EQ(result.status, PipelinesLoadStatus::Ok);
|
||||
EXPECT_EQ(result.rosters.chatCompression, QStringLiteral("First"));
|
||||
EXPECT_EQ(result.rosters.quickRefactor, QStringLiteral("Only"));
|
||||
}
|
||||
|
||||
TEST_F(PipelinesConfigTest, SaveInvalidatesCache)
|
||||
{
|
||||
PipelineRosters first;
|
||||
first.chatCompression = QStringLiteral("Before");
|
||||
ASSERT_TRUE(PipelinesConfig::save(first));
|
||||
EXPECT_EQ(PipelinesConfig::loadCached().rosters.chatCompression, QStringLiteral("Before"));
|
||||
|
||||
PipelineRosters second;
|
||||
second.chatCompression = QStringLiteral("AfterX");
|
||||
ASSERT_TRUE(PipelinesConfig::save(second));
|
||||
EXPECT_EQ(PipelinesConfig::loadCached().rosters.chatCompression, QStringLiteral("AfterX"));
|
||||
}
|
||||
@@ -57,8 +57,36 @@ TEST(ResponseCleanerTest, TrimsLeadingAndTrailingNewlines)
|
||||
|
||||
TEST(ResponseCleanerTest, FencedCodeWithExplanationLineInsideIsExtractedVerbatim)
|
||||
{
|
||||
// The fence body is returned verbatim; explanation stripping only inspects
|
||||
// the first lines of the *extracted* code, which here is real code.
|
||||
const QString input = QStringLiteral("```python\nx = 1\ny = 2\n```");
|
||||
EXPECT_EQ(ResponseCleaner::clean(input), QStringLiteral("x = 1\ny = 2"));
|
||||
}
|
||||
|
||||
TEST(ResponseCleanerTest, FencedPythonDefLineIsPreserved)
|
||||
{
|
||||
const QString input = QStringLiteral("```python\ndef foo():\n return 1\n```");
|
||||
EXPECT_EQ(ResponseCleaner::clean(input), QStringLiteral("def foo():\n return 1"));
|
||||
}
|
||||
|
||||
TEST(ResponseCleanerTest, FencedAccessSpecifierLineIsPreserved)
|
||||
{
|
||||
const QString input = QStringLiteral("```cpp\npublic:\n void foo();\n```");
|
||||
EXPECT_EQ(ResponseCleaner::clean(input), QStringLiteral("public:\n void foo();"));
|
||||
}
|
||||
|
||||
TEST(ResponseCleanerTest, UnfencedCodeStartingWithColonLineIsPreserved)
|
||||
{
|
||||
const QString input = QStringLiteral("def foo():\n return 1");
|
||||
EXPECT_EQ(ResponseCleaner::clean(input), input);
|
||||
}
|
||||
|
||||
TEST(ResponseCleanerTest, UnfencedCaseLabelIsPreserved)
|
||||
{
|
||||
const QString input = QStringLiteral("case 1:\n break;");
|
||||
EXPECT_EQ(ResponseCleaner::clean(input), input);
|
||||
}
|
||||
|
||||
TEST(ResponseCleanerTest, UnfencedProseHeaderIsStripped)
|
||||
{
|
||||
const QString input = QStringLiteral("Refactored version:\nint x = 1;");
|
||||
EXPECT_EQ(ResponseCleaner::clean(input), QStringLiteral("int x = 1;"));
|
||||
}
|
||||
|
||||
@@ -52,8 +52,7 @@ public:
|
||||
void fireFailed(const QString &id, const QString &error) { emit requestFailed(id, error); }
|
||||
|
||||
protected:
|
||||
LLMQore::RequestID sendMessage(
|
||||
const QJsonObject &, const QString &, LLMQore::RequestMode) override
|
||||
LLMQore::RequestID sendMessage(const QJsonObject &, const QString &, LLMQore::RequestMode) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
@@ -157,3 +156,91 @@ TEST(ResponseRouterTest, IgnoresEventsForInactiveRequest)
|
||||
|
||||
EXPECT_TRUE(history.isEmpty());
|
||||
}
|
||||
|
||||
TEST(ResponseRouterTest, DistinctThinkingBlocksStayDistinctWithOwnSignatures)
|
||||
{
|
||||
FakeClient client;
|
||||
ConversationHistory history;
|
||||
ResponseRouter router(&client, &history);
|
||||
|
||||
const QString id = QStringLiteral("req-4");
|
||||
router.beginRequest(id);
|
||||
client.fireThinking(id, QStringLiteral("first"), QStringLiteral("sig1"));
|
||||
client.fireThinking(id, QStringLiteral("second"), QStringLiteral("sig2"));
|
||||
|
||||
ASSERT_EQ(history.size(), 1);
|
||||
const auto &blocks = history.messages()[0].blocks();
|
||||
ASSERT_EQ(blocks.size(), 2u);
|
||||
|
||||
const auto *first = dynamic_cast<const LLMQore::ThinkingContent *>(blocks[0].get());
|
||||
const auto *second = dynamic_cast<const LLMQore::ThinkingContent *>(blocks[1].get());
|
||||
ASSERT_NE(first, nullptr);
|
||||
ASSERT_NE(second, nullptr);
|
||||
EXPECT_EQ(first->thinking(), QStringLiteral("first"));
|
||||
EXPECT_EQ(first->signature(), QStringLiteral("sig1"));
|
||||
EXPECT_EQ(second->thinking(), QStringLiteral("second"));
|
||||
EXPECT_EQ(second->signature(), QStringLiteral("sig2"));
|
||||
}
|
||||
|
||||
TEST(ResponseRouterTest, SignatureOnlyEmissionBecomesRedactedThinking)
|
||||
{
|
||||
FakeClient client;
|
||||
ConversationHistory history;
|
||||
ResponseRouter router(&client, &history);
|
||||
|
||||
const QString id = QStringLiteral("req-5");
|
||||
router.beginRequest(id);
|
||||
client.fireThinking(id, QStringLiteral("visible"), QStringLiteral("sig1"));
|
||||
client.fireThinking(id, QString(), QStringLiteral("redacted-payload"));
|
||||
|
||||
ASSERT_EQ(history.size(), 1);
|
||||
const auto &blocks = history.messages()[0].blocks();
|
||||
ASSERT_EQ(blocks.size(), 2u);
|
||||
|
||||
const auto *visible = dynamic_cast<const LLMQore::ThinkingContent *>(blocks[0].get());
|
||||
const auto *redacted = dynamic_cast<const LLMQore::RedactedThinkingContent *>(blocks[1].get());
|
||||
ASSERT_NE(visible, nullptr);
|
||||
ASSERT_NE(redacted, nullptr);
|
||||
EXPECT_EQ(visible->signature(), QStringLiteral("sig1"));
|
||||
EXPECT_EQ(redacted->signature(), QStringLiteral("redacted-payload"));
|
||||
}
|
||||
|
||||
TEST(ResponseRouterTest, TextAfterToolUseStaysAfterToolUse)
|
||||
{
|
||||
FakeClient client;
|
||||
ConversationHistory history;
|
||||
ResponseRouter router(&client, &history);
|
||||
|
||||
const QString id = QStringLiteral("req-6");
|
||||
router.beginRequest(id);
|
||||
client.fireChunk(id, QStringLiteral("before"));
|
||||
client.fireToolStarted(id, QStringLiteral("t1"), QStringLiteral("tool"), QJsonObject{});
|
||||
client.fireChunk(id, QStringLiteral("after"));
|
||||
|
||||
ASSERT_EQ(history.size(), 1);
|
||||
const auto &blocks = history.messages()[0].blocks();
|
||||
ASSERT_EQ(blocks.size(), 3u);
|
||||
EXPECT_NE(dynamic_cast<const LLMQore::TextContent *>(blocks[0].get()), nullptr);
|
||||
EXPECT_NE(dynamic_cast<const LLMQore::ToolUseContent *>(blocks[1].get()), nullptr);
|
||||
const auto *after = dynamic_cast<const LLMQore::TextContent *>(blocks[2].get());
|
||||
ASSERT_NE(after, nullptr);
|
||||
EXPECT_EQ(after->text(), QStringLiteral("after"));
|
||||
}
|
||||
|
||||
TEST(ResponseRouterTest, BatchedToolResultsMergeIntoOneUserMessage)
|
||||
{
|
||||
FakeClient client;
|
||||
ConversationHistory history;
|
||||
ResponseRouter router(&client, &history);
|
||||
|
||||
const QString id = QStringLiteral("req-7");
|
||||
router.beginRequest(id);
|
||||
client.fireChunk(id, QStringLiteral("calling tools"));
|
||||
client.fireToolResult(id, QStringLiteral("t1"), QStringLiteral("tool_a"), QStringLiteral("r1"));
|
||||
client.fireToolResult(id, QStringLiteral("t2"), QStringLiteral("tool_b"), QStringLiteral("r2"));
|
||||
|
||||
ASSERT_EQ(history.size(), 2);
|
||||
const Message &results = history.messages()[1];
|
||||
EXPECT_EQ(results.role(), Message::Role::User);
|
||||
EXPECT_EQ(results.blocks().size(), 2u);
|
||||
}
|
||||
|
||||
@@ -49,8 +49,10 @@ TEST(SystemPromptBuilderTest, EqualPriorityKeepsInsertionOrder)
|
||||
TEST(SystemPromptBuilderTest, AgentPriorityComposesBeforeDefault)
|
||||
{
|
||||
SystemPromptBuilder builder;
|
||||
builder.setLayer(QStringLiteral("env"), QStringLiteral("ENV"), SystemPromptBuilder::kDefaultPriority);
|
||||
builder.setLayer(QStringLiteral("agent"), QStringLiteral("SYS"), SystemPromptBuilder::kAgentPriority);
|
||||
builder.setLayer(
|
||||
QStringLiteral("env"), QStringLiteral("ENV"), SystemPromptBuilder::kDefaultPriority);
|
||||
builder.setLayer(
|
||||
QStringLiteral("agent"), QStringLiteral("SYS"), SystemPromptBuilder::kAgentPriority);
|
||||
|
||||
EXPECT_EQ(builder.compose(), QStringLiteral("SYS\n\nENV"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user