158 lines
8.4 KiB
JavaScript
158 lines
8.4 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const { ensureDataDirs, PLUGIN_ROOT, PLUGIN_DATA, resolveData } = require("../backend/paths");
|
|
const { canUse } = require("../backend/permissions");
|
|
const { ToolRegistry } = require("../backend/tool_router");
|
|
const { RequestQueue } = require("../backend/queue_manager");
|
|
const { RuntimeManager, runCaptured } = require("../backend/runtime_manager");
|
|
const { getRuntimeState } = require("../backend/config_manager");
|
|
const { AiProvider } = require("../backend/ai_provider");
|
|
const { shouldAutoResume } = require("../index");
|
|
const { normalizeExitCode, classifyLaunchError } = require("../backend/error_codes");
|
|
const { redact } = require("../backend/diagnostics");
|
|
const { validateArchivePath, classifyError } = require("../backend/downloader");
|
|
const { EventEmitter } = require("events");
|
|
|
|
async function run() {
|
|
ensureDataDirs();
|
|
assert(PLUGIN_DATA.startsWith(PLUGIN_ROOT));
|
|
assert(resolveData("models", "model.gguf").startsWith(PLUGIN_ROOT));
|
|
assert.throws(() => resolveData("..", "..", "outside"), /escapes/);
|
|
assert.throws(() => validateArchivePath("../../outside.exe"), /traversal/);
|
|
assert.doesNotThrow(() => validateArchivePath("bin/llama-server.exe"));
|
|
assert.equal(classifyError(new Error("source unavailable (404)")).category, "http_404");
|
|
assert.equal(classifyError(new Error("hash mismatch")).category, "hash_mismatch");
|
|
|
|
const accessViolation = normalizeExitCode(-1073741819, null, "win32");
|
|
assert.equal(accessViolation.unsigned_exit_code, 3221225477);
|
|
assert.equal(accessViolation.hex_exit_code, "0xC0000005");
|
|
assert.equal(accessViolation.code, "STATUS_ACCESS_VIOLATION");
|
|
assert.equal(normalizeExitCode(139, null, "linux").code, "SIGSEGV");
|
|
assert.equal(classifyLaunchError({ code: "EACCES" }, "linux").category, "permission_denied");
|
|
assert.deepEqual(redact({ token: "secret", nested: { password: "secret", value: "ok" } }), { token: "[REDACTED]", nested: { password: "[REDACTED]", value: "ok" } });
|
|
const captured = await runCaptured(process.execPath, ["-e", "console.log('llama server usage')"], process.cwd(), 3000);
|
|
assert.equal(captured.code, 0);
|
|
assert.match(captured.stdout, /llama server usage/);
|
|
|
|
const config = { assistant_visibility: { admins: true, mods: false, users: true } };
|
|
assert.equal(canUse({ id: "a", isAdmin: true }, config), true);
|
|
assert.equal(canUse({ id: "m", isMod: true }, config), false);
|
|
assert.equal(canUse({ id: "u" }, config), true);
|
|
assert.equal(canUse(null, config), false);
|
|
|
|
const audit = [];
|
|
const calls = [];
|
|
const registry = new ToolRegistry((entry) => audit.push(entry));
|
|
registry.register({
|
|
tool_id: "test.action",
|
|
display_name: "Test action",
|
|
description: "Runs a test workflow.",
|
|
owning_plugin: "test",
|
|
required_role: "user",
|
|
required_permission: "test.self",
|
|
permission_check: ({ user }) => user.id === "user-1",
|
|
schema: { amount: "integer", recipient: "string" },
|
|
confirmation_required: true,
|
|
risk_level: "sensitive",
|
|
audit_category: "test",
|
|
workflow_handler: async (input) => { calls.push(input); return { ok: true }; }
|
|
});
|
|
assert.throws(() => registry.prepare({ tool: "test.action", args: { amount: "bad", recipient: "x" }, user: { id: "user-1" }, role: "user", sessionId: "s1" }), /integer/);
|
|
assert.throws(() => registry.prepare({ tool: "test.action", args: { amount: 1, recipient: "x" }, user: { id: "other" }, role: "user", sessionId: "s1" }), /permission/);
|
|
const prepared = registry.prepare({ tool: "test.action", args: { amount: 2, recipient: "x" }, user: { id: "user-1" }, role: "user", sessionId: "s1" });
|
|
await assert.rejects(() => registry.confirm({ id: prepared.confirmation.id, user: { id: "user-1" }, sessionId: "wrong" }), /invalid or expired/);
|
|
const expiring = registry.prepare({ tool: "test.action", args: { amount: 2, recipient: "x" }, user: { id: "user-1" }, role: "user", sessionId: "s1" });
|
|
registry.confirmations.get(expiring.confirmation.id).expiresAt = Date.now() - 1;
|
|
await assert.rejects(() => registry.confirm({ id: expiring.confirmation.id, user: { id: "user-1" }, sessionId: "s1" }), /invalid or expired/);
|
|
const valid = registry.prepare({ tool: "test.action", args: { amount: 3, recipient: "x" }, user: { id: "user-1" }, role: "user", sessionId: "s1" });
|
|
await registry.confirm({ id: valid.confirmation.id, user: { id: "user-1" }, sessionId: "s1" });
|
|
assert.equal(calls[0].user.id, "user-1");
|
|
assert.equal(calls[0].initiated_via_ai, true);
|
|
assert.equal(audit[0].tool_executed, true);
|
|
|
|
const queueConfig = { concurrency: 1, max_queue_length: 1, per_user_requests_per_minute: 20 };
|
|
const queue = new RequestQueue(() => queueConfig);
|
|
let release;
|
|
const blocked = new Promise((resolve) => { release = resolve; });
|
|
const first = queue.run("u1", "user", () => blocked);
|
|
const second = queue.run("u2", "user", async () => "second");
|
|
await assert.rejects(() => queue.run("u3", "user", async () => "third"), /busy/);
|
|
release("first");
|
|
assert.equal(await first, "first");
|
|
assert.equal(await second, "second");
|
|
|
|
const fakeMetrics = { record() {} };
|
|
const provider = new AiProvider({
|
|
getConfig: () => ({ instructions: { out_of_scope_response: "OUT" } }),
|
|
runtime: { infer: async () => { throw new Error("must not run"); } },
|
|
queue,
|
|
tools: registry,
|
|
metrics: fakeMetrics,
|
|
getContext: () => []
|
|
});
|
|
const refused = await provider.generate({ message: "What is the capital of France?", user: { id: "u1" }, sessionId: "s1" });
|
|
assert.equal(refused.refusal_reason, "out_of_scope");
|
|
const routed = await provider.generate({ message: "Where can I find Twitch configuration?", user: { id: "u1" }, sessionId: "s1" });
|
|
assert.equal(routed.success, true);
|
|
assert.match(routed.text, /twitch-wizard/);
|
|
const ambiguousProvider = new AiProvider({
|
|
getConfig: () => ({ selected_model_id: "test", request_timeout_ms: 1000, logging: {}, instructions: { identity: "Lumi", style: "Brief", allowed_topics: "Lumi", maximum_answer_length: 700, out_of_scope_response: "OUT" } }),
|
|
runtime: { infer: async () => ({ choices: [{ message: { content: "Open the relevant Lumi settings page." }, finish_reason: "stop" }] }) },
|
|
queue,
|
|
tools: registry,
|
|
metrics: fakeMetrics,
|
|
getContext: () => []
|
|
});
|
|
const ambiguous = await ambiguousProvider.generate({ message: "How do I change this option?", user: { id: "u1" }, sessionId: "s1" });
|
|
assert.equal(ambiguous.success, true);
|
|
let diagnosticMessages;
|
|
const testProvider = new AiProvider({
|
|
getConfig: () => ({ selected_model_id: "test", instructions: { maximum_answer_length: 700 } }),
|
|
runtime: { infer: async (messages) => { diagnosticMessages = messages; return { choices: [{ message: { content: "There are 3 Rs." }, finish_reason: "stop" }] }; } },
|
|
queue,
|
|
tools: registry,
|
|
metrics: fakeMetrics,
|
|
getContext: () => []
|
|
});
|
|
const diagnosticTest = await testProvider.test({ message: 'How many "R"s are in Strawberry?', user: { id: "u1" }, includeRaw: true });
|
|
assert.equal(diagnosticMessages[1].content, 'How many "R"s are in Strawberry?');
|
|
assert.equal(diagnosticTest.text, "There are 3 Rs.");
|
|
assert.match(diagnosticTest.raw_prompt, /local model diagnostic/);
|
|
|
|
const statePath = resolveData("config", "runtime_state.json");
|
|
const originalState = fs.readFileSync(statePath, "utf8");
|
|
try {
|
|
const runtime = new RuntimeManager({ getConfig: () => ({}), getModel: () => null, runtimeManifest: {} });
|
|
runtime.child = fakeChild();
|
|
await runtime.stop({ manual: false, reason: "bot_shutdown" });
|
|
assert.equal(getRuntimeState().desired_state, "running");
|
|
assert.equal(shouldAutoResume({ enabled: true }, getRuntimeState()), true);
|
|
runtime.child = fakeChild();
|
|
await runtime.stop({ manual: true, reason: "admin_stop" });
|
|
assert.equal(getRuntimeState().desired_state, "stopped");
|
|
assert.equal(shouldAutoResume({ enabled: true }, getRuntimeState()), false);
|
|
assert.equal(shouldAutoResume({ enabled: true }, { desired_state: "running", last_manual_stop: false, last_crashed: true }), false);
|
|
} finally {
|
|
fs.writeFileSync(statePath, originalState);
|
|
}
|
|
|
|
console.log("Lumi AI verification passed.");
|
|
}
|
|
|
|
function fakeChild() {
|
|
const child = new EventEmitter();
|
|
child.killed = false;
|
|
child.exitCode = null;
|
|
child.kill = function kill() {
|
|
this.killed = true;
|
|
this.exitCode = 0;
|
|
this.emit("exit", 0, null);
|
|
};
|
|
return child;
|
|
}
|
|
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|