Lumi/plugins/lumi_ai/tests/verify-tools.js
2026-06-13 20:28:06 +02:00

304 lines
12 KiB
JavaScript

const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { ToolRegistry } = require("../backend/tool_router");
const { registerManagedTool } = require("../backend/tool_registry");
const { ToolRepoClient, CACHE_TTL_MS } = require("../backend/tool_repo_client");
const { ToolInstaller, validateToolDirectory } = require("../backend/tool_installer");
const { ToolLoader } = require("../backend/tool_loader");
const { ToolManager } = require("../backend/tool_manager");
const { isDestructivePath, issueConfirmation, consumeConfirmation } = require("../../../src/services/destructive-confirm");
async function run() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-ai-tools-"));
const pluginsDir = path.join(root, "plugins");
const stagingRoot = path.join(root, "manager-data", "staging");
const stateFile = path.join(root, "manager-data", "enabled.json");
const remoteDir = path.join(root, "remote");
fs.mkdirSync(pluginsDir, { recursive: true });
fs.mkdirSync(remoteDir, { recursive: true });
const remoteTools = new Map();
createTool(path.join(remoteDir, "lumi_ai_weather"), metadata("lumi_ai_weather", "1.0.0"), backendSource());
remoteTools.set("lumi_ai_weather", metadata("lumi_ai_weather", "1.0.0"));
const repoClient = {
calls: 0,
fail: false,
async discover() {
return {
repository: "https://git.example/owner/lumi",
branch: "main",
checked_at: new Date().toISOString(),
cached: false,
stale: false,
tools: [...remoteTools.values()]
};
},
async downloadTool(toolId, destination) {
this.calls += 1;
if (this.fail) {
fs.mkdirSync(destination, { recursive: true });
fs.writeFileSync(path.join(destination, "tool_info.json"), "{}");
return;
}
copyDirectory(path.join(remoteDir, toolId), destination);
},
async readReadme() {
return "# Weather\nRemote documentation.";
}
};
const registry = new ToolRegistry(() => {});
const installer = new ToolInstaller({ repoClient, pluginsDir, stagingRoot });
const settings = { getSetting: (key, fallback) => key === "platform_discord_enabled" ? true : fallback };
const loader = new ToolLoader({
registry,
installer,
settings,
stateFile,
lumiAiVersion: "0.7.0",
lumiVersion: "0.1.0"
});
const manager = new ToolManager({ repoClient, installer, loader });
let listing = await manager.list();
assert.equal(listing.tools.length, 1);
assert.equal(listing.tools[0].installed, false);
assert.equal(listing.tools[0].update_enabled, false);
const enabled = await manager.enable("lumi_ai_weather");
assert.equal(enabled.loaded, true);
assert.equal(registry.tools.has("lumi_ai_weather.lookup"), true);
listing = await manager.list();
assert.equal(listing.tools[0].installed, true);
assert.equal(listing.tools[0].enabled, true);
await manager.disable("lumi_ai_weather");
assert.equal(registry.tools.has("lumi_ai_weather.lookup"), false);
assert.equal(fs.existsSync(path.join(pluginsDir, "lumi_ai_weather")), true);
listing = await manager.list();
assert.equal(listing.tools[0].enabled, false);
fs.mkdirSync(path.join(pluginsDir, "lumi_ai_weather", "data"), { recursive: true });
fs.writeFileSync(path.join(pluginsDir, "lumi_ai_weather", "data", "local.json"), "preserve");
createTool(path.join(remoteDir, "lumi_ai_weather"), metadata("lumi_ai_weather", "1.1.0"), backendSource("updated"));
remoteTools.set("lumi_ai_weather", metadata("lumi_ai_weather", "1.1.0"));
listing = await manager.list();
assert.equal(listing.tools[0].update_available, true);
await manager.update("lumi_ai_weather");
assert.equal(validateToolDirectory(path.join(pluginsDir, "lumi_ai_weather")).version, "1.1.0");
assert.equal(fs.readFileSync(path.join(pluginsDir, "lumi_ai_weather", "data", "local.json"), "utf8"), "preserve");
repoClient.fail = true;
await assert.rejects(() => manager.update("lumi_ai_weather"), /required|missing/i);
assert.equal(validateToolDirectory(path.join(pluginsDir, "lumi_ai_weather")).version, "1.1.0");
repoClient.fail = false;
createTool(path.join(pluginsDir, "lumi_ai_local"), metadata("lumi_ai_local", "2.0.0"), backendSource());
listing = await manager.list();
const localOnly = listing.tools.find((tool) => tool.tool_id === "lumi_ai_local");
assert.equal(localOnly.remote_missing, true);
assert.equal(localOnly.update_enabled, false);
createTool(
path.join(pluginsDir, "lumi_ai_optional"),
{ ...metadata("lumi_ai_optional", "1.0.0"), dependencies: ["module-that-does-not-exist"] },
backendSource("ok", "lumi_ai_optional")
);
const optionalResult = await loader.enable("lumi_ai_optional");
assert.equal(optionalResult.loaded, true);
assert(optionalResult.dependencies.optional.some((entry) => entry.includes("module-that-does-not-exist")));
assert.match(loader.status("lumi_ai_optional").message, /limitations/);
createTool(
path.join(pluginsDir, "lumi_ai_cross_dependency"),
{ ...metadata("lumi_ai_cross_dependency", "1.0.0"), required_plugins: ["lumi_ai_weather"] },
backendSource("ok", "lumi_ai_cross_dependency")
);
const blockedDependency = await loader.enable("lumi_ai_cross_dependency");
assert.equal(blockedDependency.unavailable, true);
assert.match(blockedDependency.message, /cannot depend on AI tool plugin/);
createTool(
path.join(pluginsDir, "lumi_ai_partial"),
metadata("lumi_ai_partial", "1.0.0"),
`module.exports.register = ({ registerTool }) => {
registerTool(${serializeDefinition("lumi_ai_partial.first")});
registerTool(${serializeDefinition("outside_namespace.second")});
};\n`
);
const partialResult = await loader.enable("lumi_ai_partial");
assert.equal(partialResult.unavailable, true);
assert.equal(registry.tools.has("lumi_ai_partial.first"), false);
const strictRegistry = new ToolRegistry(() => {});
registerManagedTool(strictRegistry, {
...metadata("lumi_ai_strict", "1.0.0"),
permissions: { required_role: "admin" }
}, definition("lumi_ai_strict.lookup", "user"));
assert.throws(() => strictRegistry.prepare({
tool: "lumi_ai_strict.lookup",
args: {},
user: { id: "ordinary-user" },
role: "user",
sessionId: "session"
}), /Permission denied/);
assert.equal(strictRegistry.unregisterOwner("lumi_ai_strict"), 1);
const unrelated = path.join(pluginsDir, "ordinary-plugin");
fs.mkdirSync(unrelated);
assert.equal(isDestructivePath("/plugins/lumi_ai/tools/lumi_ai_weather/delete"), true);
const request = fakeRequest();
const confirmation = issueConfirmation(request, "/plugins/lumi_ai/tools/lumi_ai_weather/delete");
assert.equal(consumeConfirmation(request, "/plugins/lumi_ai/tools/lumi_ai_weather/delete", confirmation.token).reason, "too_early");
await manager.delete("lumi_ai_weather");
assert.equal(fs.existsSync(path.join(pluginsDir, "lumi_ai_weather")), false);
assert.equal(fs.existsSync(unrelated), true);
repoClient.fail = true;
remoteTools.set("lumi_ai_broken", metadata("lumi_ai_broken", "1.0.0"));
await assert.rejects(() => manager.enable("lumi_ai_broken"), /required|missing/i);
assert.equal(fs.existsSync(path.join(pluginsDir, "lumi_ai_broken")), false);
await verifyRemoteCache(root);
const settingsTemplate = fs.readFileSync(path.join(__dirname, "..", "views", "settings.ejs"), "utf8");
const modalTemplate = fs.readFileSync(path.join(__dirname, "..", "views", "tool-modal.ejs"), "utf8");
const clientScript = fs.readFileSync(path.join(__dirname, "..", "public", "tool-manager.js"), "utf8");
const pluginLoader = fs.readFileSync(path.join(__dirname, "..", "..", "..", "src", "services", "plugins.js"), "utf8");
assert(settingsTemplate.indexOf("data-ai-tools-open") < settingsTemplate.indexOf("Improvement Center"));
assert(modalTemplate.includes("data-ai-tools-list"));
assert(modalTemplate.includes("data-ai-tool-readme-modal"));
assert(clientScript.includes('button("Update"'));
assert(clientScript.includes("update.disabled = !tool.update_enabled"));
assert(clientScript.includes('form.dataset.confirmMode = "modal"'));
assert(pluginLoader.includes('entry.name, "tool_info.json"'));
fs.rmSync(root, { recursive: true, force: true });
console.log("Lumi AI tool manager verification passed.");
}
async function verifyRemoteCache(root) {
let now = Date.now();
let requests = 0;
const info = metadata("lumi_ai_remote", "1.2.3");
const fetch = async (url) => {
requests += 1;
const payload = url.includes("/contents/plugins/lumi_ai_remote/tool_info.json")
? { type: "file", encoding: "base64", content: Buffer.from(JSON.stringify(info)).toString("base64") }
: [{ type: "dir", name: "lumi_ai_remote", path: "plugins/lumi_ai_remote" }];
return { ok: true, status: 200, async json() { return payload; } };
};
const client = new ToolRepoClient({
settings: { getSetting: (key) => key === "git_remote" ? "https://git.example/owner/lumi" : "main" },
fetch,
cacheFile: path.join(root, "remote-cache.json"),
now: () => now
});
await client.discover();
assert.equal(requests, 2);
await client.discover();
assert.equal(requests, 2);
now += CACHE_TTL_MS + 1;
await client.discover();
assert.equal(requests, 4);
}
function metadata(toolId, version) {
return {
tool_id: toolId,
display_name: toolId.replaceAll("_", " "),
version,
description: "Test tool",
scope: "assistant",
permissions: { required_role: "user" },
capabilities: ["lookup"],
limitations: ["test only"],
tool_type: "lookup",
entrypoints: { backend: "index.js" },
confirmation_required: false
};
}
function definition(toolId, requiredRole = "user") {
return {
tool_id: toolId,
display_name: "Lookup",
description: "Runs a safe lookup.",
required_role: requiredRole,
required_permission: `${toolId}.use`,
audit_category: "lookup",
confirmation_required: false,
risk_level: "low",
schema: {},
permission_check: () => true,
workflow_handler: async () => ({ ok: true })
};
}
function backendSource(label = "ok", owner = "lumi_ai_weather") {
return `module.exports.register = ({ registerTool }) => {
registerTool({
tool_id: "${owner}.lookup",
display_name: "Weather lookup",
description: "Returns test weather.",
required_role: "user",
required_permission: "weather.lookup",
audit_category: "lookup",
confirmation_required: false,
risk_level: "low",
schema: {},
permission_check: () => true,
workflow_handler: async () => ({ value: ${JSON.stringify(label)} })
});
};\n`;
}
function serializeDefinition(toolId) {
return `{
tool_id: ${JSON.stringify(toolId)},
display_name: "Lookup",
description: "Safe lookup",
required_role: "user",
required_permission: "lookup.use",
audit_category: "lookup",
confirmation_required: false,
risk_level: "low",
schema: {},
permission_check: () => true,
workflow_handler: async () => ({ ok: true })
}`;
}
function createTool(directory, info, source) {
fs.rmSync(directory, { recursive: true, force: true });
fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(path.join(directory, "tool_info.json"), `${JSON.stringify(info, null, 2)}\n`);
fs.writeFileSync(path.join(directory, "readme.md"), `# ${info.display_name}\n`);
fs.writeFileSync(path.join(directory, "index.js"), source);
}
function copyDirectory(source, destination) {
fs.mkdirSync(destination, { recursive: true });
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
const from = path.join(source, entry.name);
const to = path.join(destination, entry.name);
if (entry.isDirectory()) copyDirectory(from, to);
else fs.copyFileSync(from, to);
}
}
function fakeRequest() {
return {
session: { user: { id: "admin", isAdmin: true } },
body: {},
get() { return null; }
};
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});