mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-12 09:24:24 +00:00
118 lines
8.8 KiB
JavaScript
118 lines
8.8 KiB
JavaScript
"use strict";
|
|
const assert = require("node:assert/strict");
|
|
const { readFile } = require("node:fs/promises");
|
|
const { EventEmitter } = require("node:events");
|
|
const { pathToFileURL } = require("node:url");
|
|
const test = require("node:test");
|
|
const { MessageChannel } = require("node:worker_threads");
|
|
const { PluginRpcRouter } = require("./rpcRouter.cjs");
|
|
const { PluginHostRpcRegistry } = require("./hostRpcRegistry.cjs");
|
|
const { PluginPermissionEngine } = require("./permissionEngine.cjs");
|
|
const { PluginHostInteractionService } = require("./hostInteractionService.cjs");
|
|
const { PluginContributionService } = require("./contributionService.cjs");
|
|
|
|
for (const source of ["compose", "share"]) test(`normal browser example completes the ${source} text workflow through permission middleware`, async () => {
|
|
const { startPluginRuntime } = await import("./runtime/runtimePeer.mjs");
|
|
const plugin = await import("../../examples/plugins/host-interactions/browser.js");
|
|
const manifest = JSON.parse(await readFile(new URL("../../examples/plugins/host-interactions/netcatty.plugin.json", pathToFileURL(__filename)), "utf8"));
|
|
const audit = [], copied = [], requests = [];
|
|
let reads = 0, active = true;
|
|
const database = { listPermissionGrants: () => [], recordSecurityAudit(...args) { audit.push(args); } };
|
|
const permissionEngine = new PluginPermissionEngine({ database,
|
|
requestDecision: async (request) => { requests.push(request.permission); return { requestId: request.requestId, decision: "allow", scope: "once" }; },
|
|
});
|
|
let service;
|
|
const sender = Object.assign(new EventEmitter(), { id: 7, isDestroyed: () => false,
|
|
send(_channel, message) {
|
|
if (message.closed) return;
|
|
queueMicrotask(() => {
|
|
const action = message.request.kind === "clipboard.read" && message.phase === "request" ? "preview" : "submit";
|
|
const value = message.request.kind === "quickPick" ? source : message.request.kind === "input" ? "synthetic output" : undefined;
|
|
void service.respond(sender, { requestId: message.requestId, actionToken: message.actionToken, action, ...(value === undefined ? {} : { value }) }).catch(() => {});
|
|
});
|
|
},
|
|
});
|
|
const window = Object.assign(new EventEmitter(), { webContents: sender, isFocused: () => true, isVisible: () => true, isDestroyed: () => false });
|
|
service = new PluginHostInteractionService({ database, permissionEngine, getWindow: () => window, isLocked: () => false,
|
|
quotaManager: { chargeBytes() {} },
|
|
clipboard: { readText() { reads++; return "synthetic private text"; }, writeText(text) { copied.push(text); } },
|
|
});
|
|
service.setReady(sender, true);
|
|
const registry = new PluginHostRpcRegistry();
|
|
registry.use(permissionEngine.createMiddleware()); service.registerRpcCapabilities(registry);
|
|
const routes = registry.createRoutes({ pluginId: manifest.id, pluginVersion: manifest.version, runtimeId: "runtime-synthetic", runtimeKind: "browser", manifest,
|
|
assertCurrent() { if (!active) throw new Error("stale runtime"); },
|
|
});
|
|
const { port1, port2 } = new MessageChannel();
|
|
const host = new PluginRpcRouter({ pluginId: manifest.id, send: (message) => port1.postMessage(message), handlers: routes.requestHandlers });
|
|
port1.on("message", (message) => { void host.accept(message); });
|
|
const peer = await startPluginRuntime({ port: port2,
|
|
config: { pluginId: manifest.id, pluginVersion: manifest.version, netcattyVersion: "0.0.0", apiVersion: "0.1.0-internal.1", enabledFeatures: ["host.interactions"] },
|
|
loadPlugin: async () => plugin,
|
|
});
|
|
try {
|
|
await host.request("plugin.initialize", { netcattyVersion: "0.0.0", apiVersion: "0.1.0-internal.1", supportedFeatures: ["host.interactions"] });
|
|
await host.request("plugin.activate", {});
|
|
const result = await host.request("plugin.command.execute", { command: "com.netcatty.host-interactions.text", invocation: { source: "palette" } });
|
|
const output = source === "compose" ? "synthetic output" : "SYNTHETIC PRIVATE TEXT";
|
|
assert.deepEqual(result, { copied: true, characters: output.length });
|
|
assert.deepEqual(copied, [output]); assert.equal(reads, source === "share" ? 1 : 0);
|
|
assert.ok(requests.includes("ui.interact")); assert.ok(requests.includes("clipboard.write"));
|
|
assert.equal(requests.includes("clipboard.read"), source === "share");
|
|
assert.equal(JSON.stringify(audit).includes("synthetic private text"), false);
|
|
assert.equal(JSON.stringify(audit).includes("synthetic output"), false);
|
|
assert.equal(service.pending.size, 0);
|
|
} finally { active = false; await peer.dispose(); host.close(); port1.close(); service.shutdown(); permissionEngine.shutdown(); }
|
|
});
|
|
|
|
test("outer command deadline closes its real browser clipboard confirmation and rejects late commit", async () => {
|
|
const { startPluginRuntime } = await import("./runtime/runtimePeer.mjs");
|
|
const plugin = await import("../../examples/plugins/host-interactions/browser.js");
|
|
const manifest = JSON.parse(await readFile(new URL("../../examples/plugins/host-interactions/netcatty.plugin.json", pathToFileURL(__filename)), "utf8"));
|
|
const identity = { pluginId: manifest.id, pluginVersion: manifest.version, runtimeId: "runtime-deadline", runtimeKind: "browser" };
|
|
const writes = [], messages = [];
|
|
const database = { listPermissionGrants: () => [], recordSecurityAudit() {},
|
|
listPlugins: () => [{ id: manifest.id, activeVersion: manifest.version, manifest, enabled: true, runtime: { status: "running" } }],
|
|
};
|
|
const engine = new PluginPermissionEngine({ database, requestDecision: async (request) => ({ requestId: request.requestId, decision: "allow", scope: "once" }) });
|
|
let service, confirm;
|
|
const copyPrompt = new Promise((resolve) => { confirm = resolve; });
|
|
const sender = Object.assign(new EventEmitter(), { id: 8, isDestroyed: () => false, send(_channel, message) {
|
|
messages.push(message); if (message.closed) return;
|
|
if (message.request.kind === "clipboard.write") { confirm(message); return; }
|
|
const value = message.request.kind === "quickPick" ? "compose" : "synthetic pending copy";
|
|
queueMicrotask(() => { void service.respond(sender, { requestId: message.requestId, actionToken: message.actionToken, action: "submit", value }).catch(() => {}); });
|
|
} });
|
|
const owner = Object.assign(new EventEmitter(), { webContents: sender, isFocused: () => true, isVisible: () => true, isDestroyed: () => false });
|
|
service = new PluginHostInteractionService({ database, permissionEngine: engine, getWindow: () => owner, isLocked: () => false,
|
|
quotaManager: { chargeBytes() {} }, clipboard: { writeText(text) { writes.push(text); } } });
|
|
service.setReady(sender, true);
|
|
const registry = new PluginHostRpcRegistry(); registry.use(engine.createMiddleware()); service.registerRpcCapabilities(registry);
|
|
const routes = registry.createRoutes({ ...identity, manifest, assertCurrent() {} });
|
|
const { port1, port2 } = new MessageChannel();
|
|
const host = new PluginRpcRouter({ pluginId: manifest.id, send: (message) => port1.postMessage(message), handlers: routes.requestHandlers });
|
|
port1.on("message", (message) => { void host.accept(message); });
|
|
const features = manifest.features.required;
|
|
const peer = await startPluginRuntime({ port: port2, config: { ...identity, netcattyVersion: "0.0.0", apiVersion: "0.1.0-internal.1", enabledFeatures: features }, loadPlugin: async () => plugin });
|
|
try {
|
|
await host.request("plugin.initialize", { netcattyVersion: "0.0.0", apiVersion: "0.1.0-internal.1", supportedFeatures: features });
|
|
await host.request("plugin.activate", {});
|
|
const contributions = new PluginContributionService({ database, onCommandFinished: (runtimeId) => service.cancelRuntimeRequests(runtimeId),
|
|
runtimeSupervisor: { async start() { return identity; }, request(_pluginId, method, params, options) {
|
|
assert.equal(options.timeoutMs, 180_000, "host chooses the fixed declared budget");
|
|
return host.request(method, params, { timeoutMs: 500 }); // bounded synthetic timer, no production timeout change
|
|
} },
|
|
});
|
|
const command = contributions.executeCommand("com.netcatty.host-interactions.text");
|
|
const prompt = await Promise.race([copyPrompt, command.then(() => { throw new Error("command finished before confirmation"); })]);
|
|
const attempts = service.rates.get(identity.runtimeId).count;
|
|
await assert.rejects(command, /timed out/);
|
|
assert.equal(service.pending.size, 0);
|
|
assert.equal(service.rates.get(identity.runtimeId).count, attempts);
|
|
assert.ok(messages.some((message) => message.closed && message.requestId === prompt.requestId));
|
|
assert.equal(await service.respond(sender, { requestId: prompt.requestId, actionToken: prompt.actionToken, action: "submit" }), false);
|
|
assert.deepEqual(writes, []);
|
|
assert.equal(contributions.activeCommandCalls.size, 0);
|
|
} finally { await peer.dispose(); host.close(); port1.close(); service.shutdown(); engine.shutdown(); }
|
|
});
|