"use strict"; const assert = require("node:assert/strict"); const test = require("node:test"); const { createTerminalInterceptorEnvelope, } = require("../terminalInterceptorTransport.cjs"); class FakePort { constructor() { this.listeners = new Set(); this.messages = []; this.closed = false; } addEventListener(type, listener) { if (type === "message") this.listeners.add(listener); } removeEventListener(type, listener) { if (type === "message") this.listeners.delete(listener); } postMessage(message, transfer = []) { this.messages.push({ message, transfer }); } start() {} close() { this.closed = true; } emit(data, ports = []) { for (const listener of this.listeners) listener({ data, ports }); } } async function tick() { await new Promise((resolve) => setImmediate(resolve)); } test("utility runtime dispatches dedicated terminal ports to the exact registered interceptor", async () => { const { startPluginRuntime } = await import("./runtimePeer.mjs"); const control = new FakePort(); const runtime = await startPluginRuntime({ port: control, config: { pluginId: "com.example", pluginVersion: "1.0.0", netcattyVersion: "1.0.0", apiVersion: "1.0.0", enabledFeatures: [], environment: {}, entryUrl: "file:///plugin.js", }, loadPlugin: async () => ({ default: { activate(context) { context.providers.register( "com.example.input", "terminal.interceptor.input", ({ data }) => Uint8Array.from([...data].map((byte) => byte >= 97 && byte <= 122 ? byte - 32 : byte)), ); }, }, }), }); control.emit({ jsonrpc: "2.0", id: 1, method: "plugin.initialize", params: {} }); await tick(); control.emit({ jsonrpc: "2.0", id: 2, method: "plugin.activate", params: {} }); await tick(); const dataPort = new FakePort(); control.emit({ jsonrpc: "2.0", id: 3, method: "plugin.terminal.interceptor.attach", params: { descriptor: { providerId: "com.example.input", direction: "input", session: { sessionId: "session-1", protocol: "ssh", status: "connected" }, }, }, }, [dataPort]); await tick(); assert.equal( control.messages.some(({ message }) => ( message.jsonrpc === "2.0" && message.id === 3 && message.result?.accepted === true )), true, ); dataPort.emit(createTerminalInterceptorEnvelope({ type: "netcatty:terminal-interceptor:ready", sessionId: "session-1", direction: "input", windowBytes: 64 * 1024, })); assert.equal(dataPort.closed, false); const data = Uint8Array.from(Buffer.from("hello")).buffer; dataPort.emit(createTerminalInterceptorEnvelope({ type: "netcatty:terminal-interceptor:chunk", sequence: 1, direction: "input", creditBytes: 64 * 1024, byteLength: data.byteLength, }, data)); await tick(); assert.equal(dataPort.messages.length, 1); const result = createTerminalInterceptorEnvelope( dataPort.messages[0].message.frame, dataPort.messages[0].message.transfer, ); assert.equal(result.frame.status, "ok"); assert.equal(result.frame.creditBytes, 5); assert.equal(Buffer.from(result.transfer).toString("utf8"), "HELLO"); assert.deepEqual(dataPort.messages[0].transfer, [result.transfer]); await runtime.dispose(); }); test("utility runtime closes a terminal port when provider ownership or kind is invalid", async () => { const { startPluginRuntime } = await import("./runtimePeer.mjs"); const control = new FakePort(); const runtime = await startPluginRuntime({ port: control, config: { pluginId: "com.example", pluginVersion: "1.0.0", netcattyVersion: "1.0.0", apiVersion: "1.0.0", enabledFeatures: [], environment: {}, entryUrl: "file:///plugin.js", }, loadPlugin: async () => ({ default: { activate() {} } }), }); control.emit({ jsonrpc: "2.0", id: 1, method: "plugin.initialize", params: {} }); await tick(); control.emit({ jsonrpc: "2.0", id: 2, method: "plugin.activate", params: {} }); await tick(); const dataPort = new FakePort(); control.emit({ jsonrpc: "2.0", id: 3, method: "plugin.terminal.interceptor.attach", params: { descriptor: { providerId: "com.example.missing", direction: "input", session: { sessionId: "session-1" }, }, }, }, [dataPort]); await tick(); assert.equal(dataPort.closed, true); assert.equal( control.messages.some(({ message }) => ( message.jsonrpc === "2.0" && message.id === 3 && message.error?.code === -32009 )), true, ); await runtime.dispose(); }); test("utility runtime closes the transferred terminal port when the provider belongs to another plugin", async () => { const { startPluginRuntime } = await import("./runtimePeer.mjs"); const control = new FakePort(); const runtime = await startPluginRuntime({ port: control, config: { pluginId: "com.example", pluginVersion: "1.0.0", netcattyVersion: "1.0.0", apiVersion: "1.0.0", enabledFeatures: [], environment: {}, entryUrl: "file:///plugin.js", }, loadPlugin: async () => ({ default: { activate() {} } }), }); control.emit({ jsonrpc: "2.0", id: 1, method: "plugin.initialize", params: {} }); await tick(); control.emit({ jsonrpc: "2.0", id: 2, method: "plugin.activate", params: {} }); await tick(); const dataPort = new FakePort(); control.emit({ jsonrpc: "2.0", id: 3, method: "plugin.terminal.interceptor.attach", params: { descriptor: { providerId: "other.plugin.input", direction: "input", session: { sessionId: "session-1", protocol: "ssh", status: "connected" }, }, }, }, [dataPort]); await tick(); assert.equal(dataPort.closed, true); assert.equal( control.messages.some(({ message }) => ( message.jsonrpc === "2.0" && message.id === 3 && message.error?.code === -32003 )), true, ); await runtime.dispose(); }); test("utility runtime closes unexpected transferred ports on every lifecycle request", async () => { const { startPluginRuntime } = await import("./runtimePeer.mjs"); const control = new FakePort(); const runtime = await startPluginRuntime({ port: control, config: { pluginId: "com.example", pluginVersion: "1.0.0", netcattyVersion: "1.0.0", apiVersion: "1.0.0", enabledFeatures: [], environment: {}, entryUrl: "file:///plugin.js", }, loadPlugin: async () => ({ default: { activate() {} } }), }); const lifecycleRequests = [ { id: 1, method: "plugin.initialize", params: {} }, { id: 2, method: "plugin.activate", params: {} }, { id: 3, method: "plugin.deactivate", params: {} }, ]; for (const request of lifecycleRequests) { const unexpectedPort = new FakePort(); control.emit({ jsonrpc: "2.0", ...request }, [unexpectedPort]); await tick(); assert.equal(unexpectedPort.closed, true, `${request.method} retained an unexpected port`); assert.equal( control.messages.some(({ message }) => ( message.jsonrpc === "2.0" && message.id === request.id && Object.hasOwn(message, "result") )), true, `${request.method} did not complete after closing its unexpected port`, ); } await runtime.dispose(); }); test("utility runtime rejects the retired private terminal attachment protocol", async () => { const { startPluginRuntime } = await import("./runtimePeer.mjs"); const control = new FakePort(); const runtime = await startPluginRuntime({ port: control, config: { pluginId: "com.example", pluginVersion: "1.0.0", netcattyVersion: "1.0.0", apiVersion: "1.0.0", enabledFeatures: [], environment: {}, entryUrl: "file:///plugin.js", }, loadPlugin: async () => ({ default: { activate() {} } }), }); const dataPort = new FakePort(); control.emit({ type: "netcatty-plugin:terminal-interceptor:attach", attachmentId: 1, descriptor: { providerId: "com.example.input", direction: "input" }, }, [dataPort]); assert.equal(dataPort.closed, true); assert.equal(control.messages.length, 0); await runtime.dispose(); }); test("terminal ports convert synchronous throws to failures and stop using disposed handlers", async () => { const { startPluginRuntime } = await import("./runtimePeer.mjs"); const control = new FakePort(); let registration; let calls = 0; const runtime = await startPluginRuntime({ port: control, config: { pluginId: "com.example", pluginVersion: "1.0.0", netcattyVersion: "1.0.0", apiVersion: "1.0.0", enabledFeatures: [], environment: {}, entryUrl: "file:///plugin.js", }, loadPlugin: async () => ({ default: { activate(context) { registration = context.providers.register( "com.example.input", "terminal.interceptor.input", () => { calls += 1; throw new Error("synchronous failure"); }, ); }, }, }), }); control.emit({ jsonrpc: "2.0", id: 1, method: "plugin.initialize", params: {} }); await tick(); control.emit({ jsonrpc: "2.0", id: 2, method: "plugin.activate", params: {} }); await tick(); const dataPort = new FakePort(); control.emit({ jsonrpc: "2.0", id: 3, method: "plugin.terminal.interceptor.attach", params: { descriptor: { providerId: "com.example.input", direction: "input", session: { sessionId: "session-1", protocol: "ssh", status: "connected" }, }, }, }, [dataPort]); await tick(); const send = (sequence) => { const data = Uint8Array.from([sequence]).buffer; dataPort.emit(createTerminalInterceptorEnvelope({ type: "netcatty:terminal-interceptor:chunk", sequence, direction: "input", creditBytes: 64 * 1024, byteLength: data.byteLength, }, data)); }; send(1); await tick(); assert.equal(dataPort.messages[0].message.frame.status, "failed"); assert.equal(calls, 1); registration.dispose(); send(2); await tick(); assert.equal(dataPort.messages[1].message.frame.status, "failed"); assert.equal(calls, 1); await runtime.dispose(); }); test("the utility peer closes a port whose chunk bypasses the canonical terminal frame schema", async () => { const { startPluginRuntime } = await import("./runtimePeer.mjs"); const control = new FakePort(); const runtime = await startPluginRuntime({ port: control, config: { pluginId: "com.example", pluginVersion: "1.0.0", netcattyVersion: "1.0.0", apiVersion: "1.0.0", enabledFeatures: [], environment: {}, entryUrl: "file:///plugin.js", }, loadPlugin: async () => ({ default: { activate(context) { context.providers.register( "com.example.input", "terminal.interceptor.input", ({ data }) => data, ); }, }, }), }); control.emit({ jsonrpc: "2.0", id: 1, method: "plugin.initialize", params: {} }); await tick(); control.emit({ jsonrpc: "2.0", id: 2, method: "plugin.activate", params: {} }); await tick(); const dataPort = new FakePort(); control.emit({ jsonrpc: "2.0", id: 3, method: "plugin.terminal.interceptor.attach", params: { descriptor: { providerId: "com.example.input", direction: "input", session: { sessionId: "session-1", protocol: "ssh", status: "connected" }, }, }, }, [dataPort]); await tick(); const data = Uint8Array.from(Buffer.from("unsafe")).buffer; dataPort.emit({ frame: { type: "netcatty:terminal-interceptor:chunk", sequence: 1, direction: "input", creditBytes: 64 * 1024, byteLength: data.byteLength, extra: true, }, transfer: data, }); assert.equal(dataPort.closed, true); assert.equal(dataPort.messages.length, 0); await runtime.dispose(); }); test("utility runtime disposal closes a terminal port while its transport helper is loading", async () => { const { startPluginRuntime } = await import("./runtimePeer.mjs"); const control = new FakePort(); let resolveTransport; const transportLoading = new Promise((resolve) => { resolveTransport = resolve; }); const runtime = await startPluginRuntime({ port: control, config: { pluginId: "com.example", pluginVersion: "1.0.0", netcattyVersion: "1.0.0", apiVersion: "1.0.0", enabledFeatures: [], environment: {}, entryUrl: "file:///plugin.js", }, loadPlugin: async () => ({ default: { activate(context) { context.providers.register( "com.example.input", "terminal.interceptor.input", ({ data }) => data, ); }, }, }), loadTerminalInterceptorTransport: () => transportLoading, }); control.emit({ jsonrpc: "2.0", id: 1, method: "plugin.initialize", params: {} }); await tick(); control.emit({ jsonrpc: "2.0", id: 2, method: "plugin.activate", params: {} }); await tick(); const dataPort = new FakePort(); control.emit({ jsonrpc: "2.0", id: 3, method: "plugin.terminal.interceptor.attach", params: { descriptor: { providerId: "com.example.input", direction: "input", session: { sessionId: "session-1", protocol: "ssh", status: "connected" }, }, }, }, [dataPort]); await tick(); assert.equal(dataPort.closed, false); await runtime.dispose(); assert.equal(dataPort.closed, true); resolveTransport({ TERMINAL_INTERCEPTOR_MAX_CHUNK_BYTES: 64 * 1024, createTerminalInterceptorEnvelope, }); await tick(); assert.equal( control.messages.some(({ message }) => message.id === 3 && message.result?.accepted === true), false, ); assert.equal(dataPort.closed, true); });