mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-01 15:24:13 +02:00
ddc645174f
The configuration names "python" or "python3", or the Python the Windows installer can add. When none of those can run, the assistant fails with an error far from the cause, so the dialog now says so in bold, with what to do: tick "Python pour l'assistant IA" in the installer or install Python (Windows), or install Python 3 from the system's packages. Windows 10 and 11 put a python.exe in WindowsApps that only opens the Microsoft Store, so a PATH search alone reports Python where there is none. A result there is reported as possibly that shortcut. Backslashes are converted explicitly, not with QDir::fromNativeSeparators(), which leaves them alone off Windows and so could not be tested here. tst_aiassistantsetup: 17 cases (8 new: the bundled Python is taken without looking on PATH; none, python.org, the Store shortcut with either separator, and a Linux folder named WindowsApps). Each of the 4 new rules was removed in turn and the test failed. ctest 32/32. In the GUI, the same install layout shows the warning with no python3 on PATH and no warning with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
8.0 KiB
C++
197 lines
8.0 KiB
C++
/*
|
|
Copyright 2006-2026 The QElectroTech Team
|
|
This file is part of QElectroTech.
|
|
|
|
QElectroTech is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
QElectroTech is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "ui/aiassistantsetup.h"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QTemporaryDir>
|
|
#include <QtTest>
|
|
|
|
using AiAssistantSetup::Client;
|
|
|
|
class TestAiAssistantSetup : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
static void touch(const QString &path)
|
|
{
|
|
QDir().mkpath(QFileInfo(path).absolutePath());
|
|
QFile f(path);
|
|
QVERIFY(f.open(QIODevice::WriteOnly));
|
|
}
|
|
|
|
static AiAssistantSetup::Paths windowsPaths()
|
|
{
|
|
AiAssistantSetup::Paths p;
|
|
p.server = QStringLiteral("C:\\Program Files\\QElectroTech\\mcp\\qet_mcp.py");
|
|
p.python = QStringLiteral("C:\\Program Files\\QElectroTech\\mcp\\python\\python.exe");
|
|
p.qet_binary = QStringLiteral("C:\\Program Files\\QElectroTech\\bin\\QElectroTech.exe");
|
|
return p;
|
|
}
|
|
|
|
static QJsonObject entry(Client client, const AiAssistantSetup::Paths &p, bool edit)
|
|
{
|
|
const QByteArray text = AiAssistantSetup::configuration(
|
|
client, p, QStringLiteral("D:\\Drawings"), edit).toUtf8();
|
|
QJsonParseError error;
|
|
const QJsonDocument doc = QJsonDocument::fromJson(text, &error);
|
|
if (error.error != QJsonParseError::NoError)
|
|
qWarning() << error.errorString() << text;
|
|
const QString key = client == Client::VsCode ? QStringLiteral("servers")
|
|
: QStringLiteral("mcpServers");
|
|
return doc.object().value(key).toObject().value(QStringLiteral("qet")).toObject();
|
|
}
|
|
|
|
private slots:
|
|
void windowsInstallIsFound()
|
|
{
|
|
QTemporaryDir root;
|
|
touch(root.filePath(QStringLiteral("bin/QElectroTech.exe")));
|
|
touch(root.filePath(QStringLiteral("mcp/qet_mcp.py")));
|
|
touch(root.filePath(QStringLiteral("mcp/python/python.exe")));
|
|
const auto p = AiAssistantSetup::detect(root.filePath(QStringLiteral("bin")),
|
|
root.filePath(QStringLiteral("bin/QElectroTech.exe")), true);
|
|
QCOMPARE(p.server, QDir::cleanPath(root.filePath(QStringLiteral("mcp/qet_mcp.py"))));
|
|
QCOMPARE(p.python, QDir::cleanPath(root.filePath(QStringLiteral("mcp/python/python.exe"))));
|
|
QCOMPARE(p.qet_binary, QDir::cleanPath(root.filePath(QStringLiteral("bin/QElectroTech.exe"))));
|
|
}
|
|
|
|
void windowsWithoutBundledPythonUsesPath()
|
|
{
|
|
QTemporaryDir root;
|
|
touch(root.filePath(QStringLiteral("mcp/qet_mcp.py")));
|
|
QDir().mkpath(root.filePath(QStringLiteral("bin")));
|
|
const auto p = AiAssistantSetup::detect(root.filePath(QStringLiteral("bin")),
|
|
root.filePath(QStringLiteral("bin/QElectroTech.exe")), true);
|
|
QCOMPARE(p.python, QStringLiteral("python"));
|
|
}
|
|
|
|
void unixInstallIsFound()
|
|
{
|
|
QTemporaryDir prefix;
|
|
touch(prefix.filePath(QStringLiteral("share/qelectrotech/mcp/qet_mcp.py")));
|
|
// A bundled-Python layout is only a Windows thing.
|
|
touch(prefix.filePath(QStringLiteral("mcp/python/python.exe")));
|
|
QDir().mkpath(prefix.filePath(QStringLiteral("bin")));
|
|
const auto p = AiAssistantSetup::detect(prefix.filePath(QStringLiteral("bin")),
|
|
prefix.filePath(QStringLiteral("bin/qelectrotech")), false);
|
|
QCOMPARE(p.server, QDir::cleanPath(prefix.filePath(QStringLiteral("share/qelectrotech/mcp/qet_mcp.py"))));
|
|
QCOMPARE(p.python, QStringLiteral("python3"));
|
|
}
|
|
|
|
void bundledPythonIsFoundWithoutLookingOnPath()
|
|
{
|
|
QTemporaryDir root;
|
|
touch(root.filePath(QStringLiteral("mcp/qet_mcp.py")));
|
|
touch(root.filePath(QStringLiteral("mcp/python/python.exe")));
|
|
QDir().mkpath(root.filePath(QStringLiteral("bin")));
|
|
bool looked = false;
|
|
const auto p = AiAssistantSetup::detect(root.filePath(QStringLiteral("bin")),
|
|
root.filePath(QStringLiteral("bin/QElectroTech.exe")), true,
|
|
[&looked](const QString &) { looked = true; return QString(); });
|
|
QCOMPARE(p.python_status, AiAssistantSetup::PythonStatus::Found);
|
|
QVERIFY(!looked);
|
|
}
|
|
|
|
void pythonStatusFromPath_data()
|
|
{
|
|
QTest::addColumn<bool>("windows");
|
|
QTest::addColumn<QString>("found");
|
|
QTest::addColumn<int>("status");
|
|
using S = AiAssistantSetup::PythonStatus;
|
|
QTest::newRow("windows, none") << true << QString() << int(S::Missing);
|
|
QTest::newRow("windows, python.org") << true
|
|
<< QStringLiteral("C:/Users/me/AppData/Local/Programs/Python/Python314/python.exe") << int(S::Found);
|
|
QTest::newRow("windows, Store shortcut") << true
|
|
<< QStringLiteral("C:/Users/me/AppData/Local/Microsoft/WindowsApps/python.exe") << int(S::StoreShortcut);
|
|
QTest::newRow("windows, Store shortcut, backslashes") << true
|
|
<< QStringLiteral("C:\\Users\\me\\AppData\\Local\\Microsoft\\WINDOWSAPPS\\python.exe") << int(S::StoreShortcut);
|
|
QTest::newRow("linux, none") << false << QString() << int(S::Missing);
|
|
QTest::newRow("linux, python3") << false << QStringLiteral("/usr/bin/python3") << int(S::Found);
|
|
QTest::newRow("linux, a folder named WindowsApps") << false
|
|
<< QStringLiteral("/opt/WindowsApps/python3") << int(S::Found);
|
|
}
|
|
|
|
void pythonStatusFromPath()
|
|
{
|
|
QFETCH(bool, windows);
|
|
QFETCH(QString, found);
|
|
QFETCH(int, status);
|
|
QTemporaryDir build;
|
|
QString asked;
|
|
const auto p = AiAssistantSetup::detect(build.path(),
|
|
build.filePath(QStringLiteral("qelectrotech")), windows,
|
|
[&](const QString &name) { asked = name; return found; });
|
|
QCOMPARE(int(p.python_status), status);
|
|
QCOMPARE(asked, windows ? QStringLiteral("python") : QStringLiteral("python3"));
|
|
}
|
|
|
|
void noServerIsReportedAsEmpty()
|
|
{
|
|
QTemporaryDir build;
|
|
const auto p = AiAssistantSetup::detect(build.path(),
|
|
build.filePath(QStringLiteral("qelectrotech")), false);
|
|
QVERIFY(p.server.isEmpty());
|
|
}
|
|
|
|
void everyJsonClientGetsValidJson()
|
|
{
|
|
for (Client c : AiAssistantSetup::clients()) {
|
|
if (c == Client::CodexCli)
|
|
continue;
|
|
const QJsonObject e = entry(c, windowsPaths(), false);
|
|
QVERIFY2(!e.isEmpty(), qPrintable(QString::number(int(c))));
|
|
// Backslashes survive: parsing the text gives the path back.
|
|
QCOMPARE(e.value(QStringLiteral("args")).toArray().at(0).toString(), windowsPaths().server);
|
|
QCOMPARE(e.value(QStringLiteral("command")).toString(), windowsPaths().python);
|
|
const QJsonObject env = e.value(QStringLiteral("env")).toObject();
|
|
QCOMPARE(env.value(QStringLiteral("QET_MCP_WORKSPACE")).toString(), QStringLiteral("D:\\Drawings"));
|
|
QCOMPARE(env.value(QStringLiteral("QET_BINARY")).toString(), windowsPaths().qet_binary);
|
|
const bool typed = c == Client::VsCode || c == Client::Cursor;
|
|
QCOMPARE(e.contains(QStringLiteral("type")), typed);
|
|
}
|
|
}
|
|
|
|
void editingIsOffUnlessAllowed()
|
|
{
|
|
for (Client c : AiAssistantSetup::clients()) {
|
|
const QString off = AiAssistantSetup::configuration(c, windowsPaths(), QStringLiteral("D:\\x"), false);
|
|
const QString on = AiAssistantSetup::configuration(c, windowsPaths(), QStringLiteral("D:\\x"), true);
|
|
QVERIFY(!off.contains(QStringLiteral("QET_ENABLE_SCRIPTING")));
|
|
QVERIFY(on.contains(QStringLiteral("QET_ENABLE_SCRIPTING")));
|
|
}
|
|
}
|
|
|
|
void codexGetsEscapedToml()
|
|
{
|
|
const QString t = AiAssistantSetup::configuration(
|
|
Client::CodexCli, windowsPaths(), QStringLiteral("D:\\Drawings"), true);
|
|
QVERIFY(t.startsWith(QStringLiteral("[mcp_servers.qet]\n")));
|
|
QVERIFY(t.contains(QStringLiteral("args = [\"C:\\\\Program Files\\\\QElectroTech\\\\mcp\\\\qet_mcp.py\"]")));
|
|
QVERIFY(t.contains(QStringLiteral("[mcp_servers.qet.env]\n")));
|
|
QVERIFY(t.contains(QStringLiteral("QET_MCP_WORKSPACE = \"D:\\\\Drawings\"")));
|
|
QVERIFY(t.contains(QStringLiteral("QET_ENABLE_SCRIPTING = \"1\"")));
|
|
}
|
|
};
|
|
|
|
QTEST_GUILESS_MAIN(TestAiAssistantSetup)
|
|
#include "tst_aiassistantsetup.moc"
|