[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
618
electron/plugins/runtimePeer.test.cjs
Normal file
618
electron/plugins/runtimePeer.test.cjs
Normal file
@@ -0,0 +1,618 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
const { MessageChannel } = require("node:worker_threads");
|
||||
|
||||
const { PluginRpcError, PluginRpcRouter, RPC_ERRORS } = require("./rpcRouter.cjs");
|
||||
|
||||
test("runtime peer exposes secure host capabilities over the canonical RPC transport", async () => {
|
||||
const { startPluginRuntime } = await import("./runtime/runtimePeer.mjs");
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const values = new Map();
|
||||
const calls = [];
|
||||
const deadlines = [];
|
||||
const host = new PluginRpcRouter({
|
||||
pluginId: "com.example.peer",
|
||||
send(message) { port1.postMessage(message); },
|
||||
handlers: {
|
||||
"storage.get": async () => {
|
||||
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Invalid storage key");
|
||||
},
|
||||
"storage.set": async ({ key, value }) => { values.set(key, value); return null; },
|
||||
"secrets.set": async ({ key, value }) => {
|
||||
calls.push(["secret", key, value]);
|
||||
return {
|
||||
secret: { kind: "secret", id: "secret-reference-0000000000000000", key },
|
||||
};
|
||||
},
|
||||
"secrets.get": async () => ({
|
||||
found: true,
|
||||
secret: { kind: "secret", id: "secret-reference-0000000000000000", key: "api-key" },
|
||||
}),
|
||||
"credentials.createLease": async ({ operationId }) => ({
|
||||
kind: "secret-lease",
|
||||
id: "l".repeat(32),
|
||||
operationId,
|
||||
expiresAt: 10_000,
|
||||
}),
|
||||
"network.request": async ({ url, timeoutMs }, requestContext) => {
|
||||
deadlines.push(["network", requestContext.deadlineMs, timeoutMs]);
|
||||
return {
|
||||
url,
|
||||
status: 200,
|
||||
headers: {},
|
||||
body: { encoding: "base64", data: "b2s=" },
|
||||
};
|
||||
},
|
||||
"filesystem.readFile": async ({ path }) => ({ data: `read:${path}` }),
|
||||
"filesystem.writeFile": async ({ path, data }) => { calls.push(["write", path, data]); return null; },
|
||||
"filesystem.stat": async () => ({ kind: "file", size: 2, modifiedAt: 1 }),
|
||||
"filesystem.readDirectory": async () => ({ entries: [{ name: "file", kind: "file" }] }),
|
||||
"companion.start": async () => ({ handleId: "companion-handle-000000000000" }),
|
||||
"companion.request": async ({ method, params, timeoutMs }, requestContext) => {
|
||||
deadlines.push(["companion", requestContext.deadlineMs, timeoutMs]);
|
||||
return { method, params };
|
||||
},
|
||||
"companion.stop": async ({ handleId }) => { calls.push(["stop", handleId]); return null; },
|
||||
"log.write": async () => null,
|
||||
},
|
||||
});
|
||||
let resolveUnhandledStreamCancellation;
|
||||
let rejectUnhandledStreamCancellation;
|
||||
const unhandledStreamCancellation = new Promise((resolve, reject) => {
|
||||
resolveUnhandledStreamCancellation = resolve;
|
||||
rejectUnhandledStreamCancellation = reject;
|
||||
});
|
||||
port1.on("message", (message) => {
|
||||
const accepted = host.accept(message);
|
||||
if (message?.frame?.streamId === "unhandled" && message.frame.kind === "cancel") {
|
||||
accepted.then(resolveUnhandledStreamCancellation, rejectUnhandledStreamCancellation);
|
||||
}
|
||||
});
|
||||
const lifecycle = [];
|
||||
const peer = await startPluginRuntime({
|
||||
port: port2,
|
||||
config: {
|
||||
pluginId: "com.example.peer",
|
||||
pluginVersion: "1.0.0",
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
enabledFeatures: [],
|
||||
},
|
||||
async loadPlugin() {
|
||||
return {
|
||||
default: {
|
||||
async activate(context) {
|
||||
lifecycle.push("activate");
|
||||
await assert.rejects(
|
||||
context.storage.get("invalid"),
|
||||
(error) => error?.code === "invalid_argument"
|
||||
&& error?.message === "Invalid storage key",
|
||||
);
|
||||
await context.storage.set("answer", 42);
|
||||
const secret = await context.secrets.set("api-key", "value");
|
||||
const lease = await context.credentials.createLease(secret, {
|
||||
operationId: "network:login",
|
||||
purpose: "Authenticate",
|
||||
});
|
||||
assert.equal(lease.operationId, "network:login");
|
||||
const vaultLease = await context.credentials.createLease({
|
||||
kind: "credential",
|
||||
id: "vault-credential-1",
|
||||
}, {
|
||||
operationId: "connection:login",
|
||||
purpose: "Authenticate connection",
|
||||
});
|
||||
assert.equal(vaultLease.operationId, "connection:login");
|
||||
assert.equal((await context.network.request({
|
||||
url: "https://example.com",
|
||||
timeoutMs: 45_000,
|
||||
})).status, 200);
|
||||
assert.equal(await context.filesystem.readFile("/tmp/file"), "read:/tmp/file");
|
||||
await context.filesystem.writeFile("/tmp/file", "ok", { overwrite: true });
|
||||
assert.equal((await context.filesystem.stat("/tmp/file")).kind, "file");
|
||||
assert.equal((await context.filesystem.readDirectory("/tmp")).length, 1);
|
||||
const companion = await context.companions.start("com.example.peer.helper");
|
||||
assert.deepEqual(await companion.request("echo", { value: 1 }, { timeoutMs: 50_000 }), {
|
||||
method: "echo",
|
||||
params: { value: 1 },
|
||||
});
|
||||
await companion.stop();
|
||||
},
|
||||
async deactivate() { lifecycle.push("deactivate"); },
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
const initialized = await host.request("plugin.initialize", {
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
supportedFeatures: [],
|
||||
});
|
||||
assert.equal(initialized.pluginId, "com.example.peer");
|
||||
const unhandledStream = await host.streams.openOutgoing("unhandled", 1024);
|
||||
await unhandledStreamCancellation;
|
||||
await assert.rejects(unhandledStream.write(new Uint8Array([1])), /closed/);
|
||||
await assert.rejects(
|
||||
host.request("plugin.unknown", {}),
|
||||
(error) => error?.code === -32012 && error?.data?.pluginCode === "unsupported",
|
||||
);
|
||||
await host.request("plugin.activate", {});
|
||||
assert.equal(values.get("answer"), 42);
|
||||
assert.deepEqual(calls, [
|
||||
["secret", "api-key", "value"],
|
||||
["write", "/tmp/file", "ok"],
|
||||
["stop", "companion-handle-000000000000"],
|
||||
]);
|
||||
assert.deepEqual(deadlines, [
|
||||
["network", 45_000, 45_000],
|
||||
["companion", 50_000, 50_000],
|
||||
]);
|
||||
await host.request("plugin.deactivate", {});
|
||||
await peer.dispose();
|
||||
host.close();
|
||||
assert.deepEqual(lifecycle, ["activate", "deactivate"]);
|
||||
});
|
||||
|
||||
test("runtime companion handles retry a failed stop request", async () => {
|
||||
const { startPluginRuntime } = await import("./runtime/runtimePeer.mjs");
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
let stopCalls = 0;
|
||||
const host = new PluginRpcRouter({
|
||||
pluginId: "com.example.retry-stop",
|
||||
send(message) { port1.postMessage(message); },
|
||||
handlers: {
|
||||
"companion.start": async () => ({ handleId: "companion-handle-000000000001" }),
|
||||
"companion.stop": async () => {
|
||||
stopCalls += 1;
|
||||
if (stopCalls === 1) throw new Error("transient stop failure");
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
port1.on("message", (message) => host.accept(message));
|
||||
const peer = await startPluginRuntime({
|
||||
port: port2,
|
||||
config: {
|
||||
pluginId: "com.example.retry-stop",
|
||||
pluginVersion: "1.0.0",
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
enabledFeatures: [],
|
||||
},
|
||||
async loadPlugin() {
|
||||
return {
|
||||
default: {
|
||||
async activate(context) {
|
||||
const companion = await context.companions.start("com.example.retry-stop.helper");
|
||||
await assert.rejects(companion.stop());
|
||||
await companion.stop();
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await host.request("plugin.initialize", {
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
supportedFeatures: [],
|
||||
});
|
||||
await host.request("plugin.activate", {});
|
||||
assert.equal(stopCalls, 2);
|
||||
await peer.dispose();
|
||||
host.close();
|
||||
});
|
||||
|
||||
test("runtime peer exposes contribution APIs and routes host UI events", async () => {
|
||||
const { startPluginRuntime } = await import("./runtime/runtimePeer.mjs");
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const calls = [];
|
||||
const host = new PluginRpcRouter({
|
||||
pluginId: "com.example.ui",
|
||||
send(message) { port1.postMessage(message); },
|
||||
handlers: {
|
||||
"settings.get": async ({ settingId }) => ({ found: true, value: `value:${settingId}` }),
|
||||
"settings.update": async ({ settingId, value }) => {
|
||||
calls.push(["setting", settingId, value]);
|
||||
return { restartRequired: false };
|
||||
},
|
||||
"commands.execute": async ({ command, args }) => {
|
||||
calls.push(["execute", command, args]);
|
||||
return "host-result";
|
||||
},
|
||||
"contextKeys.set": async ({ key, value }) => { calls.push(["context", key, value]); return null; },
|
||||
"views.getState": async ({ viewId, scopeId }) => ({ state: { viewId, scopeId } }),
|
||||
"views.setState": async ({ viewId, scopeId, state }) => {
|
||||
calls.push(["view-state", viewId, scopeId, state]);
|
||||
return null;
|
||||
},
|
||||
"views.postMessage": async ({ viewId, message }) => { calls.push(["view-message", viewId, message]); },
|
||||
},
|
||||
});
|
||||
port1.on("message", (message) => host.accept(message));
|
||||
const events = [];
|
||||
let pluginContext;
|
||||
const peer = await startPluginRuntime({
|
||||
port: port2,
|
||||
config: {
|
||||
pluginId: "com.example.ui",
|
||||
pluginVersion: "1.0.0",
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
enabledFeatures: [],
|
||||
environment: {
|
||||
locale: "en-GB",
|
||||
theme: "light",
|
||||
reducedMotion: false,
|
||||
highContrast: true,
|
||||
themeTokens: { "--background": "initial" },
|
||||
},
|
||||
},
|
||||
async loadPlugin() {
|
||||
return {
|
||||
default: {
|
||||
async activate(context) {
|
||||
pluginContext = context;
|
||||
assert.equal(context.environment.locale, "fr-FR");
|
||||
assert.equal(context.environment.theme, "dark");
|
||||
assert.equal(context.environment.highContrast, true);
|
||||
assert.deepEqual(context.environment.themeTokens, { "--background": "active" });
|
||||
assert.throws(() => {
|
||||
context.environment.themeTokens["--background"] = "mutated";
|
||||
}, /read only/u);
|
||||
context.settings.onDidChange((event) => events.push(["settings", event.settingId]));
|
||||
context.environment.onDidChange((event) => events.push([
|
||||
"environment",
|
||||
event.locale,
|
||||
event.theme,
|
||||
event.themeTokens["--background"],
|
||||
]));
|
||||
context.views.onDidReceiveMessage("com.example.ui.view", (message) => events.push(["view", message]));
|
||||
context.commands.registerCommand("com.example.ui.hello", async (args, invocation) => ({ args, source: invocation.source }));
|
||||
context.commands.registerCommand("com.example.ui.void", async () => undefined);
|
||||
const staleDisposable = context.commands.registerCommand("com.example.ui.replaceable", async () => "old");
|
||||
staleDisposable.dispose();
|
||||
context.commands.registerCommand("com.example.ui.replaceable", async () => "new");
|
||||
staleDisposable.dispose();
|
||||
assert.equal(await context.settings.get("com.example.ui.greeting"), "value:com.example.ui.greeting");
|
||||
assert.deepEqual(await context.settings.update("com.example.ui.greeting", "hello"), { restartRequired: false });
|
||||
assert.equal(await context.commands.executeCommand("com.example.ui.hello", { nested: true }), "host-result");
|
||||
await context.contextKeys.set("com.example.ui.ready", true);
|
||||
assert.deepEqual(await context.views.getState("com.example.ui.view", "window-1"), {
|
||||
viewId: "com.example.ui.view",
|
||||
scopeId: "window-1",
|
||||
});
|
||||
await context.views.setState("com.example.ui.view", "window-1", { selected: 2 });
|
||||
context.views.postMessage("com.example.ui.view", { ready: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await host.request("plugin.initialize", {
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
supportedFeatures: [],
|
||||
});
|
||||
await host.request("plugin.activate", {
|
||||
environment: {
|
||||
locale: "fr-FR",
|
||||
theme: "dark",
|
||||
reducedMotion: true,
|
||||
highContrast: true,
|
||||
themeTokens: { "--background": "active" },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await host.request("plugin.command.execute", {
|
||||
command: "com.example.ui.hello",
|
||||
args: { name: "Catty" },
|
||||
invocation: { source: "palette" },
|
||||
}), { args: { name: "Catty" }, source: "palette" });
|
||||
assert.equal(await host.request("plugin.command.execute", {
|
||||
command: "com.example.ui.void",
|
||||
invocation: { source: "palette" },
|
||||
}), null);
|
||||
assert.equal(await host.request("plugin.command.execute", {
|
||||
command: "com.example.ui.replaceable",
|
||||
invocation: { source: "palette" },
|
||||
}), "new");
|
||||
|
||||
host.notify("plugin.settings.changed", { settingId: "com.example.ui.greeting", scope: "application", scopeId: "application", source: "host" });
|
||||
host.notify("plugin.environment.changed", {
|
||||
locale: "zh-CN",
|
||||
theme: "dark",
|
||||
reducedMotion: true,
|
||||
highContrast: false,
|
||||
themeTokens: { "--background": "updated" },
|
||||
});
|
||||
host.notify("plugin.view.message", { viewId: "com.example.ui.view", message: { ping: true } });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.equal(pluginContext.environment.locale, "zh-CN");
|
||||
assert.equal(pluginContext.environment.reducedMotion, true);
|
||||
assert.deepEqual(pluginContext.environment.themeTokens, { "--background": "updated" });
|
||||
assert.deepEqual(events, [
|
||||
["settings", "com.example.ui.greeting"],
|
||||
["environment", "zh-CN", "dark", "updated"],
|
||||
["view", { ping: true }],
|
||||
]);
|
||||
assert.deepEqual(calls, [
|
||||
["setting", "com.example.ui.greeting", "hello"],
|
||||
["execute", "com.example.ui.hello", { nested: true }],
|
||||
["context", "com.example.ui.ready", true],
|
||||
["view-state", "com.example.ui.view", "window-1", { selected: 2 }],
|
||||
["view-message", "com.example.ui.view", { ready: true }],
|
||||
]);
|
||||
await peer.dispose();
|
||||
host.close();
|
||||
});
|
||||
|
||||
test("runtime peer registers provider handlers and routes immutable terminal lifecycle events", async () => {
|
||||
const { startPluginRuntime } = await import("./runtime/runtimePeer.mjs");
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const host = new PluginRpcRouter({
|
||||
pluginId: "com.example.provider",
|
||||
send(message) { port1.postMessage(message); },
|
||||
});
|
||||
port1.on("message", (message) => host.accept(message));
|
||||
const invocations = [];
|
||||
const terminalEvents = [];
|
||||
let cancellationObservedResolve;
|
||||
const cancellationObserved = new Promise((resolve) => {
|
||||
cancellationObservedResolve = resolve;
|
||||
});
|
||||
const peer = await startPluginRuntime({
|
||||
port: port2,
|
||||
config: {
|
||||
pluginId: "com.example.provider",
|
||||
pluginVersion: "1.0.0",
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
enabledFeatures: [],
|
||||
},
|
||||
async loadPlugin() {
|
||||
return {
|
||||
default: {
|
||||
async activate(context) {
|
||||
context.terminals.onDidChange((event) => terminalEvents.push(event));
|
||||
const staleDisposable = context.providers.register(
|
||||
"com.example.provider.completion",
|
||||
"terminal.completion",
|
||||
async () => "stale",
|
||||
);
|
||||
staleDisposable.dispose();
|
||||
context.providers.register(
|
||||
"com.example.provider.completion",
|
||||
"terminal.completion",
|
||||
async (invocation) => {
|
||||
invocations.push(invocation);
|
||||
if (invocation.operation === "wait") {
|
||||
await new Promise((resolve) => {
|
||||
invocation.cancellationToken.onCancellationRequested(() => {
|
||||
cancellationObservedResolve();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
return { items: [{ label: "status" }] };
|
||||
},
|
||||
);
|
||||
staleDisposable.dispose();
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await host.request("plugin.initialize", {
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
supportedFeatures: [],
|
||||
});
|
||||
await host.request("plugin.activate", {});
|
||||
|
||||
assert.deepEqual(await host.request("provider.invoke", {
|
||||
providerId: "com.example.provider.completion",
|
||||
kind: "terminal.completion",
|
||||
operation: "provide",
|
||||
requestId: "request-1",
|
||||
payload: { input: "sta" },
|
||||
deadlineMs: 1_000,
|
||||
}), {
|
||||
requestId: "request-1",
|
||||
status: "ok",
|
||||
result: { items: [{ label: "status" }] },
|
||||
});
|
||||
assert.equal(invocations.length, 1);
|
||||
assert.equal(invocations[0].providerId, "com.example.provider.completion");
|
||||
assert.equal(invocations[0].kind, "terminal.completion");
|
||||
assert.equal(invocations[0].operation, "provide");
|
||||
assert.equal(invocations[0].deadlineMs, 1_000);
|
||||
assert.equal(invocations[0].cancellationToken.isCancellationRequested, false);
|
||||
assert.equal(Object.isFrozen(invocations[0].payload), true);
|
||||
|
||||
await assert.rejects(
|
||||
host.request("provider.invoke", {
|
||||
providerId: "com.example.provider.completion",
|
||||
kind: "terminal.hover",
|
||||
operation: "provide",
|
||||
requestId: "request-kind-mismatch",
|
||||
}),
|
||||
(error) => error?.code === RPC_ERRORS.failedPrecondition,
|
||||
);
|
||||
|
||||
const event = {
|
||||
type: "cwdChanged",
|
||||
session: {
|
||||
sessionId: "session-1",
|
||||
protocol: "ssh",
|
||||
status: "connected",
|
||||
cwd: "/srv/app",
|
||||
},
|
||||
};
|
||||
host.notify("plugin.terminal.event", event);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.deepEqual(terminalEvents, [event]);
|
||||
assert.equal(Object.isFrozen(terminalEvents[0]), true);
|
||||
assert.equal(Object.isFrozen(terminalEvents[0].session), true);
|
||||
|
||||
const controller = new AbortController();
|
||||
const cancelledRequest = host.request("provider.invoke", {
|
||||
providerId: "com.example.provider.completion",
|
||||
kind: "terminal.completion",
|
||||
operation: "wait",
|
||||
requestId: "request-cancelled",
|
||||
}, { signal: controller.signal });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
controller.abort();
|
||||
await assert.rejects(cancelledRequest, (error) => error?.code === RPC_ERRORS.cancelled);
|
||||
await cancellationObserved;
|
||||
|
||||
await peer.dispose();
|
||||
host.close();
|
||||
});
|
||||
|
||||
test("runtime peer routes connection and importer Provider operation maps", async () => {
|
||||
const { startPluginRuntime } = await import("./runtime/runtimePeer.mjs");
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const host = new PluginRpcRouter({
|
||||
pluginId: "com.example.connection",
|
||||
send(message) { port1.postMessage(message); },
|
||||
});
|
||||
port1.on("message", (message) => host.accept(message));
|
||||
const invocations = [];
|
||||
let legacyError;
|
||||
let legacyImporterError;
|
||||
const peer = await startPluginRuntime({
|
||||
port: port2,
|
||||
config: {
|
||||
pluginId: "com.example.connection",
|
||||
pluginVersion: "1.0.0",
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
enabledFeatures: [],
|
||||
},
|
||||
async loadPlugin() {
|
||||
return {
|
||||
default: {
|
||||
async activate(context) {
|
||||
try {
|
||||
context.providers.register("com.example.connection.legacy", "connection", async () => null);
|
||||
} catch (error) {
|
||||
legacyError = { code: error?.code, message: error?.message };
|
||||
}
|
||||
context.providers.register("com.example.connection.transport", "connection", {
|
||||
validateConfiguration(invocation) {
|
||||
invocations.push([invocation.operation, invocation.payload.configuration]);
|
||||
return { valid: true, issues: [] };
|
||||
},
|
||||
probe(invocation) {
|
||||
invocations.push([invocation.operation, invocation.payload.configuration]);
|
||||
return { available: true };
|
||||
},
|
||||
open() {
|
||||
throw new Error("open is not invoked by this test");
|
||||
},
|
||||
resize(invocation) {
|
||||
invocations.push([invocation.operation, invocation.payload.columns, invocation.payload.rows]);
|
||||
return null;
|
||||
},
|
||||
signal(invocation) {
|
||||
invocations.push([invocation.operation, invocation.payload.signal]);
|
||||
return null;
|
||||
},
|
||||
reconnect(invocation) {
|
||||
invocations.push([invocation.operation, invocation.payload.connectionId]);
|
||||
return null;
|
||||
},
|
||||
close(invocation) {
|
||||
invocations.push([invocation.operation, invocation.payload.connectionId]);
|
||||
return null;
|
||||
},
|
||||
getStatus(invocation) {
|
||||
invocations.push([invocation.operation, invocation.payload.connectionId]);
|
||||
return { status: "connected" };
|
||||
},
|
||||
});
|
||||
try {
|
||||
context.providers.register("com.example.connection.legacyImporter", "importer", async () => null);
|
||||
} catch (error) {
|
||||
legacyImporterError = { code: error?.code, message: error?.message };
|
||||
}
|
||||
context.providers.register("com.example.connection.importer", "importer", {
|
||||
detect(invocation) {
|
||||
invocations.push([invocation.operation, invocation.payload.fileName]);
|
||||
return { confidence: 0.75, format: "example" };
|
||||
},
|
||||
parse() {
|
||||
throw new Error("parse is not invoked by this test");
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await host.request("plugin.initialize", {
|
||||
netcattyVersion: "1.0.0",
|
||||
apiVersion: "0.1.0-internal",
|
||||
supportedFeatures: [],
|
||||
});
|
||||
await host.request("plugin.activate", {});
|
||||
|
||||
assert.deepEqual(legacyError, {
|
||||
code: "invalid_argument",
|
||||
message: "Connection Provider handler must be an operation map",
|
||||
});
|
||||
assert.deepEqual(legacyImporterError, {
|
||||
code: "invalid_argument",
|
||||
message: "Importer Provider handler must be an operation map",
|
||||
});
|
||||
assert.deepEqual(await host.request("provider.invoke", {
|
||||
providerId: "com.example.connection.transport",
|
||||
kind: "connection",
|
||||
operation: "resize",
|
||||
requestId: "connection-resize-1",
|
||||
payload: { connectionId: "connection-1", operationId: "operation-1", columns: 132, rows: 44 },
|
||||
deadlineMs: 1_000,
|
||||
}), {
|
||||
requestId: "connection-resize-1",
|
||||
status: "ok",
|
||||
result: null,
|
||||
});
|
||||
assert.deepEqual(await host.request("provider.invoke", {
|
||||
providerId: "com.example.connection.importer",
|
||||
kind: "importer",
|
||||
operation: "detect",
|
||||
requestId: "importer-detect-1",
|
||||
payload: {
|
||||
fileName: "hosts.json",
|
||||
sample: { encoding: "base64", data: "e30=" },
|
||||
},
|
||||
deadlineMs: 1_000,
|
||||
}), {
|
||||
requestId: "importer-detect-1",
|
||||
status: "ok",
|
||||
result: { confidence: 0.75, format: "example" },
|
||||
});
|
||||
assert.deepEqual(await host.request("provider.invoke", {
|
||||
providerId: "com.example.connection.transport",
|
||||
kind: "connection",
|
||||
operation: "getStatus",
|
||||
requestId: "connection-status-1",
|
||||
payload: { connectionId: "connection-1", operationId: "operation-2" },
|
||||
deadlineMs: 1_000,
|
||||
}), {
|
||||
requestId: "connection-status-1",
|
||||
status: "ok",
|
||||
result: { status: "connected" },
|
||||
});
|
||||
assert.deepEqual(invocations, [
|
||||
["resize", 132, 44],
|
||||
["detect", "hosts.json"],
|
||||
["getStatus", "connection-1"],
|
||||
]);
|
||||
|
||||
await peer.dispose();
|
||||
host.close();
|
||||
});
|
||||
Reference in New Issue
Block a user