322 lines
10 KiB
JavaScript
322 lines
10 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { AiProvider } = require("../backend/ai_provider");
|
|
const { buildPrompt } = require("../backend/prompt_builder");
|
|
const { ToolInstaller } = require("../backend/tool_installer");
|
|
const { ToolLoader } = require("../backend/tool_loader");
|
|
const { ToolManager } = require("../backend/tool_manager");
|
|
const { ToolRegistry, parseToolCallResult } = require("../backend/tool_router");
|
|
|
|
async function run() {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-ai-loading-"));
|
|
try {
|
|
const pluginsDir = path.join(root, "plugins");
|
|
const stateFile = path.join(root, "data", "tools", "enabled.json");
|
|
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
fs.mkdirSync(path.join(pluginsDir, "lumi_ai"), { recursive: true });
|
|
createTool(pluginsDir, "lumi_ai_web_search", { readOnly: true });
|
|
createTool(pluginsDir, "lumi_ai_test_tool", { readOnly: true });
|
|
createInvalidTool(pluginsDir, "lumi_ai_invalid");
|
|
createMissingEntrypointTool(pluginsDir, "lumi_ai_missing_entrypoint");
|
|
|
|
const audit = [];
|
|
const registry = new ToolRegistry((entry) => audit.push(entry));
|
|
const installer = new ToolInstaller({
|
|
pluginsDir,
|
|
stagingRoot: path.join(root, "staging"),
|
|
repoClient: {}
|
|
});
|
|
const loader = new ToolLoader({
|
|
registry,
|
|
installer,
|
|
settings: { getSetting: (_key, fallback) => fallback },
|
|
stateFile,
|
|
lumiAiVersion: "0.8.0",
|
|
lumiVersion: "0.1.0"
|
|
});
|
|
const manager = new ToolManager({
|
|
loader,
|
|
installer,
|
|
settings: { describe: () => ({}) },
|
|
repoClient: {
|
|
async discover() {
|
|
return {
|
|
repository: "local",
|
|
branch: "main",
|
|
checked_at: new Date().toISOString(),
|
|
cached: false,
|
|
stale: false,
|
|
tools: []
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.deepEqual(
|
|
installer.scanLocal().map((entry) => entry.tool_id).sort(),
|
|
["lumi_ai_invalid", "lumi_ai_missing_entrypoint", "lumi_ai_test_tool", "lumi_ai_web_search"]
|
|
);
|
|
|
|
loader.setEnabled("lumi_ai_web_search", true);
|
|
loader.setEnabled("lumi_ai_test_tool", true);
|
|
loader.setEnabled("lumi_ai_invalid", true);
|
|
loader.setEnabled("lumi_ai_missing_entrypoint", true);
|
|
await loader.loadEnabled();
|
|
assert.equal(registry.has("lumi_ai_web_search.lookup"), true);
|
|
assert.equal(registry.has("lumi_ai_test_tool.lookup"), true);
|
|
assert.equal(loader.status("lumi_ai_invalid").state, "unavailable");
|
|
assert.equal(loader.status("lumi_ai_missing_entrypoint").state, "unavailable");
|
|
|
|
await loader.disable("lumi_ai_test_tool");
|
|
assert.equal(registry.has("lumi_ai_test_tool.lookup"), false);
|
|
const disabledDiagnostics = await manager.diagnostics({
|
|
role: "user",
|
|
user: actor("user"),
|
|
context: origin("webui")
|
|
});
|
|
assert.equal(
|
|
disabledDiagnostics.plugins.find((plugin) => plugin.tool_id === "lumi_ai_test_tool").hidden_reason,
|
|
"disabled"
|
|
);
|
|
assert.equal(
|
|
disabledDiagnostics.plugins.find((plugin) => plugin.tool_id === "lumi_ai_invalid").hidden_reason,
|
|
"schema_invalid"
|
|
);
|
|
|
|
const webui = registry.inspect({
|
|
role: "user",
|
|
user: actor("user"),
|
|
context: origin("webui")
|
|
});
|
|
assert(webui.exposed.some((tool) => tool.tool_id === "lumi_ai_web_search.lookup"));
|
|
const discord = registry.inspect({
|
|
role: "user",
|
|
user: actor("user"),
|
|
context: origin("discord", false)
|
|
});
|
|
assert(discord.exposed.some((tool) => tool.tool_id === "lumi_ai_web_search.lookup"));
|
|
|
|
registry.register(actionDefinition());
|
|
const platformActions = registry.inspect({
|
|
role: "admin",
|
|
user: actor("admin"),
|
|
context: origin("discord", false)
|
|
});
|
|
assert.equal(
|
|
platformActions.considered.find((entry) => entry.tool.tool_id === "test_admin.remove").reason,
|
|
"scope_blocked"
|
|
);
|
|
|
|
const prompt = buildPrompt({
|
|
config: promptConfig(),
|
|
role: "user",
|
|
message: "Search for current Lumi information",
|
|
tools: webui.exposed,
|
|
originContext: origin("webui")
|
|
});
|
|
assert(prompt.includes('{"type":"tool_call","tool":"tool_id","arguments":{}}'));
|
|
assert(prompt.includes('"tool_id":"lumi_ai_web_search.lookup"'));
|
|
assert(prompt.includes('"kind":"lookup/read"'));
|
|
|
|
assert.equal(parseToolCallResult('{"type":"tool_call","tool":"x","arguments":{}}').status, "valid");
|
|
assert.equal(parseToolCallResult('Use this: {"type":"tool_call","tool":"x","arguments":{}}').status, "malformed");
|
|
assert.equal(
|
|
parseToolCallResult('{"type":"tool_call","tool":"x","arguments":{},"comment":"no"}').status,
|
|
"malformed"
|
|
);
|
|
|
|
await verifyReadOnlyFinalization(registry);
|
|
verifyConfirmation(registry);
|
|
assert(audit.some((entry) => entry.kind === "tool" && entry.status === "success"));
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
console.log("Lumi AI tool loading verification passed.");
|
|
}
|
|
|
|
async function verifyReadOnlyFinalization(registry) {
|
|
const outputs = [
|
|
'{"type":"tool_call","tool":"lumi_ai_web_search.lookup","arguments":{"query":"current Lumi release"}}',
|
|
"The current result is Lumi 1.2.3 according to the returned source."
|
|
];
|
|
let inferCalls = 0;
|
|
const provider = new AiProvider({
|
|
getConfig: () => ({
|
|
selected_model_id: "test",
|
|
support_scope: {},
|
|
instructions: {},
|
|
logging: {},
|
|
hard_generation_timeout_ms: 5000,
|
|
output_budgets: { simple_answer: 128 }
|
|
}),
|
|
runtime: {
|
|
infer: async () => ({
|
|
choices: [{ message: { content: outputs[inferCalls++] }, finish_reason: "stop" }],
|
|
usage: { prompt_tokens: 10, completion_tokens: 5 }
|
|
})
|
|
},
|
|
queue: {
|
|
length: 0,
|
|
run: async (_userId, _role, callback) => callback(0)
|
|
},
|
|
tools: registry,
|
|
metrics: { record() {} },
|
|
getContext: () => [],
|
|
lookupRepo: () => null,
|
|
getRepoContext: () => [],
|
|
getCorrections: () => []
|
|
});
|
|
const result = await provider.generate({
|
|
message: "What is the current Lumi release?",
|
|
user: actor("user"),
|
|
sessionId: "tool-test",
|
|
originContext: origin("webui"),
|
|
allowTools: true
|
|
});
|
|
assert.equal(inferCalls, 2);
|
|
assert.match(result.text, /Lumi 1\.2\.3/);
|
|
assert.deepEqual(result.tool_result, {
|
|
status: "ok",
|
|
query: "current Lumi release",
|
|
results: [{ title: "Lumi 1.2.3", url: "https://example.test/lumi" }]
|
|
});
|
|
}
|
|
|
|
function verifyConfirmation(registry) {
|
|
const prepared = registry.prepare({
|
|
tool: "test_admin.remove",
|
|
args: { target: "example" },
|
|
user: actor("admin"),
|
|
role: "admin",
|
|
sessionId: "confirm-test",
|
|
context: origin("webui")
|
|
});
|
|
assert.equal(prepared.execute, false);
|
|
assert(prepared.confirmation.id);
|
|
}
|
|
|
|
function createTool(pluginsDir, toolId, { readOnly }) {
|
|
const directory = path.join(pluginsDir, toolId);
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
const metadata = {
|
|
tool_id: toolId,
|
|
display_name: toolId,
|
|
version: "1.0.0",
|
|
description: "Test lookup tool",
|
|
scope: "assistant",
|
|
permissions: { required_role: "user" },
|
|
capabilities: ["Current information lookup"],
|
|
limitations: ["Test data only"],
|
|
tool_type: "lookup",
|
|
entrypoints: { backend: "index.js" },
|
|
risk_level: "low",
|
|
confirmation_required: false
|
|
};
|
|
fs.writeFileSync(path.join(directory, "tool_info.json"), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
fs.writeFileSync(path.join(directory, "readme.md"), `# ${toolId}\n`);
|
|
fs.writeFileSync(path.join(directory, "index.js"), `
|
|
module.exports.register = ({ registerTool }) => registerTool({
|
|
tool_id: "${toolId}.lookup",
|
|
display_name: "Lookup",
|
|
description: "Looks up current test information.",
|
|
required_role: "user",
|
|
required_permission: "${toolId}.use",
|
|
audit_category: "lookup",
|
|
confirmation_required: false,
|
|
risk_level: "low",
|
|
read_only: ${readOnly},
|
|
use_cases: ["Current information"],
|
|
output_expectations: "Structured test results.",
|
|
schema: { query: { type: "string", required: true } },
|
|
permission_check: ({ user }) => Boolean(user && user.id),
|
|
workflow_handler: async ({ arguments: args }) => ({
|
|
status: "ok",
|
|
query: args.query,
|
|
results: [{ title: "Lumi 1.2.3", url: "https://example.test/lumi" }]
|
|
})
|
|
});
|
|
`);
|
|
}
|
|
|
|
function createInvalidTool(pluginsDir, toolId) {
|
|
const directory = path.join(pluginsDir, toolId);
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
fs.writeFileSync(path.join(directory, "tool_info.json"), "{invalid");
|
|
fs.writeFileSync(path.join(directory, "readme.md"), "# Invalid\n");
|
|
}
|
|
|
|
function createMissingEntrypointTool(pluginsDir, toolId) {
|
|
const directory = path.join(pluginsDir, toolId);
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
const metadata = {
|
|
tool_id: toolId,
|
|
display_name: toolId,
|
|
version: "1.0.0",
|
|
description: "Missing entrypoint test",
|
|
scope: "assistant",
|
|
permissions: { required_role: "user" },
|
|
capabilities: ["Test"],
|
|
limitations: ["Unavailable"],
|
|
entrypoints: { backend: "missing.js" }
|
|
};
|
|
fs.writeFileSync(path.join(directory, "tool_info.json"), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
fs.writeFileSync(path.join(directory, "readme.md"), "# Missing entrypoint\n");
|
|
}
|
|
|
|
function actionDefinition() {
|
|
return {
|
|
tool_id: "test_admin.remove",
|
|
display_name: "Remove test data",
|
|
description: "Mutates test data.",
|
|
owning_plugin: "test_admin",
|
|
required_role: "admin",
|
|
required_permission: "test.remove",
|
|
audit_category: "test",
|
|
confirmation_required: true,
|
|
risk_level: "sensitive",
|
|
read_only: false,
|
|
schema: { target: { type: "string", required: true } },
|
|
permission_check: ({ user }) => user?.isAdmin === true,
|
|
workflow_handler: async () => ({ status: "ok" })
|
|
};
|
|
}
|
|
|
|
function actor(role) {
|
|
return {
|
|
id: `${role}-1`,
|
|
username: role,
|
|
isAdmin: role === "admin",
|
|
isMod: role === "mod"
|
|
};
|
|
}
|
|
|
|
function origin(platform, webuiActionsAllowed = platform === "webui") {
|
|
return {
|
|
origin: platform,
|
|
platform,
|
|
max_message_length: platform === "twitch" ? 450 : 1900,
|
|
permission_context: {
|
|
identified_user: true,
|
|
webui_actions_allowed: webuiActionsAllowed
|
|
}
|
|
};
|
|
}
|
|
|
|
function promptConfig() {
|
|
return {
|
|
support_scope: {},
|
|
instructions: {
|
|
roleplay_intensity: 0,
|
|
community_tone: "",
|
|
admin_custom: ""
|
|
}
|
|
};
|
|
}
|
|
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|