Fügt CLI-Toolverwaltung und erweiterte UI-Funktionen hinzu
Ermöglicht die Verwaltung (Erstellen, Auflisten, Ausführen) von Tools über eine Befehlszeilenschnittstelle für Automatisierung und Integration. Die Benutzeroberfläche wurde mit dedizierten Ansichten für Toolerstellung und -bearbeitung, In-App-Lua-Hilfe sowie kontextsensitiven Menüs erweitert. Neue Abhängigkeiten (CLI11, rang) wurden integriert und eine umfassende Testsuite hinzugefügt. Die Lizenzierung wurde auf AGPL-3.0 umgestellt und ein Kontributionsleitfaden bereitgestellt.
This commit is contained in:
138
tests/luaRunnerTests.cpp
Normal file
138
tests/luaRunnerTests.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "luaRunner.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
|
||||
namespace {
|
||||
|
||||
bool runScript(LuaRunner& runner, const std::string& taskId, const std::filesystem::path& path) {
|
||||
std::promise<bool> completion;
|
||||
auto result = completion.get_future();
|
||||
const auto started = runner.run(taskId, path, 5, [&completion](bool success) {
|
||||
completion.set_value(success);
|
||||
});
|
||||
assert(started.started);
|
||||
assert(result.wait_for(std::chrono::seconds(10)) == std::future_status::ready);
|
||||
return result.get();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
const auto root = std::filesystem::temp_directory_path() /
|
||||
std::filesystem::u8path("toolbox-unicode-工具-" + std::to_string(stamp));
|
||||
std::filesystem::create_directories(root);
|
||||
|
||||
const auto validScript = root / std::filesystem::u8path("gültig.lua");
|
||||
const auto invalidScript = root / std::filesystem::u8path("fehler.lua");
|
||||
const auto longRunningScript = root / std::filesystem::u8path("langlaufend.lua");
|
||||
const auto singleInstanceScript = root / std::filesystem::u8path("einzelinstanz.lua");
|
||||
std::ofstream(validScript, std::ios::binary) << "local answer = 21 * 2\nassert(answer == 42)\n";
|
||||
std::ofstream(invalidScript, std::ios::binary) << "error('expected failure')\n";
|
||||
std::ofstream(longRunningScript, std::ios::binary) << "while true do end\n";
|
||||
std::ofstream(singleInstanceScript, std::ios::binary)
|
||||
<< "-- toolbox: single-instance\r\nwhile true do end\r\n";
|
||||
|
||||
LuaRunner runner;
|
||||
assert(runScript(runner, "valid-tool", validScript));
|
||||
assert(!runScript(runner, "invalid-tool", invalidScript));
|
||||
|
||||
std::promise<bool> longRunningCompletion;
|
||||
auto longRunningResult = longRunningCompletion.get_future();
|
||||
runner.run("C:/tools/first", longRunningScript, 0, [&longRunningCompletion](bool success) {
|
||||
longRunningCompletion.set_value(success);
|
||||
});
|
||||
assert(runScript(runner, "C:/tools/second", validScript));
|
||||
assert(longRunningResult.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout);
|
||||
runner.cancel("C:/tools/first");
|
||||
assert(longRunningResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
|
||||
assert(!longRunningResult.get());
|
||||
|
||||
std::promise<bool> singleCompletion;
|
||||
auto singleResult = singleCompletion.get_future();
|
||||
const auto idleSingleStatus = runner.status("C:/tools/single", singleInstanceScript);
|
||||
assert(idleSingleStatus.singleInstance);
|
||||
assert(!idleSingleStatus.running);
|
||||
const auto firstSingle = runner.run(
|
||||
"C:/tools/single", singleInstanceScript, 0,
|
||||
[&singleCompletion](bool success) { singleCompletion.set_value(success); });
|
||||
const auto duplicateSingle = runner.run(
|
||||
"C:/tools/single", singleInstanceScript, 0, [](bool) {});
|
||||
assert(firstSingle.started);
|
||||
assert(firstSingle.singleInstance);
|
||||
assert(!duplicateSingle.started);
|
||||
assert(duplicateSingle.singleInstance);
|
||||
const auto runningSingleStatus = runner.status("C:/tools/single", singleInstanceScript);
|
||||
assert(runningSingleStatus.singleInstance);
|
||||
assert(runningSingleStatus.running);
|
||||
runner.cancel("C:/tools/single");
|
||||
assert(singleResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
|
||||
assert(!singleResult.get());
|
||||
const auto completedSingleStatus = runner.status("C:/tools/single", singleInstanceScript);
|
||||
assert(completedSingleStatus.singleInstance);
|
||||
assert(!completedSingleStatus.running);
|
||||
|
||||
std::promise<bool> firstParallelCompletion;
|
||||
std::promise<bool> secondParallelCompletion;
|
||||
auto firstParallelResult = firstParallelCompletion.get_future();
|
||||
auto secondParallelResult = secondParallelCompletion.get_future();
|
||||
const auto firstParallel = runner.run(
|
||||
"C:/tools/parallel", longRunningScript, 0,
|
||||
[&firstParallelCompletion](bool success) { firstParallelCompletion.set_value(success); });
|
||||
const auto secondParallel = runner.run(
|
||||
"C:/tools/parallel", longRunningScript, 0,
|
||||
[&secondParallelCompletion](bool success) { secondParallelCompletion.set_value(success); });
|
||||
assert(firstParallel.started);
|
||||
assert(!firstParallel.singleInstance);
|
||||
assert(secondParallel.started);
|
||||
assert(!secondParallel.singleInstance);
|
||||
assert(firstParallelResult.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout);
|
||||
assert(secondParallelResult.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout);
|
||||
runner.cancel("C:/tools/parallel");
|
||||
assert(firstParallelResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
|
||||
assert(secondParallelResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
|
||||
assert(!firstParallelResult.get());
|
||||
assert(!secondParallelResult.get());
|
||||
|
||||
const auto quickSingleScript = root / std::filesystem::u8path("kurze-einzelinstanz.lua");
|
||||
std::ofstream(quickSingleScript, std::ios::binary)
|
||||
<< "-- toolbox: single-instance\r\nreturn true\r\n";
|
||||
std::promise<void> callbackEntered;
|
||||
auto callbackEnteredResult = callbackEntered.get_future();
|
||||
std::promise<void> releaseCallback;
|
||||
auto releaseCallbackResult = releaseCallback.get_future().share();
|
||||
std::promise<bool> quickSingleCompletion;
|
||||
auto quickSingleResult = quickSingleCompletion.get_future();
|
||||
const auto quickSingle = runner.run(
|
||||
"C:/tools/quick-single", quickSingleScript, 0,
|
||||
[&callbackEntered, releaseCallbackResult, &quickSingleCompletion](bool success) {
|
||||
callbackEntered.set_value();
|
||||
releaseCallbackResult.wait();
|
||||
quickSingleCompletion.set_value(success);
|
||||
});
|
||||
assert(quickSingle.started);
|
||||
assert(callbackEnteredResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
|
||||
const auto statusDuringCallback = runner.status("C:/tools/quick-single", quickSingleScript);
|
||||
auto duplicateDuringCallback = std::async(std::launch::async, [&runner, &quickSingleScript] {
|
||||
return runner.run("C:/tools/quick-single", quickSingleScript, 0, [](bool) {});
|
||||
});
|
||||
const auto duplicateWait = duplicateDuringCallback.wait_for(std::chrono::milliseconds(100));
|
||||
releaseCallback.set_value();
|
||||
assert(duplicateDuringCallback.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
|
||||
const auto duplicateDuringCallbackResult = duplicateDuringCallback.get();
|
||||
assert(statusDuringCallback.singleInstance);
|
||||
assert(!statusDuringCallback.running);
|
||||
assert(duplicateWait == std::future_status::ready);
|
||||
assert(!duplicateDuringCallbackResult.started);
|
||||
assert(duplicateDuringCallbackResult.singleInstance);
|
||||
assert(quickSingleResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
|
||||
assert(quickSingleResult.get());
|
||||
runner.shutdown();
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
return 0;
|
||||
}
|
||||
154
tests/toolRegistrySettingsTests.cpp
Normal file
154
tests/toolRegistrySettingsTests.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
#include "toolRegistry.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string readText(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const auto suffix = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
|
||||
const std::filesystem::path home = std::filesystem::temp_directory_path() /
|
||||
("toolbox-settings-contract-" + suffix);
|
||||
std::filesystem::create_directories(home);
|
||||
|
||||
ToolDraft created{
|
||||
.name = "Acme",
|
||||
.description = "Startet Acme.",
|
||||
.luaScript = "error('must be replaced')",
|
||||
.iconBase64 = {},
|
||||
.timeoutSeconds = 30,
|
||||
.templateType = "program",
|
||||
.templateTarget = R"(C:\Program Files\Acme\Acme.exe)",
|
||||
};
|
||||
const auto creation = ToolRegistry::createTool(home, created);
|
||||
assert(creation.success);
|
||||
|
||||
const auto oldPath = ToolRegistry::toolboxDirectory(home) / "Acme";
|
||||
const auto toolboxRoot = ToolRegistry::toolboxDirectory(home);
|
||||
const auto preparedStage = toolboxRoot / ".toolbox-update-101";
|
||||
const auto preparedJournal = toolboxRoot / ".toolbox-transaction-101.json";
|
||||
std::filesystem::copy(oldPath, preparedStage, std::filesystem::copy_options::recursive);
|
||||
std::ofstream(preparedJournal) <<
|
||||
R"({"version":1,"id":"101","originalName":"Acme","targetName":"Acme Website"})";
|
||||
ToolRegistry::loadTools(home);
|
||||
assert(std::filesystem::exists(oldPath));
|
||||
assert(!std::filesystem::exists(preparedStage));
|
||||
assert(!std::filesystem::exists(preparedJournal));
|
||||
|
||||
const auto movedStage = toolboxRoot / ".toolbox-update-102";
|
||||
const auto movedBackup = toolboxRoot / ".toolbox-backup-102";
|
||||
const auto movedJournal = toolboxRoot / ".toolbox-transaction-102.json";
|
||||
std::filesystem::copy(oldPath, movedStage, std::filesystem::copy_options::recursive);
|
||||
std::ofstream(movedJournal) <<
|
||||
R"({"version":1,"id":"102","originalName":"Acme","targetName":"Acme Website"})";
|
||||
std::filesystem::rename(oldPath, movedBackup);
|
||||
ToolRegistry::loadTools(home);
|
||||
assert(std::filesystem::exists(oldPath));
|
||||
assert(!std::filesystem::exists(movedStage));
|
||||
assert(!std::filesystem::exists(movedBackup));
|
||||
assert(!std::filesystem::exists(movedJournal));
|
||||
|
||||
const auto mismatchedBackup = toolboxRoot / ".toolbox-backup-104";
|
||||
const auto mismatchedJournal = toolboxRoot / ".toolbox-transaction-wrong.json";
|
||||
std::filesystem::copy(oldPath, mismatchedBackup, std::filesystem::copy_options::recursive);
|
||||
std::ofstream(mismatchedJournal) <<
|
||||
R"({"version":1,"id":"104","originalName":"Acme","targetName":"Acme Website"})";
|
||||
ToolRegistry::loadTools(home);
|
||||
assert(std::filesystem::exists(mismatchedBackup));
|
||||
assert(std::filesystem::exists(mismatchedJournal));
|
||||
|
||||
const auto outsideTool = home / "outside-tool";
|
||||
std::filesystem::create_directory(outsideTool);
|
||||
std::ofstream(outsideTool / "start.lua") << "print('outside')";
|
||||
assert(!ToolRegistry::getToolDetails(home, outsideTool).success);
|
||||
|
||||
const auto orphanPath = ToolRegistry::toolboxDirectory(home) / ".toolbox-update-orphan";
|
||||
std::filesystem::create_directory(orphanPath);
|
||||
std::ofstream(orphanPath / "start.lua") << "print('orphan')";
|
||||
const auto visibleTools = ToolRegistry::loadTools(home);
|
||||
assert(visibleTools.size() == 1);
|
||||
assert(visibleTools.front().name == "Acme");
|
||||
std::ofstream(oldPath / "asset.txt") << "preserve me";
|
||||
|
||||
const auto loaded = ToolRegistry::getToolDetails(home, oldPath);
|
||||
assert(loaded.success);
|
||||
assert(loaded.details.name == "Acme");
|
||||
assert(loaded.details.description == "Startet Acme.");
|
||||
assert(loaded.details.timeoutSeconds == 30);
|
||||
assert(loaded.details.templateType == "program");
|
||||
assert(loaded.details.templateTarget == R"(C:\Program Files\Acme\Acme.exe)");
|
||||
assert(loaded.details.luaScript.find("start") != std::string::npos);
|
||||
assert(loaded.details.luaScript.find("must be replaced") == std::string::npos);
|
||||
|
||||
ToolUpdateDraft update{
|
||||
.name = "Acme Website",
|
||||
.description = "Öffnet die Acme-Website.",
|
||||
.luaScript = "error('must also be replaced')",
|
||||
.timeoutSeconds = 45,
|
||||
.templateType = "website",
|
||||
.templateTarget = "https://www.example.com/docs",
|
||||
};
|
||||
const auto updated = ToolRegistry::updateTool(home, oldPath, update);
|
||||
assert(updated.success);
|
||||
|
||||
const auto newPath = ToolRegistry::toolboxDirectory(home) / "Acme Website";
|
||||
assert(updated.toolPath == newPath);
|
||||
assert(!std::filesystem::exists(oldPath));
|
||||
assert(readText(newPath / "asset.txt") == "preserve me");
|
||||
|
||||
const auto committedBackup = toolboxRoot / ".toolbox-backup-103";
|
||||
const auto committedJournal = toolboxRoot / ".toolbox-transaction-103.json";
|
||||
std::filesystem::copy(newPath, committedBackup, std::filesystem::copy_options::recursive);
|
||||
std::ofstream(committedJournal) <<
|
||||
R"({"version":1,"id":"103","originalName":"Acme","targetName":"Acme Website"})";
|
||||
ToolRegistry::loadTools(home);
|
||||
assert(std::filesystem::exists(newPath));
|
||||
assert(!std::filesystem::exists(committedBackup));
|
||||
assert(!std::filesystem::exists(committedJournal));
|
||||
|
||||
const auto reloaded = ToolRegistry::getToolDetails(home, newPath);
|
||||
assert(reloaded.success);
|
||||
assert(reloaded.details.name == "Acme Website");
|
||||
assert(reloaded.details.description == "Öffnet die Acme-Website.");
|
||||
assert(reloaded.details.timeoutSeconds == 45);
|
||||
assert(reloaded.details.templateType == "website");
|
||||
assert(reloaded.details.templateTarget == "https://www.example.com/docs");
|
||||
assert(reloaded.details.luaScript.find("https://www.example.com/docs") != std::string::npos);
|
||||
assert(reloaded.details.luaScript.find("must also be replaced") == std::string::npos);
|
||||
|
||||
std::ofstream(newPath / "tool.json") << R"({"version":1,"type":"bogus","target":"ignored"})";
|
||||
const auto invalidMetadata = ToolRegistry::getToolDetails(home, newPath);
|
||||
assert(invalidMetadata.success);
|
||||
assert(invalidMetadata.details.templateType == "lua");
|
||||
assert(invalidMetadata.details.templateTarget.empty());
|
||||
|
||||
std::filesystem::remove(newPath / "tool.json");
|
||||
const auto legacy = ToolRegistry::getToolDetails(home, newPath);
|
||||
assert(legacy.success);
|
||||
assert(legacy.details.templateType == "lua");
|
||||
assert(legacy.details.templateTarget.empty());
|
||||
|
||||
const auto invalid = ToolRegistry::updateTool(home, newPath, ToolUpdateDraft{
|
||||
.name = "../escape",
|
||||
.description = "invalid",
|
||||
.luaScript = "print('invalid')",
|
||||
.timeoutSeconds = 30,
|
||||
.templateType = "lua",
|
||||
});
|
||||
assert(!invalid.success);
|
||||
assert(std::filesystem::exists(newPath));
|
||||
assert(readText(newPath / "description.txt") == "Öffnet die Acme-Website.");
|
||||
|
||||
std::filesystem::remove_all(home);
|
||||
return 0;
|
||||
}
|
||||
42
tests/toolRuntime.test.js
Normal file
42
tests/toolRuntime.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert/strict')
|
||||
const runtime = require('../res/html/tool-runtime.js')
|
||||
|
||||
function fakeCard() {
|
||||
const classes = new Set()
|
||||
const status = { hidden: true }
|
||||
return {
|
||||
classList: {
|
||||
add: value => classes.add(value),
|
||||
remove: value => classes.delete(value),
|
||||
toggle: (value, enabled) => enabled ? classes.add(value) : classes.delete(value),
|
||||
contains: value => classes.has(value),
|
||||
},
|
||||
dataset: {},
|
||||
attributes: {},
|
||||
setAttribute(name, value) { this.attributes[name] = value },
|
||||
querySelector(selector) {
|
||||
return selector === '.tool-running-status' ? status : null
|
||||
},
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(runtime.initialRunning({ singleInstance: true, running: true }), true)
|
||||
assert.equal(runtime.initialRunning({ singleInstance: false, running: true }), false)
|
||||
|
||||
const card = fakeCard()
|
||||
runtime.applyCardState(card, true)
|
||||
assert.equal(runtime.isCardRunBlocked(card), true)
|
||||
assert.equal(card.classList.contains('tool-running'), true)
|
||||
assert.equal(card.dataset.runDisabled, 'true')
|
||||
assert.equal(card.attributes['aria-busy'], 'true')
|
||||
assert.equal(card.status.hidden, false)
|
||||
|
||||
runtime.applyCardState(card, false)
|
||||
assert.equal(runtime.isCardRunBlocked(card), false)
|
||||
assert.equal(card.classList.contains('tool-running'), false)
|
||||
assert.equal(card.dataset.runDisabled, 'false')
|
||||
assert.equal(card.attributes['aria-busy'], 'false')
|
||||
assert.equal(card.status.hidden, true)
|
||||
95
tests/toolTemplatesTests.cpp
Normal file
95
tests/toolTemplatesTests.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include "toolTemplates.h"
|
||||
|
||||
#include <sol/sol.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
namespace {
|
||||
|
||||
std::tuple<std::string, std::string> capturedLaunch(const std::string& script) {
|
||||
sol::state lua;
|
||||
lua.open_libraries(sol::lib::base, sol::lib::string);
|
||||
std::string operation;
|
||||
std::string target;
|
||||
auto toolbox = lua.create_named_table("toolbox");
|
||||
for (const std::string name : {"open_program", "open_folder", "open_url"}) {
|
||||
toolbox.set_function(name, [&, name](const std::string& value) {
|
||||
operation = name;
|
||||
target = value;
|
||||
return std::tuple{true, std::string{}};
|
||||
});
|
||||
}
|
||||
const sol::protected_function_result result = lua.safe_script(script, sol::script_pass_on_error);
|
||||
assert(result.valid());
|
||||
return {operation, target};
|
||||
}
|
||||
|
||||
std::string capturedPrint(const std::string& script) {
|
||||
sol::state lua;
|
||||
lua.open_libraries(sol::lib::base, sol::lib::string);
|
||||
std::string output;
|
||||
lua.set_function("print", [&output](const std::string& value) { output = value; });
|
||||
const sol::protected_function_result result = lua.safe_script(script, sol::script_pass_on_error);
|
||||
assert(result.valid());
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::string syntaxSensitiveName = "x'); error('injected\n\t\\\"";
|
||||
assert(capturedPrint(ToolTemplates::defaultLuaScript(syntaxSensitiveName)) == syntaxSensitiveName);
|
||||
|
||||
const std::filesystem::path programPath(R"(C:\Program Files\Acme\Acme.exe)");
|
||||
const auto program = ToolTemplates::programLauncher(programPath);
|
||||
assert(program.success);
|
||||
assert(program.draft.name == "Acme");
|
||||
assert(program.draft.description == "Startet Acme.");
|
||||
assert(program.draft.luaScript.find(R"(C:\\Program Files\\Acme\\Acme.exe)") != std::string::npos);
|
||||
assert(program.draft.luaScript.find("os.execute") == std::string::npos);
|
||||
assert((capturedLaunch(program.draft.luaScript) ==
|
||||
std::tuple{std::string("open_program"), program.draft.templateTarget}));
|
||||
|
||||
const std::filesystem::path folderPath(R"(C:\Users\User\Projects)");
|
||||
const auto folder = ToolTemplates::folderLauncher(folderPath);
|
||||
assert(folder.success);
|
||||
assert(folder.draft.name == "Projects öffnen");
|
||||
assert(folder.draft.description == "Öffnet den Ordner Projects.");
|
||||
assert(folder.draft.luaScript.find(R"(C:\\Users\\User\\Projects)") != std::string::npos);
|
||||
assert(folder.draft.luaScript.find("os.execute") == std::string::npos);
|
||||
assert((capturedLaunch(folder.draft.luaScript) ==
|
||||
std::tuple{std::string("open_folder"), folder.draft.templateTarget}));
|
||||
|
||||
const auto website = ToolTemplates::websiteLauncher("https://www.example.com/docs");
|
||||
assert(website.success);
|
||||
assert(website.draft.name == "example.com öffnen");
|
||||
assert(website.draft.description == "Öffnet https://www.example.com/docs.");
|
||||
assert(website.draft.luaScript.find("https://www.example.com/docs") != std::string::npos);
|
||||
assert(website.draft.luaScript.find("os.execute") == std::string::npos);
|
||||
assert((capturedLaunch(website.draft.luaScript) ==
|
||||
std::tuple{std::string("open_url"), std::string("https://www.example.com/docs")}));
|
||||
|
||||
const std::string shellSensitiveUrl = "https://example.com/%TEMP%?a=1&b=(two)";
|
||||
const auto shellSensitiveWebsite = ToolTemplates::websiteLauncher(shellSensitiveUrl);
|
||||
assert(shellSensitiveWebsite.success);
|
||||
assert((capturedLaunch(shellSensitiveWebsite.draft.luaScript) ==
|
||||
std::tuple{std::string("open_url"), shellSensitiveUrl}));
|
||||
|
||||
const auto invalidWebsite = ToolTemplates::websiteLauncher("javascript:alert(1)");
|
||||
assert(!invalidWebsite.success);
|
||||
|
||||
const auto invalidProgram = ToolTemplates::programLauncher(
|
||||
std::filesystem::path(R"(C:\Temp\notes.txt)"));
|
||||
assert(!invalidProgram.success);
|
||||
|
||||
const auto unicodeProgram = ToolTemplates::programLauncher(
|
||||
std::filesystem::path(LR"(C:\Programme\工具.exe)"));
|
||||
assert(unicodeProgram.success);
|
||||
assert(unicodeProgram.draft.name == "工具");
|
||||
assert(unicodeProgram.draft.luaScript.find("工具.exe") != std::string::npos);
|
||||
|
||||
return 0;
|
||||
}
|
||||
162
tests/toolbox_cli_test.py
Normal file
162
tests/toolbox_cli_test.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def run(cli: pathlib.Path, *args: str, expected_code: int = 0,
|
||||
env: dict[str, str] | None = None) -> dict:
|
||||
completed = subprocess.run(
|
||||
[str(cli), *args],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
)
|
||||
assert completed.returncode == expected_code, completed.stderr or completed.stdout
|
||||
if expected_code == 0:
|
||||
assert completed.stderr == "", completed.stderr
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def run_text(cli: pathlib.Path, *args: str) -> str:
|
||||
completed = subprocess.run(
|
||||
[str(cli), *args],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert completed.returncode == 0, completed.stderr or completed.stdout
|
||||
assert completed.stderr == "", completed.stderr
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cli = pathlib.Path(sys.argv[1])
|
||||
assert cli.is_file(), f"CLI binary missing: {cli}"
|
||||
|
||||
short_help = run_text(cli, "-h")
|
||||
long_help = run_text(cli, "--help")
|
||||
for help_text in (short_help, long_help):
|
||||
assert "TOOLBOX CLI" in help_text
|
||||
assert "BEFEHLE" in help_text
|
||||
assert "list" in help_text
|
||||
assert "create" in help_text
|
||||
assert "run" in help_text
|
||||
assert not help_text.lstrip().startswith("{")
|
||||
colored_help = run_text(cli, "--color", "always", "-h")
|
||||
assert "\x1b[" in colored_help
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="toolbox-cli-test-工具-") as home:
|
||||
created = run(
|
||||
cli,
|
||||
"--home", home,
|
||||
"create",
|
||||
"--name", "Agent 工具",
|
||||
"--description", "Created through the CLI",
|
||||
"--script", 'assert(type(toolbox.open_url) == "function")\nprint("agent-ready-✓")',
|
||||
"--timeout", "5",
|
||||
)
|
||||
assert created == {
|
||||
"message": "Tool wurde erstellt.",
|
||||
"ok": True,
|
||||
"tool": "Agent 工具",
|
||||
}
|
||||
|
||||
listed = run(cli, "--home", home, "list")
|
||||
assert listed["ok"] is True
|
||||
assert listed["tools"] == [{
|
||||
"description": "Created through the CLI",
|
||||
"name": "Agent 工具",
|
||||
}]
|
||||
listed_with_trailing_home = run(cli, "list", "--home", home)
|
||||
assert listed_with_trailing_home == listed
|
||||
forced_color_listed = run(cli, "--color", "always", "--home", home, "list")
|
||||
assert forced_color_listed == listed
|
||||
|
||||
executed = run(cli, "--home", home, "run", "Agent 工具")
|
||||
assert executed == {
|
||||
"ok": True,
|
||||
"output": "agent-ready-✓\n",
|
||||
"tool": "Agent 工具",
|
||||
}
|
||||
|
||||
script_file = pathlib.Path(home) / "skript-工具.lua"
|
||||
script_file.write_text('print("datei-✓")\n', encoding="utf-8")
|
||||
file_created = run(
|
||||
cli,
|
||||
"--home", home,
|
||||
"create",
|
||||
"--name", "Datei 工具",
|
||||
"--script-file", str(script_file),
|
||||
)
|
||||
assert file_created["ok"] is True
|
||||
file_executed = run(cli, "--home", home, "run", "Datei 工具")
|
||||
assert file_executed == {
|
||||
"ok": True,
|
||||
"output": "datei-✓\n",
|
||||
"tool": "Datei 工具",
|
||||
}
|
||||
|
||||
syntax_sensitive_name = "x'); error('injected"
|
||||
injected = run(
|
||||
cli,
|
||||
"--home", home,
|
||||
"create",
|
||||
"--name", syntax_sensitive_name,
|
||||
)
|
||||
assert injected["ok"] is True
|
||||
injected_run = run(cli, "--home", home, "run", syntax_sensitive_name)
|
||||
assert injected_run == {
|
||||
"ok": True,
|
||||
"output": f"Hallo aus {syntax_sensitive_name}\n",
|
||||
"tool": syntax_sensitive_name,
|
||||
}
|
||||
|
||||
default_home_env = os.environ.copy()
|
||||
default_home_env["USERPROFILE"] = home
|
||||
default_listed = run(cli, "list", env=default_home_env)
|
||||
assert {tool["name"] for tool in default_listed["tools"]} == {
|
||||
"Agent 工具", "Datei 工具", syntax_sensitive_name
|
||||
}
|
||||
|
||||
rejected = run(
|
||||
cli,
|
||||
"--home", home,
|
||||
"create",
|
||||
"--name", "../escape",
|
||||
expected_code=2,
|
||||
)
|
||||
assert rejected["ok"] is False
|
||||
assert not (pathlib.Path(home) / "escape").exists()
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="toolbox-cli-root-") as home, \
|
||||
tempfile.TemporaryDirectory(prefix="toolbox-cli-external-") as external:
|
||||
toolbox_root = pathlib.Path(home) / ".Toolbox"
|
||||
if sys.platform == "win32":
|
||||
linked = subprocess.run(
|
||||
["cmd.exe", "/c", "mklink", "/J", str(toolbox_root), external],
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
assert linked.returncode == 0
|
||||
else:
|
||||
toolbox_root.symlink_to(external, target_is_directory=True)
|
||||
rejected_root = run(
|
||||
cli,
|
||||
"--home", home,
|
||||
"create",
|
||||
"--name", "Must Not Escape",
|
||||
expected_code=2,
|
||||
)
|
||||
assert rejected_root["ok"] is False
|
||||
assert not (pathlib.Path(external) / "Must Not Escape").exists()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user