mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-23 19:51:05 -04:00
177 lines
5.9 KiB
C++
177 lines
5.9 KiB
C++
// Copyright (C) 2026 Petr Mironychev
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
|
|
#include "ToolsManagerGateTest.hpp"
|
|
|
|
#include <QJsonObject>
|
|
#include <QSignalSpy>
|
|
#include <QTest>
|
|
|
|
#include <LLMQore/BaseTool.hpp>
|
|
#include <LLMQore/ToolsManager.hpp>
|
|
|
|
#include "tools/BuildProjectTool.hpp"
|
|
#include "tools/CreateNewFileTool.hpp"
|
|
#include "tools/EditFileTool.hpp"
|
|
#include "tools/EditorStateTools.hpp"
|
|
#include "tools/ExecuteTerminalCommandTool.hpp"
|
|
#include "tools/GetIssuesListTool.hpp"
|
|
#include "tools/ReadFileTool.hpp"
|
|
|
|
namespace QodeAssist {
|
|
|
|
namespace {
|
|
|
|
class FakeGatedTool : public ::LLMQore::BaseTool
|
|
{
|
|
public:
|
|
FakeGatedTool(QString toolId, ::LLMQore::ToolSafety safety, QObject *parent = nullptr)
|
|
: ::LLMQore::BaseTool(parent)
|
|
, m_id(std::move(toolId))
|
|
, m_safety(safety)
|
|
{}
|
|
|
|
QString id() const override { return m_id; }
|
|
QString displayName() const override { return m_id; }
|
|
QString description() const override { return m_id; }
|
|
QJsonObject parametersSchema() const override { return {}; }
|
|
::LLMQore::ToolSafety safety() const override { return m_safety; }
|
|
|
|
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &) override
|
|
{
|
|
++executions;
|
|
QPromise<LLMQore::ToolResult> promise;
|
|
promise.start();
|
|
promise.addResult(LLMQore::ToolResult::text(QStringLiteral("ran ") + m_id));
|
|
promise.finish();
|
|
return promise.future();
|
|
}
|
|
|
|
int executions = 0;
|
|
|
|
private:
|
|
QString m_id;
|
|
::LLMQore::ToolSafety m_safety;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
void ToolsManagerGateTest::testToolSafetyDefaultsToMutating()
|
|
{
|
|
FakeGatedTool undeclared("undeclared", ::LLMQore::ToolSafety::Mutating);
|
|
QCOMPARE(undeclared.safety(), ::LLMQore::ToolSafety::Mutating);
|
|
|
|
QCOMPARE(Tools::ReadFileTool().safety(), ::LLMQore::ToolSafety::ReadOnly);
|
|
QCOMPARE(Tools::GetIssuesListTool().safety(), ::LLMQore::ToolSafety::ReadOnly);
|
|
QCOMPARE(Tools::ListOpenEditorsTool().safety(), ::LLMQore::ToolSafety::ReadOnly);
|
|
|
|
QCOMPARE(Tools::EditFileTool().safety(), ::LLMQore::ToolSafety::Mutating);
|
|
QCOMPARE(Tools::CreateNewFileTool().safety(), ::LLMQore::ToolSafety::Mutating);
|
|
QCOMPARE(Tools::ExecuteTerminalCommandTool().safety(), ::LLMQore::ToolSafety::Mutating);
|
|
QCOMPARE(Tools::BuildProjectTool().safety(), ::LLMQore::ToolSafety::Mutating);
|
|
}
|
|
|
|
void ToolsManagerGateTest::testToolsManagerWithoutAGateRunsImmediately()
|
|
{
|
|
::LLMQore::ToolsManager manager(::LLMQore::ToolSchemaFormat::OpenAI);
|
|
auto *tool = new FakeGatedTool("edit_thing", ::LLMQore::ToolSafety::Mutating, &manager);
|
|
manager.addTool(tool);
|
|
|
|
manager.executeToolCall("req-1", "call-1", "edit_thing", QJsonObject{});
|
|
|
|
QTRY_COMPARE(tool->executions, 1);
|
|
}
|
|
|
|
void ToolsManagerGateTest::testToolsManagerGateCanDeclineAToolCall()
|
|
{
|
|
::LLMQore::ToolsManager manager(::LLMQore::ToolSchemaFormat::OpenAI);
|
|
auto *tool = new FakeGatedTool("edit_thing", ::LLMQore::ToolSafety::Mutating, &manager);
|
|
manager.addTool(tool);
|
|
|
|
QStringList gated;
|
|
manager.setExecutionGate(
|
|
[&gated](
|
|
const QString &, const QString &, const QString &toolName, const QJsonObject &) {
|
|
gated.append(toolName);
|
|
QPromise<bool> promise;
|
|
promise.start();
|
|
promise.addResult(false);
|
|
promise.finish();
|
|
return promise.future();
|
|
});
|
|
|
|
QString resultText;
|
|
QObject::connect(
|
|
&manager,
|
|
&::LLMQore::ToolsManager::toolExecutionResult,
|
|
&manager,
|
|
[&resultText](const QString &, const QString &, const QString &, const QString &result) {
|
|
resultText = result;
|
|
});
|
|
|
|
manager.executeToolCall("req-1", "call-1", "edit_thing", QJsonObject{});
|
|
|
|
QTRY_VERIFY(!resultText.isEmpty());
|
|
|
|
QCOMPARE(gated, QStringList{"edit_thing"});
|
|
QCOMPARE(tool->executions, 0);
|
|
QVERIFY2(resultText.contains("declined"), qPrintable(resultText));
|
|
}
|
|
|
|
void ToolsManagerGateTest::testToolsManagerGateCanAllowAToolCall()
|
|
{
|
|
::LLMQore::ToolsManager manager(::LLMQore::ToolSchemaFormat::OpenAI);
|
|
auto *tool = new FakeGatedTool("edit_thing", ::LLMQore::ToolSafety::Mutating, &manager);
|
|
manager.addTool(tool);
|
|
|
|
manager.setExecutionGate(
|
|
[](const QString &, const QString &, const QString &, const QJsonObject &) {
|
|
QPromise<bool> promise;
|
|
promise.start();
|
|
promise.addResult(true);
|
|
promise.finish();
|
|
return promise.future();
|
|
});
|
|
|
|
manager.executeToolCall("req-1", "call-1", "edit_thing", QJsonObject{});
|
|
|
|
QTRY_COMPARE(tool->executions, 1);
|
|
}
|
|
|
|
void ToolsManagerGateTest::testToolsManagerGateResumesTheQueueAfterADenial()
|
|
{
|
|
::LLMQore::ToolsManager manager(::LLMQore::ToolSchemaFormat::OpenAI);
|
|
auto *denied = new FakeGatedTool("edit_thing", ::LLMQore::ToolSafety::Mutating, &manager);
|
|
auto *allowed = new FakeGatedTool("read_thing", ::LLMQore::ToolSafety::ReadOnly, &manager);
|
|
manager.addTool(denied);
|
|
manager.addTool(allowed);
|
|
|
|
manager.setExecutionGate(
|
|
[](const QString &, const QString &, const QString &toolName, const QJsonObject &) {
|
|
QPromise<bool> promise;
|
|
promise.start();
|
|
promise.addResult(toolName != QLatin1String("edit_thing"));
|
|
promise.finish();
|
|
return promise.future();
|
|
});
|
|
|
|
bool finished = false;
|
|
QObject::connect(
|
|
&manager,
|
|
&::LLMQore::ToolsManager::toolExecutionComplete,
|
|
&manager,
|
|
[&finished](const QString &, const QHash<QString, LLMQore::ToolResult> &) {
|
|
finished = true;
|
|
});
|
|
|
|
manager.executeToolCall("req-1", "call-1", "edit_thing", QJsonObject{});
|
|
manager.executeToolCall("req-1", "call-2", "read_thing", QJsonObject{});
|
|
|
|
QTRY_VERIFY(finished);
|
|
QCOMPARE(denied->executions, 0);
|
|
QCOMPARE(allowed->executions, 1);
|
|
}
|
|
|
|
} // namespace QodeAssist
|