Lumi/scripts/verify-assistant-panels.js
2026-06-12 11:54:46 +02:00

137 lines
4.7 KiB
JavaScript

const assert = require("assert");
const fs = require("fs");
const path = require("path");
const { createAssistantPanelManager } = require("../src/web/public/assistant-panels");
async function run() {
verifySlotPlacement();
await verifyDynamicLifecycle();
console.log("Assistant panel verification passed.");
}
function verifySlotPlacement() {
const layout = fs.readFileSync(
path.join(__dirname, "..", "src", "web", "views", "partials", "layout-top.ejs"),
"utf8"
);
const nav = layout.indexOf('<nav class="sidebar-nav">');
const slot = layout.indexOf("data-assistant-panel-slot");
const footer = layout.indexOf('<div class="sidebar-footer">');
assert(nav >= 0 && slot > nav && footer > slot, "Assistant slot must be between nav and footer.");
assert.equal(layout.includes("include(panel.view"), false, "Assistant panels must not be server-rendered into navigation.");
const server = fs.readFileSync(
path.join(__dirname, "..", "src", "web", "server.js"),
"utf8"
);
assert(server.includes('app.get("/api/assistant-panels"'), "Assistant availability endpoint must exist.");
assert(server.includes("if (!req.session.user)"), "Assistant endpoint must reject anonymous visibility.");
assert(server.includes('typeof panel.canAccess === "function"'), "Assistant endpoint must honor plugin permission resolvers.");
assert(server.includes("render_error:"), "Assistant endpoint must return structured render failures.");
const plugin = fs.readFileSync(
path.join(__dirname, "..", "plugins", "lumi_ai", "index.js"),
"utf8"
);
assert(plugin.includes("canAccess: (user) => canUseAssistant"), "Lumi AI panel must use the shared permission resolver.");
assert.equal(plugin.includes('role: "user",\n version:'), false, "Lumi AI panel must not use unsupported core user RBAC.");
}
async function verifyDynamicLifecycle() {
const roots = [];
const diagnosticReports = [];
const slot = {
appendChild(root) {
roots.push(root);
root.parent = this;
root.isConnected = true;
}
};
const document = {
hidden: false,
head: {
appendChild(element) {
element.onload?.();
}
},
querySelector(selector) {
return selector === "[data-assistant-panel-slot]" ? slot : null;
},
createElement(tag) {
if (tag === "template") {
const template = { content: { firstElementChild: null } };
Object.defineProperty(template, "innerHTML", {
set() {
const root = {
dataset: {},
isConnected: false,
remove() {
const index = roots.indexOf(this);
if (index >= 0) roots.splice(index, 1);
this.isConnected = false;
}
};
template.content.firstElementChild = root;
}
});
return template;
}
return {};
},
addEventListener() {},
removeEventListener() {}
};
const manager = createAssistantPanelManager({
document,
fetch: async (url, options = {}) => {
if (String(url).includes("visibility-debug")) {
diagnosticReports.push(JSON.parse(options.body));
return { ok: true, status: 200, json: async () => ({ success: true }) };
}
return { ok: true, status: 200, json: async () => ({ panels: [] }) };
},
setInterval: () => 1,
clearInterval() {}
});
let mounts = 0;
let unmounts = 0;
manager.register("lumi_ai", {
mount() { mounts += 1; },
unmount() { unmounts += 1; }
});
const available = {
panels: [{
available: true,
panel_id: "lumi_ai",
version: "1",
html: '<div data-assistant-panel-id="lumi_ai"></div>'
}]
};
await manager.reconcile(available);
await manager.reconcile(available);
assert.equal(roots.length, 1, "Repeated availability must not duplicate the panel.");
assert.equal(mounts, 1, "Repeated availability must not duplicate initialization.");
await manager.reconcile({
panels: [{ ...available.panels[0], version: "2" }]
});
assert.equal(roots.length, 1, "Version changes must replace rather than duplicate the panel.");
assert.equal(mounts, 2);
assert.equal(unmounts, 1);
await manager.reconcile({
panels: [{ available: false, panel_id: "lumi_ai", status: "offline" }]
});
assert.equal(roots.length, 0, "Unavailable panels must be removed.");
assert.equal(unmounts, 2, "Removal must invoke plugin cleanup.");
await manager.reconcile({
panels: [{ available: true, panel_id: "lumi_ai", version: "3", html: "" }]
});
assert.equal(roots.length, 0, "Empty panel HTML must not mount.");
assert(diagnosticReports.some((report) => report.mount_error?.includes("empty HTML")));
manager.destroy();
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});