Files
Netcatty/electron/plugins/pluginBridge.test.cjs
T
ryan-wong-coder 7a47187e37 feat(plugins): add isolated plugin host runtime (#2303)
* feat(plugins): add isolated plugin host runtime

* test(plugins): build runtime dependencies before tests

* fix(plugins): harden runtime lifecycle and recovery

* fix(plugins): harden runtime extension boundaries

* fix(plugins): enforce runtime integrity boundaries

* fix(plugins): await utility process exit

* fix(plugins): harden utility process containment

* fix(plugins): close runtime recovery gaps

* fix(plugins): harden runtime lifecycle boundaries
2026-07-19 08:22:17 +08:00

94 lines
3.0 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const {
CHANNELS,
createTrustedPluginBridgeSender,
registerPluginBridge,
} = require("./pluginBridge.cjs");
function createIpcMain() {
const handlers = new Map();
return {
handlers,
handle(channel, handler) { handlers.set(channel, handler); },
};
}
test("plugin management bridge is unavailable unless the local development gate is explicit", async () => {
const ipcMain = createIpcMain();
registerPluginBridge(ipcMain, {
manager: null,
env: {},
isTrustedSender: () => true,
});
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.status)({}), {
available: false,
experimental: true,
});
await assert.rejects(ipcMain.handlers.get(CHANNELS.list)({}), /runtime is disabled/);
});
test("plugin management bridge fails closed when the host manager is unavailable", async () => {
const ipcMain = createIpcMain();
registerPluginBridge(ipcMain, {
manager: null,
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.status)({}), {
available: false,
experimental: true,
});
await assert.rejects(ipcMain.handlers.get(CHANNELS.list)({}), /disabled or unavailable/);
});
test("plugin management bridge checks sender ownership before invoking manager", async () => {
const calls = [];
const ipcMain = createIpcMain();
registerPluginBridge(ipcMain, {
manager: {
initialize: async () => {},
list: async () => [],
install: async (...args) => calls.push(args),
setEnabled: async () => null,
restart: async () => null,
uninstall: async () => true,
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: createTrustedPluginBridgeSender({ devServerUrl: "http://localhost:5173" }),
});
const trusted = { senderFrame: { url: "app://netcatty/index.html" } };
await ipcMain.handlers.get(CHANNELS.install)(trusted, { archivePath: "/plugin.ncpkg", enable: true });
assert.deepEqual(calls, [["/plugin.ncpkg", { enable: true }]]);
await assert.rejects(
ipcMain.handlers.get(CHANNELS.list)({ senderFrame: { url: "https://attacker.invalid/" } }),
/Untrusted/,
);
});
test("plugin management availability follows asynchronous host initialization", async () => {
const ipcMain = createIpcMain();
let listCalls = 0;
const initializationError = new Error("package recovery failed");
registerPluginBridge(ipcMain, {
manager: {
initialize: async () => { throw initializationError; },
list: async () => { listCalls += 1; return []; },
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.status)({}), {
available: false,
experimental: true,
});
await assert.rejects(ipcMain.handlers.get(CHANNELS.list)({}), (error) => (
error.message.includes("disabled or unavailable") && error.cause === initializationError
));
assert.equal(listCalls, 0);
});