137 lines
4.7 KiB
JavaScript
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;
|
|
});
|