mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-23 19:51:05 -04:00
351 lines
14 KiB
C++
351 lines
14 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 "AgentCatalogTest.hpp"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QSet>
|
|
#include <QSignalSpy>
|
|
#include <QTemporaryDir>
|
|
#include <QTest>
|
|
|
|
#include "ChatView/ChatConfigurationController.hpp"
|
|
#include "acp/AgentCatalog.hpp"
|
|
#include "acp/AgentCatalogStore.hpp"
|
|
#include "acp/AgentDefinition.hpp"
|
|
#include "acp/AgentLaunch.hpp"
|
|
#include "acp/AgentRegistryParser.hpp"
|
|
#include "acp/AgentSpawn.hpp"
|
|
|
|
namespace QodeAssist {
|
|
|
|
void AgentCatalogTest::testAgentRegistryParsesEveryDistributionKind()
|
|
{
|
|
const QByteArray payload = R"({"version":"1.0.0","agents":[
|
|
{"id":"npx-agent","name":"Npx Agent","version":"1.2.3","description":"An agent",
|
|
"authors":["Ada","Grace"],"license":"MIT","repository":"https://example.com/repo",
|
|
"website":"https://example.com","icon":"https://example.com/icon.svg",
|
|
"distribution":{"npx":{"package":"pkg@1.2.3","args":["--acp"],"env":{"KEY":"VALUE"}}}},
|
|
{"id":"uvx-agent","name":"Uvx Agent",
|
|
"distribution":{"uvx":{"package":"pypkg==1.0","args":["-x"]}}},
|
|
{"id":"binary-agent","name":"Binary Agent","distribution":{"binary":{
|
|
"darwin-aarch64":{"archive":"https://example.com/a.tar.gz","sha256":"abc",
|
|
"cmd":"./dist/agent","args":["acp"]}}}},
|
|
{"id":"command-agent","name":"Command Agent",
|
|
"distribution":{"command":{"cmd":"/usr/local/bin/agent","args":["acp"]}}}
|
|
]})";
|
|
|
|
const Acp::AgentParseResult result = Acp::AgentRegistryParser::parse(
|
|
payload, Acp::AgentSource::LiveRegistry, "registry");
|
|
|
|
QVERIFY(result.warnings.isEmpty());
|
|
QCOMPARE(result.agents.size(), 4);
|
|
|
|
const Acp::AgentDefinition &npx = result.agents.at(0);
|
|
QCOMPARE(npx.id, QString("npx-agent"));
|
|
QCOMPARE(npx.name, QString("Npx Agent"));
|
|
QCOMPARE(npx.version, QString("1.2.3"));
|
|
QCOMPARE(npx.description, QString("An agent"));
|
|
QCOMPARE(npx.authors, (QStringList{"Ada", "Grace"}));
|
|
QCOMPARE(npx.license, QString("MIT"));
|
|
QCOMPARE(npx.repository, QString("https://example.com/repo"));
|
|
QCOMPARE(npx.source, Acp::AgentSource::LiveRegistry);
|
|
QCOMPARE(npx.origin, QString("registry"));
|
|
QCOMPARE(npx.distribution.kind, Acp::AgentDistributionKind::Npx);
|
|
QCOMPARE(npx.distribution.package, QString("pkg@1.2.3"));
|
|
QCOMPARE(npx.distribution.args, (QStringList{"--acp"}));
|
|
QCOMPARE(npx.distribution.env.size(), 1);
|
|
QCOMPARE(npx.distribution.env.at(0).name, QString("KEY"));
|
|
QCOMPARE(npx.distribution.env.at(0).value, QString("VALUE"));
|
|
QVERIFY(npx.isLaunchable());
|
|
|
|
const Acp::AgentDefinition &uvx = result.agents.at(1);
|
|
QCOMPARE(uvx.distribution.kind, Acp::AgentDistributionKind::Uvx);
|
|
QCOMPARE(uvx.distribution.package, QString("pypkg==1.0"));
|
|
QVERIFY(uvx.isLaunchable());
|
|
|
|
const Acp::AgentDefinition &binary = result.agents.at(2);
|
|
QCOMPARE(binary.distribution.kind, Acp::AgentDistributionKind::Binary);
|
|
QCOMPARE(binary.distribution.binaryTargets.size(), 1);
|
|
QCOMPARE(binary.distribution.binaryTargets.at(0).platform, QString("darwin-aarch64"));
|
|
QCOMPARE(binary.distribution.binaryTargets.at(0).cmd, QString("./dist/agent"));
|
|
QCOMPARE(binary.distribution.binaryTargets.at(0).sha256, QString("abc"));
|
|
QVERIFY(!binary.isLaunchable());
|
|
|
|
const Acp::AgentDefinition &command = result.agents.at(3);
|
|
QCOMPARE(command.distribution.kind, Acp::AgentDistributionKind::Command);
|
|
QCOMPARE(command.distribution.command, QString("/usr/local/bin/agent"));
|
|
QVERIFY(command.isLaunchable());
|
|
}
|
|
|
|
void AgentCatalogTest::testAgentRegistryReportsUnusableEntries()
|
|
{
|
|
const QByteArray payload = R"({"agents":[
|
|
{"name":"No Id","distribution":{"npx":{"package":"p"}}},
|
|
{"id":"no-distribution","name":"No Distribution"},
|
|
{"id":"usable","name":"Usable","distribution":{"npx":{"package":"p"}}}
|
|
]})";
|
|
|
|
const Acp::AgentParseResult result = Acp::AgentRegistryParser::parse(
|
|
payload, Acp::AgentSource::LiveRegistry, "registry");
|
|
|
|
QCOMPARE(result.agents.size(), 2);
|
|
QCOMPARE(result.warnings.size(), 2);
|
|
QVERIFY(result.warnings.at(0).contains("entry 0"));
|
|
QVERIFY(result.warnings.at(1).contains("no-distribution"));
|
|
|
|
QCOMPARE(result.agents.at(0).id, QString("no-distribution"));
|
|
QVERIFY(!result.agents.at(0).isLaunchable());
|
|
QVERIFY(result.agents.at(1).isLaunchable());
|
|
}
|
|
|
|
void AgentCatalogTest::testAgentRegistryParsesSingleAgentUserFile()
|
|
{
|
|
const QByteArray payload
|
|
= R"({"id":"local","distribution":{"command":{"cmd":"/opt/local-agent"}}})";
|
|
|
|
const Acp::AgentParseResult result = Acp::AgentRegistryParser::parse(
|
|
payload, Acp::AgentSource::UserFile, "local.json");
|
|
|
|
QCOMPARE(result.agents.size(), 1);
|
|
QCOMPARE(result.agents.at(0).id, QString("local"));
|
|
QCOMPARE(result.agents.at(0).name, QString("local"));
|
|
QCOMPARE(result.agents.at(0).source, Acp::AgentSource::UserFile);
|
|
QVERIFY(result.agents.at(0).isLaunchable());
|
|
}
|
|
|
|
void AgentCatalogTest::testAgentCatalogAppliesMergePriority()
|
|
{
|
|
const QByteArray bundled = R"({"agents":[
|
|
{"id":"alpha","name":"Alpha Bundled","distribution":{"npx":{"package":"alpha@1"}}},
|
|
{"id":"beta","name":"Beta","distribution":{"npx":{"package":"beta@1"}}}
|
|
]})";
|
|
const QByteArray live = R"({"agents":[
|
|
{"id":"alpha","name":"Alpha Registry","distribution":{"npx":{"package":"alpha@2"}}},
|
|
{"id":"gamma","name":"Gamma","distribution":{"uvx":{"package":"gamma"}}}
|
|
]})";
|
|
const QByteArray user
|
|
= R"({"id":"alpha","name":"Alpha User","distribution":{"command":{"cmd":"/opt/alpha"}}})";
|
|
|
|
Acp::AgentCatalog catalog;
|
|
catalog.setLayer(
|
|
Acp::AgentSource::BundledSnapshot,
|
|
Acp::AgentRegistryParser::parse(bundled, Acp::AgentSource::BundledSnapshot, "bundled")
|
|
.agents);
|
|
catalog.setLayer(
|
|
Acp::AgentSource::LiveRegistry,
|
|
Acp::AgentRegistryParser::parse(live, Acp::AgentSource::LiveRegistry, "registry").agents);
|
|
catalog.setLayer(
|
|
Acp::AgentSource::UserFile,
|
|
Acp::AgentRegistryParser::parse(user, Acp::AgentSource::UserFile, "alpha.json").agents);
|
|
|
|
QCOMPARE(catalog.size(), 3);
|
|
|
|
QStringList names;
|
|
for (const Acp::AgentDefinition &agent : catalog.agents())
|
|
names.append(agent.name);
|
|
QCOMPARE(names, (QStringList{"Alpha User", "Beta", "Gamma"}));
|
|
|
|
auto alpha = catalog.agent("alpha");
|
|
QVERIFY(alpha.has_value());
|
|
QCOMPARE(alpha->source, Acp::AgentSource::UserFile);
|
|
QCOMPARE(alpha->distribution.kind, Acp::AgentDistributionKind::Command);
|
|
|
|
catalog.setLayer(Acp::AgentSource::UserFile, {});
|
|
alpha = catalog.agent("alpha");
|
|
QVERIFY(alpha.has_value());
|
|
QCOMPARE(alpha->name, QString("Alpha Registry"));
|
|
QCOMPARE(alpha->distribution.package, QString("alpha@2"));
|
|
|
|
catalog.setLayer(Acp::AgentSource::LiveRegistry, {});
|
|
alpha = catalog.agent("alpha");
|
|
QVERIFY(alpha.has_value());
|
|
QCOMPARE(alpha->name, QString("Alpha Bundled"));
|
|
}
|
|
|
|
void AgentCatalogTest::testAgentCatalogUserFileMakesBinaryAgentLaunchable()
|
|
{
|
|
const QByteArray live = R"({"agents":[{"id":"cursor","name":"Cursor",
|
|
"distribution":{"binary":{"darwin-aarch64":{"archive":"https://example.com/a.tar.gz",
|
|
"cmd":"./cursor-agent","args":["acp"]}}}}]})";
|
|
const QByteArray user = R"({"id":"cursor","name":"Cursor",
|
|
"distribution":{"command":{"cmd":"/usr/local/bin/cursor-agent","args":["acp"]}}})";
|
|
|
|
Acp::AgentCatalog catalog;
|
|
catalog.setLayer(
|
|
Acp::AgentSource::LiveRegistry,
|
|
Acp::AgentRegistryParser::parse(live, Acp::AgentSource::LiveRegistry, "registry").agents);
|
|
QVERIFY(catalog.launchableAgents().isEmpty());
|
|
|
|
catalog.setLayer(
|
|
Acp::AgentSource::UserFile,
|
|
Acp::AgentRegistryParser::parse(user, Acp::AgentSource::UserFile, "cursor.json").agents);
|
|
|
|
QCOMPARE(catalog.size(), 1);
|
|
QCOMPARE(catalog.launchableAgents().size(), 1);
|
|
QCOMPARE(catalog.agent("cursor")->distribution.command, QString("/usr/local/bin/cursor-agent"));
|
|
}
|
|
|
|
void AgentCatalogTest::testAgentLaunchConfigUsesRunnerConventions()
|
|
{
|
|
Acp::AgentDefinition npx;
|
|
npx.id = "npx-agent";
|
|
npx.distribution.kind = Acp::AgentDistributionKind::Npx;
|
|
npx.distribution.package = "pkg@1.0";
|
|
npx.distribution.args = QStringList{"--acp"};
|
|
npx.distribution.env = {{"KEY", "VALUE"}};
|
|
|
|
const auto npxConfig = Acp::agentLaunchConfig(npx, "/tmp/project");
|
|
QVERIFY(npxConfig.has_value());
|
|
QCOMPARE(npxConfig->cwd, QString("/tmp/project"));
|
|
QCOMPARE(npxConfig->command, QString("npx"));
|
|
QCOMPARE(npxConfig->args, (QStringList{"-y", "pkg@1.0", "--acp"}));
|
|
QCOMPARE(npxConfig->env.size(), 1);
|
|
QCOMPARE(npxConfig->env.at(0).name, QString("KEY"));
|
|
|
|
Acp::AgentDefinition uvx;
|
|
uvx.id = "uvx-agent";
|
|
uvx.distribution.kind = Acp::AgentDistributionKind::Uvx;
|
|
uvx.distribution.package = "pypkg==1.0";
|
|
uvx.distribution.args = QStringList{"-x"};
|
|
|
|
const auto uvxConfig = Acp::agentLaunchConfig(uvx, QString());
|
|
QVERIFY(uvxConfig.has_value());
|
|
QCOMPARE(uvxConfig->command, QString("uvx"));
|
|
QCOMPARE(uvxConfig->args, (QStringList{"pypkg==1.0", "-x"}));
|
|
|
|
Acp::AgentDefinition command;
|
|
command.id = "command-agent";
|
|
command.distribution.kind = Acp::AgentDistributionKind::Command;
|
|
command.distribution.command = "/opt/agent";
|
|
command.distribution.args = QStringList{"acp"};
|
|
|
|
const auto commandConfig = Acp::agentLaunchConfig(command, QString());
|
|
QVERIFY(commandConfig.has_value());
|
|
QCOMPARE(commandConfig->command, QString("/opt/agent"));
|
|
QCOMPARE(commandConfig->args, (QStringList{"acp"}));
|
|
|
|
Acp::AgentDefinition binary;
|
|
binary.id = "binary-agent";
|
|
binary.distribution.kind = Acp::AgentDistributionKind::Binary;
|
|
QVERIFY(!Acp::agentLaunchConfig(binary, QString()).has_value());
|
|
}
|
|
|
|
void AgentCatalogTest::testAgentSearchPathsSplitting()
|
|
{
|
|
const QString separator(QDir::listSeparator());
|
|
|
|
QCOMPARE(Acp::splitSearchPaths(QString()), QStringList());
|
|
QCOMPARE(
|
|
Acp::splitSearchPaths(" /opt/homebrew/bin " + separator + separator + "/usr/local/bin"),
|
|
(QStringList{"/opt/homebrew/bin", "/usr/local/bin"}));
|
|
}
|
|
|
|
void AgentCatalogTest::testAgentExtraSearchPathsReachTheChildEnvironment()
|
|
{
|
|
Acp::AgentDefinition agent;
|
|
agent.id = "npx-agent";
|
|
agent.distribution.kind = Acp::AgentDistributionKind::Npx;
|
|
agent.distribution.package = "pkg";
|
|
|
|
auto config = Acp::agentLaunchConfig(agent, QString());
|
|
QVERIFY(config.has_value());
|
|
|
|
Acp::applyExtraSearchPaths(*config, {});
|
|
QVERIFY(config->env.isEmpty());
|
|
QCOMPARE(config->command, QString("npx"));
|
|
|
|
Acp::applyExtraSearchPaths(*config, QStringList{"/no/such/qodeassist/dir"});
|
|
QCOMPARE(config->command, QString("npx"));
|
|
QCOMPARE(config->env.size(), 1);
|
|
QCOMPARE(config->env.at(0).name, QString("PATH"));
|
|
QVERIFY(config->env.at(0).value.startsWith("/no/such/qodeassist/dir"));
|
|
|
|
Acp::AgentDefinition withOwnPath;
|
|
withOwnPath.id = "own-path";
|
|
withOwnPath.distribution.kind = Acp::AgentDistributionKind::Npx;
|
|
withOwnPath.distribution.package = "pkg";
|
|
withOwnPath.distribution.env = {{"PATH", "/agent/own"}};
|
|
|
|
auto ownPathConfig = Acp::agentLaunchConfig(withOwnPath, QString());
|
|
QVERIFY(ownPathConfig.has_value());
|
|
Acp::applyExtraSearchPaths(*ownPathConfig, QStringList{"/extra"});
|
|
|
|
QCOMPARE(ownPathConfig->env.size(), 2);
|
|
QCOMPARE(ownPathConfig->env.at(0).name, QString("PATH"));
|
|
QCOMPARE(ownPathConfig->env.at(1).value, QString("/agent/own"));
|
|
}
|
|
|
|
void AgentCatalogTest::testAgentForwardedVariablesReachTheChildEnvironment()
|
|
{
|
|
QCOMPARE(
|
|
Acp::splitVariableNames(" FOO, BAR\tBAZ "), (QStringList{"FOO", "BAR", "BAZ"}));
|
|
QCOMPARE(Acp::splitVariableNames(QString()), QStringList());
|
|
|
|
qputenv("QODEASSIST_TEST_TOKEN", "from-environment");
|
|
|
|
Acp::AgentDefinition agent;
|
|
agent.id = "npx-agent";
|
|
agent.distribution.kind = Acp::AgentDistributionKind::Npx;
|
|
agent.distribution.package = "pkg";
|
|
agent.distribution.env = {{"QODEASSIST_TEST_TOKEN", "from-definition"}};
|
|
|
|
auto config = Acp::agentLaunchConfig(agent, QString());
|
|
QVERIFY(config.has_value());
|
|
|
|
Acp::applyForwardedEnvironment(*config, {});
|
|
QCOMPARE(config->env.size(), 1);
|
|
|
|
Acp::applyForwardedEnvironment(*config, QStringList{"QODEASSIST_TEST_TOKEN"});
|
|
|
|
QCOMPARE(config->env.size(), 2);
|
|
QCOMPARE(config->env.at(0).name, QString("QODEASSIST_TEST_TOKEN"));
|
|
QCOMPARE(config->env.at(0).value, QString("from-environment"));
|
|
QCOMPARE(config->env.at(1).value, QString("from-definition"));
|
|
|
|
qunsetenv("QODEASSIST_TEST_TOKEN");
|
|
}
|
|
|
|
void AgentCatalogTest::testBundledAgentSnapshotParses()
|
|
{
|
|
QFile file(Acp::AgentCatalogStore::bundledSnapshotPath());
|
|
QVERIFY2(file.open(QIODevice::ReadOnly), qPrintable(file.fileName()));
|
|
|
|
const Acp::AgentParseResult result = Acp::AgentRegistryParser::parse(
|
|
file.readAll(), Acp::AgentSource::BundledSnapshot, "bundled snapshot");
|
|
|
|
QVERIFY(result.agents.size() > 10);
|
|
QVERIFY2(result.warnings.isEmpty(), qPrintable(result.warnings.join("; ")));
|
|
|
|
Acp::AgentCatalog catalog;
|
|
catalog.setLayer(Acp::AgentSource::BundledSnapshot, result.agents);
|
|
QVERIFY(!catalog.launchableAgents().isEmpty());
|
|
}
|
|
|
|
void AgentCatalogTest::testPickerObservesTheSharedAgentCatalog()
|
|
{
|
|
Acp::AgentCatalogStore store;
|
|
Chat::ChatConfigurationController controller;
|
|
|
|
QSignalSpy listChanged(
|
|
&controller, &Chat::ChatConfigurationController::availableConfigurationsChanged);
|
|
|
|
controller.setAgentCatalog(&store);
|
|
QVERIFY(listChanged.count() >= 1);
|
|
QVERIFY(!controller.availableConfigurations().isEmpty());
|
|
|
|
const int before = listChanged.count();
|
|
emit store.catalogChanged();
|
|
QCOMPARE(listChanged.count(), before + 1);
|
|
|
|
controller.setAgentCatalog(&store);
|
|
QCOMPARE(listChanged.count(), before + 1);
|
|
}
|
|
|
|
} // namespace QodeAssist
|