Files
NetMesh/electron/plugins/hostRpcRegistry.test.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

310 lines
11 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const {
PluginHostRpcRegistry,
assertHostMethod,
cloneAndFreezeMetadata,
} = require("./hostRpcRegistry.cjs");
function identity(overrides = {}) {
return {
pluginId: "com.example.registry",
pluginVersion: "1.0.0",
runtimeId: "runtime-1",
runtimeKind: "browser",
...overrides,
};
}
test("host RPC registry separates requests from notifications and rejects duplicate ownership", async () => {
const registry = new PluginHostRpcRegistry();
registry.registerRequest("settings.get", async (params, context) => ({
params,
kind: context.kind,
pluginId: context.pluginId,
}));
assert.throws(
() => registry.registerNotification("settings.get", () => {}),
/already registered/,
);
registry.registerNotification("telemetry.write", () => undefined);
const routes = registry.createRoutes(identity());
assert.deepEqual(
await routes.requestHandlers["settings.get"]({ key: "theme" }, { signal: AbortSignal.timeout(10) }),
{ params: { key: "theme" }, kind: "request", pluginId: "com.example.registry" },
);
assert.equal(routes.notificationHandlers["settings.get"], undefined);
assert.equal(routes.requestHandlers["telemetry.write"], undefined);
});
test("host RPC middleware receives immutable host identity, method metadata, and transport context", async () => {
const registry = new PluginHostRpcRegistry();
const seen = [];
const metadata = { permission: { name: "vault.metadata", resources: ["hosts"] } };
registry.use(async (context, next) => {
seen.push(context);
return next();
});
registry.registerRequest("vault.read", async (_params, context) => context.runtimeId, {
metadata,
});
metadata.permission.name = "spoofed";
metadata.permission.resources.push("credentials");
const routes = registry.createRoutes(identity());
const signal = AbortSignal.timeout(100);
assert.equal(await routes.requestHandlers["vault.read"](
{ hostId: "host-1" },
{
cancellationId: "cancel-1",
deadlineMs: 100,
pluginId: "com.attacker.spoofed",
requestId: 7,
runtimeId: "spoofed-runtime",
signal,
},
), "runtime-1");
assert.equal(Object.isFrozen(seen[0]), true);
assert.equal(seen[0].pluginVersion, "1.0.0");
assert.equal(seen[0].pluginId, "com.example.registry");
assert.equal(seen[0].runtimeId, "runtime-1");
assert.equal(seen[0].method, "vault.read");
assert.deepEqual(seen[0].metadata.permission, { name: "vault.metadata", resources: ["hosts"] });
assert.equal(Object.isFrozen(seen[0].metadata.permission), true);
assert.equal(Object.isFrozen(seen[0].metadata.permission.resources), true);
assert.deepEqual(seen[0].params, { hostId: "host-1" });
assert.equal(seen[0].requestId, 7);
assert.equal(seen[0].signal, signal);
assert.equal(typeof seen[0].assertActive, "function");
});
test("method parameters are validated and normalized before capability middleware", async () => {
const registry = new PluginHostRpcRegistry();
const seen = [];
registry.use(async (context, next) => {
seen.push(context.params);
return next();
});
registry.registerRequest("filesystem.stat", async (params) => params, {
validateParams(params) {
if (typeof params?.path !== "string") throw new TypeError("path is required");
return { path: params.path.normalize("NFC") };
},
});
const routes = registry.createRoutes(identity());
const normalized = await routes.requestHandlers["filesystem.stat"](
{ path: "cafe\u0301" },
{ signal: AbortSignal.timeout(100) },
);
assert.deepEqual(normalized, { path: "caf\u00e9" });
assert.deepEqual(seen, [{ path: "caf\u00e9" }]);
await assert.rejects(
routes.requestHandlers["filesystem.stat"]({}, { signal: AbortSignal.timeout(100) }),
/path is required/,
);
assert.equal(seen.length, 1);
});
test("parameter validators must be synchronous and metadata cloning cannot invoke __proto__", async () => {
const cloned = cloneAndFreezeMetadata(JSON.parse('{"__proto__":{"polluted":true}}'));
assert.equal(Object.getPrototypeOf(cloned), Object.prototype);
assert.equal(Object.hasOwn(cloned, "__proto__"), true);
assert.equal(cloned.__proto__.polluted, true);
assert.equal({}.polluted, undefined);
const registry = new PluginHostRpcRegistry();
registry.registerRequest("network.fetch", () => null, {
validateParams: async (params) => params,
});
const routes = registry.createRoutes(identity());
await assert.rejects(
routes.requestHandlers["network.fetch"]({}, { signal: AbortSignal.timeout(100) }),
/must be synchronous/,
);
});
test("host RPC identity is rechecked after asynchronous middleware and handler work", async () => {
const registry = new PluginHostRpcRegistry();
let current = true;
let releaseMiddleware;
let handlerCalls = 0;
registry.use(async (_context, next) => {
await new Promise((resolve) => { releaseMiddleware = resolve; });
return next();
});
registry.registerRequest("vault.write", async () => {
handlerCalls += 1;
return null;
});
const routes = registry.createRoutes(identity({
assertCurrent() {
if (!current) throw new Error("stale activation");
},
}));
const pending = routes.requestHandlers["vault.write"]({}, { signal: AbortSignal.timeout(100) });
await new Promise((resolve) => setImmediate(resolve));
current = false;
releaseMiddleware();
await assert.rejects(pending, /stale activation/);
assert.equal(handlerCalls, 0);
});
test("host RPC results are discarded when identity becomes stale during handler work", async () => {
const registry = new PluginHostRpcRegistry();
let current = true;
let releaseHandler;
registry.registerRequest("vault.read", async () => new Promise((resolve) => {
releaseHandler = resolve;
}));
const routes = registry.createRoutes(identity({
assertCurrent() {
if (!current) throw new Error("stale activation");
},
}));
const pending = routes.requestHandlers["vault.read"]({}, { signal: AbortSignal.timeout(100) });
await new Promise((resolve) => setImmediate(resolve));
current = false;
releaseHandler({ secret: "stale" });
await assert.rejects(pending, /stale activation/);
});
test("async capability handlers can recheck activation immediately before committing", async () => {
const registry = new PluginHostRpcRegistry();
let current = true;
let releasePreparation;
let commits = 0;
registry.registerRequest("settings.set", async (_params, context) => {
await new Promise((resolve) => { releasePreparation = resolve; });
await context.assertActive();
commits += 1;
return null;
});
const routes = registry.createRoutes(identity({
assertCurrent() {
if (!current) throw new Error("stale activation");
},
}));
const pending = routes.requestHandlers["settings.set"]({}, { signal: AbortSignal.timeout(100) });
await new Promise((resolve) => setImmediate(resolve));
current = false;
releasePreparation();
await assert.rejects(pending, /stale activation/);
assert.equal(commits, 0);
});
test("capability commit guards reject requests cancelled while handler work is pending", async () => {
const registry = new PluginHostRpcRegistry();
let releaseWork;
const work = new Promise((resolve) => { releaseWork = resolve; });
let committed = false;
registry.registerRequest("filesystem.write", async (_params, context) => {
await work;
await context.assertActive();
committed = true;
return null;
});
const controller = new AbortController();
const routes = registry.createRoutes({
pluginId: "com.example.cancelled",
pluginVersion: "1.0.0",
runtimeId: "runtime-cancelled",
runtimeKind: "utility",
});
const pending = routes.requestHandlers["filesystem.write"]({}, {
signal: controller.signal,
});
controller.abort(new Error("request cancelled"));
releaseWork();
await assert.rejects(pending, /request cancelled/);
assert.equal(committed, false);
});
test("cancelled middleware cannot enter a privileged capability handler", async () => {
const registry = new PluginHostRpcRegistry();
let releaseMiddleware;
const middlewareWork = new Promise((resolve) => { releaseMiddleware = resolve; });
let invoked = false;
registry.use(async (_context, next) => {
await middlewareWork;
return next();
});
registry.registerRequest("network.fetch", async () => {
invoked = true;
return null;
});
const controller = new AbortController();
const routes = registry.createRoutes({
pluginId: "com.example.cancelled",
pluginVersion: "1.0.0",
runtimeId: "runtime-cancelled",
runtimeKind: "utility",
});
const pending = routes.requestHandlers["network.fetch"]({}, {
signal: controller.signal,
});
controller.abort(new Error("approval cancelled"));
releaseMiddleware();
await assert.rejects(pending, /approval cancelled/);
assert.equal(invoked, false);
});
test("host RPC route snapshots remain stable while later registrations apply after restart", () => {
const registry = new PluginHostRpcRegistry();
registry.registerRequest("first.call", () => null);
const first = registry.createRoutes(identity());
registry.registerRequest("second.call", () => null);
const second = registry.createRoutes(identity({ runtimeId: "runtime-2" }));
assert.equal(first.requestHandlers["second.call"], undefined);
assert.equal(typeof second.requestHandlers["second.call"], "function");
assert.ok(second.revision > first.revision);
});
test("incoming streams route to the first owner and expose a host-assigned runtime identity", async () => {
const registry = new PluginHostRpcRegistry();
const seen = [];
registry.registerIncomingStream(async (stream, context) => {
seen.push([stream.streamId, context.runtimeId]);
return false;
});
registry.registerIncomingStream(async (stream) => stream.streamId === "owned");
const routes = registry.createRoutes(identity());
assert.equal(await routes.onIncomingStream({ streamId: "owned" }), true);
assert.equal(await routes.onIncomingStream({ streamId: "unknown" }), false);
assert.deepEqual(seen, [["owned", "runtime-1"], ["unknown", "runtime-1"]]);
});
test("incoming stream owners receive cancellation through the shared active guard", async () => {
const registry = new PluginHostRpcRegistry();
let releaseOwner;
const ownerWork = new Promise((resolve) => { releaseOwner = resolve; });
let committed = false;
registry.registerIncomingStream(async (_stream, context) => {
await ownerWork;
await context.assertActive();
committed = true;
return true;
});
const routes = registry.createRoutes(identity());
const controller = new AbortController();
const pending = routes.onIncomingStream({
streamId: "cancelled",
signal: controller.signal,
});
controller.abort(new Error("stream owner cancelled"));
releaseOwner();
await assert.rejects(pending, /stream owner cancelled/);
assert.equal(committed, false);
});
test("reserved lifecycle and transport methods cannot be claimed as host capabilities", () => {
const registry = new PluginHostRpcRegistry();
for (const method of ["plugin.initialize", "plugin.activate", "plugin.deactivate", "$/progress"]) {
assert.throws(() => assertHostMethod(method), /reserved/);
assert.throws(() => registry.registerRequest(method, () => null), /reserved/);
}
assert.equal(assertHostMethod("commands.execute"), "commands.execute");
});