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; });