Files
NetMesh/electron/plugins/pluginBridge.test.cjs

1602 lines
58 KiB
JavaScript
Raw Normal View History

"use strict";
const assert = require("node:assert/strict");
const { mkdtemp, rename, rm, writeFile } = require("node:fs/promises");
const { tmpdir } = require("node:os");
const { join } = require("node:path");
const test = require("node:test");
const {
CHANNELS,
createTrustedPluginBridgeSender,
registerPluginBridge,
normalizePluginScopeCatalog,
} = require("./pluginBridge.cjs");
function createIpcMain() {
const handlers = new Map();
const listeners = new Map();
return {
handlers,
listeners,
handle(channel, handler) { handlers.set(channel, handler); },
on(channel, listener) {
const list = listeners.get(channel) ?? [];
list.push(listener);
listeners.set(channel, list);
},
};
}
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("disabled plugin runtime keeps passive host integration channels quiet", async () => {
const ipcMain = createIpcMain();
registerPluginBridge(ipcMain, {
manager: null,
env: {},
isTrustedSender: () => true,
});
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.terminalProviders)({}, {}), []);
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.terminalSessionEvent)({}, {}), []);
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.extensionProviders)({}, {}), []);
assert.equal(await ipcMain.handlers.get(CHANNELS.credentialCatalogUpdate)({}, { entries: [] }), 0);
assert.equal(await ipcMain.handlers.get(CHANNELS.setScopeCatalog)({}, {}), null);
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.getScopeCatalog)({}, {}), {
workspace: [],
host: [],
session: [],
device: [{ id: "device", label: "This device" }],
});
});
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 Vault credential catalog updates only through the trusted host bridge", async () => {
const ipcMain = createIpcMain();
const updates = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
credentialResolver: {
update(entries) { updates.push(entries); return entries.length; },
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: (event) => event?.trusted === true,
});
const entries = [{ id: "credential-reference-0001", ciphertext: "enc:v1:Y2lwaGVy" }];
assert.equal(await ipcMain.handlers.get(CHANNELS.credentialCatalogUpdate)(
{ trusted: true },
{ entries },
), 1);
assert.deepEqual(updates, [entries]);
await assert.rejects(
ipcMain.handlers.get(CHANNELS.credentialCatalogUpdate)({ trusted: false }, { entries }),
/Untrusted/i,
);
});
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);
});
test("plugin view host closures are broadcast to renderer windows", async () => {
const ipcMain = createIpcMain();
const broadcasts = [];
let closeListener;
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
contributionService: {},
viewHost: {
onDidClose(listener) { closeListener = listener; return { dispose() {} }; },
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
broadcast: (...args) => broadcasts.push(args),
});
const event = {
instanceId: "view-1",
pluginId: "com.example.view",
viewId: "com.example.view.panel",
reason: "runtime-error",
};
closeListener(event);
assert.deepEqual(broadcasts, [[CHANNELS.viewClosed, event]]);
});
test("plugin contribution icon requests use the host-owned resolver", async () => {
const ipcMain = createIpcMain();
const calls = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
resolveContributionIcon: async (payload) => {
calls.push(payload);
return { light: "data:image/png;base64,bGlnaHQ=" };
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const payload = {
pluginId: "com.example.icon",
icon: { kind: "package", light: "assets/icon.png" },
};
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.contributionIcon)({}, payload), {
light: "data:image/png;base64,bGlnaHQ=",
});
assert.deepEqual(calls, [payload]);
});
test("plugin setting scope catalogs are bounded, sender-owned, and merged for settings windows", async () => {
assert.deepEqual(normalizePluginScopeCatalog({
host: [{ id: "host-1", label: "Production" }, { id: "host-1", label: "Duplicate" }],
workspace: [{ id: "", label: "Invalid" }],
}), {
workspace: [],
host: [{ id: "host-1", label: "Production" }],
session: [],
device: [],
});
const ipcMain = createIpcMain();
const broadcasts = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
broadcast: (...args) => broadcasts.push(args),
});
let firstDestroyed;
const first = {
sender: {
id: 1,
once: (event, listener) => { if (event === "destroyed") firstDestroyed = listener; },
},
};
const second = { sender: { id: 2, once() {} } };
const settingsWindow = { sender: { id: 3, once() {} } };
const next = { host: [{ id: "host-1", label: "Production" }] };
await ipcMain.handlers.get(CHANNELS.setScopeCatalog)(first, next);
await ipcMain.handlers.get(CHANNELS.setScopeCatalog)(second, {
workspace: [{ id: "workspace-2", label: "Second window" }],
host: [{ id: "host-1", label: "Duplicate from second window" }],
});
const merged = {
workspace: [{ id: "workspace-2", label: "Second window" }],
host: [{ id: "host-1", label: "Production" }],
session: [],
device: [{ id: "device", label: "This device" }],
};
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.getScopeCatalog)(first), merged);
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.getScopeCatalog)(second), merged);
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.getScopeCatalog)(settingsWindow), merged);
assert.deepEqual(broadcasts.at(-1), [CHANNELS.scopeCatalogChanged, merged]);
firstDestroyed();
const afterFirstWindowClosed = {
workspace: [{ id: "workspace-2", label: "Second window" }],
host: [{ id: "host-1", label: "Duplicate from second window" }],
session: [],
device: [{ id: "device", label: "This device" }],
};
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.getScopeCatalog)(settingsWindow), afterFirstWindowClosed);
assert.deepEqual(broadcasts.at(-1), [CHANNELS.scopeCatalogChanged, afterFirstWindowClosed]);
});
test("plugin terminal Provider bridge owns cancellation by renderer sender", async () => {
const ipcMain = createIpcMain();
const calls = [];
const pipelineCalls = [];
let destroyed;
const terminalProviderService = {
listProviders(options) {
calls.push(["list", options]);
return [{ provider: { id: "com.example.completion" } }];
},
async provide(request, options) {
calls.push(["provide", request]);
return new Promise((resolve) => {
options.signal.addEventListener("abort", () => resolve([{ status: "cancelled" }]), { once: true });
});
},
async publishSessionEvent(event) {
calls.push(["event", event]);
return [{ pluginId: "com.example", delivered: true }];
},
};
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
terminalProviderService,
terminalDataPipelineService: {
async handleSessionEvent(payload, options) {
pipelineCalls.push([payload, options]);
return [{ direction: "input", attached: true }];
},
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = {
sender: {
id: 42,
once(name, listener) { if (name === "destroyed") destroyed = listener; },
},
};
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.terminalProviders)(event, {
kind: "terminal.completion",
}), [{ provider: { id: "com.example.completion" } }]);
const pending = ipcMain.handlers.get(CHANNELS.terminalProvide)(event, {
requestId: "renderer-request-1",
kind: "terminal.completion",
operation: "provideCompletions",
session: { sessionId: "session-1" },
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(await ipcMain.handlers.get(CHANNELS.terminalCancel)(event, {
requestId: "renderer-request-1",
}), true);
assert.deepEqual(await pending, [{ status: "cancelled" }]);
assert.equal(await ipcMain.handlers.get(CHANNELS.terminalCancel)(event, {
requestId: "renderer-request-1",
}), false);
assert.deepEqual(await ipcMain.handlers.get(CHANNELS.terminalSessionEvent)(event, {
type: "created",
session: { sessionId: "session-1" },
}), [{ pluginId: "com.example", delivered: true }]);
assert.deepEqual(pipelineCalls, [[
{ type: "created", session: { sessionId: "session-1" } },
{ webContentsId: 42 },
]]);
assert.equal(typeof destroyed, "function");
const destroyedPending = ipcMain.handlers.get(CHANNELS.terminalProvide)(event, {
requestId: "renderer-request-destroyed",
kind: "terminal.completion",
operation: "provideCompletions",
session: { sessionId: "session-1" },
});
await new Promise((resolve) => setImmediate(resolve));
destroyed();
assert.deepEqual(await destroyedPending, [{ status: "cancelled" }]);
assert.deepEqual(calls, [
["list", { kind: "terminal.completion" }],
["provide", {
kind: "terminal.completion",
operation: "provideCompletions",
session: { sessionId: "session-1" },
}],
["event", { type: "created", session: { sessionId: "session-1" } }],
["provide", {
kind: "terminal.completion",
operation: "provideCompletions",
session: { sessionId: "session-1" },
}],
]);
});
test("plugin terminal Provider bridge releases cancellation during host initialization", async () => {
const ipcMain = createIpcMain();
let releaseInitialization;
let initializationStarted;
const started = new Promise((resolve) => { initializationStarted = resolve; });
const initialization = new Promise((resolve) => { releaseInitialization = resolve; });
const observedSignals = [];
registerPluginBridge(ipcMain, {
manager: {
async initialize() {
initializationStarted();
await initialization;
},
},
terminalProviderService: {
async provide(_request, options) {
observedSignals.push(options.signal);
return [{ status: options.signal.aborted ? "cancelled" : "ok" }];
},
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 84, once() {} } };
const pending = ipcMain.handlers.get(CHANNELS.terminalProvide)(event, {
requestId: "cancel-during-initialize",
kind: "terminal.completion",
operation: "provideCompletions",
session: { sessionId: "session-1" },
});
await started;
assert.equal(await ipcMain.handlers.get(CHANNELS.terminalCancel)(event, {
requestId: "cancel-during-initialize",
}), true);
await assert.rejects(pending, (error) => error?.name === "AbortError");
assert.equal(observedSignals.length, 0);
releaseInitialization();
const retry = await ipcMain.handlers.get(CHANNELS.terminalProvide)(event, {
requestId: "after-cancelled-initialize",
kind: "terminal.completion",
operation: "provideCompletions",
session: { sessionId: "session-1" },
});
assert.deepEqual(retry, [{ status: "ok" }]);
assert.equal(observedSignals.length, 1);
assert.equal(observedSignals[0].aborted, false);
});
test("plugin extension Provider requests are cancellable and sender-owned", async () => {
const ipcMain = createIpcMain();
let destroyed;
const calls = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async invoke(payload, options) {
calls.push(payload);
return new Promise((resolve) => {
options.signal.addEventListener("abort", () => resolve({ cancelled: true }), { once: true });
});
},
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = {
sender: {
id: 73,
once(name, listener) { if (name === "destroyed") destroyed = listener; },
},
};
const pending = ipcMain.handlers.get(CHANNELS.extensionInvoke)(event, {
requestId: "extension-request-1",
providerId: "com.example.transport.connection",
kind: "connection",
operation: "probe",
payload: {},
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(await ipcMain.handlers.get(CHANNELS.extensionCancel)(event, {
requestId: "extension-request-1",
}), true);
assert.deepEqual(await pending, { cancelled: true });
assert.equal(await ipcMain.handlers.get(CHANNELS.extensionCancel)(event, {
requestId: "extension-request-1",
}), false);
assert.equal(typeof destroyed, "function");
assert.equal(calls.length, 1);
});
test("generic extension invocation cannot bypass authentication or connection-session ownership", async () => {
const ipcMain = createIpcMain();
let calls = 0;
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async invoke() { calls += 1; return null; },
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const invoke = ipcMain.handlers.get(CHANNELS.extensionInvoke);
const event = { sender: { once() {}, id: 41 } };
await assert.rejects(invoke(event, {
requestId: "auth-bypass",
kind: "authentication",
providerId: "com.example.auth.provider",
operation: "respond",
payload: { response: "plaintext" },
}), /dedicated host workflow/i);
await assert.rejects(invoke(event, {
requestId: "connection-bypass",
kind: "connection",
providerId: "com.example.connection.provider",
operation: "close",
payload: { connectionId: "not-owned" },
}), /dedicated host workflow/i);
assert.equal(calls, 0);
});
test("plugin importer files use sender-owned native selections and bounded streaming", async (context) => {
const directory = await mkdtemp(join(tmpdir(), "netcatty-plugin-importer-"));
context.after(() => rm(directory, { recursive: true, force: true }));
const filePath = join(directory, "hosts.json");
await writeFile(filePath, "streamed-import");
const parsed = [];
const ipcMain = createIpcMain();
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async parseImporter(params, options) {
for await (const chunk of params.source) parsed.push(Buffer.from(chunk));
assert.equal(params.sourceByteLength, 15);
assert.equal(params.fileName, "hosts.json");
options.onProgress({ type: "progress", completed: 1, total: 2, message: "Reading" });
return { providerId: params.providerId, result: { parsed: 0, warnings: 0, errors: 0 }, records: [] };
},
},
selectImporterFile: async () => filePath,
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const progressEvents = [];
const first = { sender: {
id: 1,
once() {},
isDestroyed: () => false,
send(channel, payload) { progressEvents.push([channel, payload]); },
} };
const second = { sender: { id: 2, once() {} } };
const selection = await ipcMain.handlers.get(CHANNELS.importerSelectFile)(first, {});
assert.equal(selection.fileName, "hosts.json");
assert.equal(Buffer.from(selection.sample).toString(), "streamed-import");
await assert.rejects(ipcMain.handlers.get(CHANNELS.importerParseFile)(second, {
requestId: "other-window",
providerId: "com.example.importer",
selectionToken: selection.selectionToken,
}), /selection expired/i);
await rename(filePath, `${filePath}.selected`);
await writeFile(filePath, "path-replaced!!");
const preview = await ipcMain.handlers.get(CHANNELS.importerParseFile)(first, {
requestId: "owner-window",
providerId: "com.example.importer",
selectionToken: selection.selectionToken,
});
assert.equal(preview.providerId, "com.example.importer");
assert.equal(Buffer.concat(parsed).toString(), "streamed-import");
assert.deepEqual(progressEvents, [[CHANNELS.importerProgress, {
requestId: "owner-window",
providerId: "com.example.importer",
progress: { type: "progress", completed: 1, total: 2, message: "Reading" },
}]]);
await assert.rejects(ipcMain.handlers.get(CHANNELS.importerParseFile)(first, {
requestId: "replay",
providerId: "com.example.importer",
selectionToken: selection.selectionToken,
}), /selection expired/i);
});
test("plugin connection authentication uses host-rendered sender-owned challenges", async () => {
const ipcMain = createIpcMain();
const sent = [];
const calls = [];
const external = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async authenticate(params, requestChallenge) {
calls.push(["authenticate", params]);
const response = await requestChallenge({
id: "password-1",
kind: "password",
title: "Password",
});
calls.push(["response", response]);
return {
status: "authenticated",
credential: { kind: "credential", id: "credential-after-auth" },
};
},
async openConnection(params, options) {
calls.push(["open", params]);
await options.onData(Uint8Array.from([0xe2]));
await options.onData(Uint8Array.from([0x82, 0xac]));
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { external.push(["start", options]); return { sessionId: options.sessionId }; },
async pushExternalOutput(sessionId, data) { external.push(["output", sessionId, data]); },
async finishExternalSession(sessionId, details) { external.push(["finish", sessionId, details]); },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = {
sender: {
id: 74,
once() {},
isDestroyed: () => false,
send(channel, payload) { sent.push([channel, payload]); },
},
};
const pending = ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-auth-1",
sessionId: "session-auth-1",
providerId: "com.example.transport.connection",
authenticationProviderId: "com.example.transport.authentication",
configuration: { host: "example.test" },
hostLabel: "Example transport",
hostname: "example.test",
columns: 80,
rows: 24,
sessionLog: {
enabled: true,
directory: "/logs",
format: "html",
timestampsEnabled: true,
},
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(sent.length, 1);
assert.equal(sent[0][0], CHANNELS.authenticationChallenge);
const challengeEvent = sent[0][1];
await assert.rejects(
ipcMain.handlers.get(CHANNELS.authenticationRespond)({ sender: { id: 75, once() {} } }, {
...challengeEvent,
challengeId: challengeEvent.challenge.id,
response: "stolen",
}),
/not owned/,
);
await ipcMain.handlers.get(CHANNELS.authenticationRespond)(event, {
requestId: challengeEvent.requestId,
challengeRequestId: challengeEvent.challengeRequestId,
challengeId: challengeEvent.challenge.id,
response: "secret answer",
});
assert.deepEqual(await pending, {
sessionId: "session-auth-1",
providerId: "com.example.transport.connection",
status: "connected",
diagnostics: [],
});
assert.deepEqual(calls[0], ["authenticate", {
providerId: "com.example.transport.authentication",
connectionProviderId: "com.example.transport.connection",
configuration: { host: "example.test" },
}]);
assert.deepEqual(calls[1], ["response", "secret answer"]);
assert.deepEqual(calls[2][0], "open");
assert.deepEqual(calls[2][1].credential, {
kind: "credential",
id: "credential-after-auth",
});
assert.equal(external[0][0], "start");
assert.deepEqual({
sessionId: external[0][1].sessionId,
hostLabel: external[0][1].hostLabel,
hostname: external[0][1].hostname,
sessionLog: external[0][1].sessionLog,
}, {
sessionId: "session-auth-1",
hostLabel: "Example transport",
hostname: "example.test",
sessionLog: {
enabled: true,
directory: "/logs",
format: "html",
timestampsEnabled: true,
},
});
assert.deepEqual(external[1], ["output", "session-auth-1", "€"]);
});
test("plugin authentication cancellation removes the queued renderer challenge", async () => {
const ipcMain = createIpcMain();
const sent = [];
const external = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async authenticate(_params, requestChallenge) {
await requestChallenge({
id: "password-cancel",
kind: "password",
title: "Password",
});
throw new Error("authentication should not continue after cancellation");
},
async openConnection() {
throw new Error("connection should not open after cancellation");
},
closeSessionLocal(sessionId) { external.push(["close-local", sessionId]); },
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { external.push(["start", options.sessionId]); return { sessionId: options.sessionId }; },
async finishExternalSession(sessionId, details) { external.push(["finish", sessionId, details.reason]); },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = {
sender: {
id: 76,
once() {},
isDestroyed: () => false,
send(channel, payload) { sent.push([channel, payload]); },
},
};
const pending = ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-auth-cancel-1",
sessionId: "session-auth-cancel-1",
providerId: "com.example.transport.connection",
authenticationProviderId: "com.example.transport.authentication",
configuration: { host: "example.test" },
hostLabel: "Example transport",
hostname: "example.test",
columns: 80,
rows: 24,
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(sent.length, 1);
assert.equal(sent[0][0], CHANNELS.authenticationChallenge);
const challengeEvent = sent[0][1];
assert.equal(await ipcMain.handlers.get(CHANNELS.extensionCancel)(event, {
requestId: "connection-auth-cancel-1",
}), true);
await assert.rejects(pending, /abort|cancel/i);
assert.deepEqual(sent[1], [CHANNELS.authenticationChallenge, {
requestId: "connection-auth-cancel-1",
challengeRequestId: challengeEvent.challengeRequestId,
challengeId: "password-cancel",
cancelled: true,
}]);
assert.deepEqual(external.at(-1), ["finish", "session-auth-cancel-1", "error"]);
});
test("plugin connection status monitoring releases readiness then keeps polling later failures", async () => {
const ipcMain = createIpcMain();
const statusCalls = [];
let resolveConnectedOutput;
const connectedOutput = new Promise((resolve) => { resolveConnectedOutput = resolve; });
let resolveFinished;
const finished = new Promise((resolve) => { resolveFinished = resolve; });
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params) {
return { sessionId: params.sessionId, providerId: params.providerId, status: "connecting", diagnostics: [] };
},
async control(sessionId, operation, payload, options) {
statusCalls.push([sessionId, operation, payload, options.signal.aborted]);
if (statusCalls.length === 1) return { status: "connecting" };
if (statusCalls.length === 2) return { status: "connected" };
return {
status: "error",
message: "Link lost after connect",
diagnostics: [{ severity: "warning", message: "Provider reported reconnect exhaustion" }],
};
},
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { return { sessionId: options.sessionId }; },
async pushExternalOutput(sessionId, data, meta) { resolveConnectedOutput([sessionId, data, meta]); },
async finishExternalSession(sessionId, details) { resolveFinished([sessionId, details]); return true; },
}),
connectionStatusPollMs: 0,
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 77, once() {}, isDestroyed: () => false } };
const opened = await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-silent-start",
sessionId: "session-silent-start",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
assert.equal(opened.status, "connecting");
assert.deepEqual(await connectedOutput, [
"session-silent-start",
"",
{ pluginPipelineIngressBytes: 0, pluginConnectionReady: true },
]);
assert.deepEqual(await finished, [
"session-silent-start",
{
reason: "error",
error: "Link lost after connect",
diagnostics: [{ severity: "warning", message: "Provider reported reconnect exhaustion" }],
},
]);
assert.equal(statusCalls.length, 3);
assert.equal(statusCalls.every((call) => call[1] === "getStatus" && call[3] === false), true);
});
test("plugin connections that open connected release silent terminal startup", async () => {
const ipcMain = createIpcMain();
const output = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params) {
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { return { sessionId: options.sessionId }; },
async pushExternalOutput(sessionId, data, meta) { output.push([sessionId, data, meta]); return true; },
async finishExternalSession() { return true; },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 79, once() {}, isDestroyed: () => false } };
const opened = await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-silent-connected",
sessionId: "session-silent-connected",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
assert.equal(opened.status, "connected");
assert.deepEqual(output, [[
"session-silent-connected",
"",
{ pluginPipelineIngressBytes: 0, pluginConnectionReady: true },
]]);
});
test("plugin connections that open connected continue status monitoring", async () => {
const ipcMain = createIpcMain();
const statusCalls = [];
let resolveFinished;
const finished = new Promise((resolve) => { resolveFinished = resolve; });
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params) {
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
async control(sessionId, operation) {
statusCalls.push([sessionId, operation]);
return { status: "closed", message: "Provider closed" };
},
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { return { sessionId: options.sessionId }; },
async pushExternalOutput() { return true; },
async finishExternalSession(sessionId, details) { resolveFinished([sessionId, details]); return true; },
}),
connectionStatusPollMs: 0,
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 81, once() {}, isDestroyed: () => false } };
const opened = await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-connected-monitored",
sessionId: "session-connected-monitored",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
assert.equal(opened.status, "connected");
assert.deepEqual(await finished, [
"session-connected-monitored",
{ reason: "closed", error: "Provider closed" },
]);
assert.deepEqual(statusCalls, [["session-connected-monitored", "getStatus"]]);
});
test("an explicit slow close stops status monitoring before the Provider responds", async () => {
const ipcMain = createIpcMain();
let releaseClose;
const closeGate = new Promise((resolve) => { releaseClose = resolve; });
let resolveCloseStarted;
const closeStarted = new Promise((resolve) => { resolveCloseStarted = resolve; });
const statusCalls = [];
const finished = [];
const externalSessions = new Map();
const owners = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params) {
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
async control(sessionId, operation) {
if (operation === "close") {
resolveCloseStarted();
await closeGate;
return null;
}
statusCalls.push([sessionId, operation]);
throw new Error("Plugin connection session was not found");
},
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) {
if (externalSessions.has(options.sessionId)) throw new Error("external session already registered");
owners.push(options.ownerToken);
externalSessions.set(options.sessionId, options.ownerToken);
return { sessionId: options.sessionId };
},
async pushExternalOutput() { return true; },
async finishExternalSession(sessionId, details, owner) {
if (externalSessions.get(sessionId) !== owner) return false;
externalSessions.delete(sessionId);
finished.push([sessionId, details, owner]);
return true;
},
}),
connectionStatusPollMs: 5,
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 89, once() {}, isDestroyed: () => false } };
await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-explicit-slow-close",
sessionId: "session-explicit-slow-close",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
const closing = ipcMain.handlers.get(CHANNELS.connectionControl)(event, {
sessionId: "session-explicit-slow-close",
operation: "close",
payload: {},
});
await closeStarted;
await new Promise((resolve) => setTimeout(resolve, 20));
assert.deepEqual(statusCalls, []);
assert.deepEqual(finished, [[
"session-explicit-slow-close",
{ reason: "closed" },
owners[0],
]]);
await assert.rejects(ipcMain.handlers.get(CHANNELS.connectionWrite)(event, {
sessionId: "session-explicit-slow-close",
data: "late input",
}), /not owned/i);
await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-explicit-slow-close-replacement",
sessionId: "session-explicit-slow-close",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
assert.notEqual(owners[1], owners[0]);
assert.equal(externalSessions.get("session-explicit-slow-close"), owners[1]);
releaseClose();
assert.equal(await closing, null);
assert.equal(externalSessions.get("session-explicit-slow-close"), owners[1]);
});
test("silent connection readiness delivery failure closes the opened provider session", async () => {
const ipcMain = createIpcMain();
const controls = [];
const finished = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params) {
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
async control(...args) { controls.push(args); return null; },
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { return { sessionId: options.sessionId }; },
async pushExternalOutput() { throw new Error("renderer unavailable"); },
async finishExternalSession(sessionId, details) { finished.push([sessionId, details]); return true; },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 80, once() {}, isDestroyed: () => false } };
await assert.rejects(
ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-readiness-failure",
sessionId: "session-readiness-failure",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
}),
/renderer unavailable/,
);
assert.deepEqual(controls.map(([sessionId, operation]) => [sessionId, operation]), [
["session-readiness-failure", "close"],
]);
assert.deepEqual(finished, [[
"session-readiness-failure",
{ reason: "error", error: "renderer unavailable" },
]]);
});
test("plugin connection status monitoring closes asynchronous provider errors", async () => {
const ipcMain = createIpcMain();
const closed = [];
const externalSessions = new Map();
const finishAttempts = [];
let providerOptions;
let resolveFinished;
const finished = new Promise((resolve) => { resolveFinished = resolve; });
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params, options) {
providerOptions = options;
return { sessionId: params.sessionId, providerId: params.providerId, status: "connecting", diagnostics: [] };
},
async control() {
return {
status: "error",
message: "Handshake rejected",
diagnostics: [{ severity: "error", message: "Host key mismatch", path: "configuration.hostKey" }],
};
},
closeSessionLocal(sessionId) {
closed.push(sessionId);
void providerOptions.onOutputClose(new Error("Plugin stream cancelled: status-close-repro"));
},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) {
externalSessions.set(options.sessionId, options.ownerToken);
return { sessionId: options.sessionId };
},
async pushExternalOutput() {},
async finishExternalSession(sessionId, details, owner) {
const accepted = externalSessions.get(sessionId) === owner;
finishAttempts.push([sessionId, details, accepted]);
if (!accepted) return false;
externalSessions.delete(sessionId);
resolveFinished([sessionId, details]);
return true;
},
}),
connectionStatusPollMs: 0,
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 78, once() {}, isDestroyed: () => false } };
await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-late-error",
sessionId: "session-late-error",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
assert.deepEqual(await finished, [
"session-late-error",
{
reason: "error",
error: "Handshake rejected",
diagnostics: [{ severity: "error", message: "Host key mismatch", path: "configuration.hostKey" }],
},
]);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(closed, ["session-late-error"]);
assert.equal(finishAttempts[0][1].error, "Handshake rejected");
assert.equal(finishAttempts[0][2], true);
assert.equal(finishAttempts[1][1].error, "Plugin stream cancelled: status-close-repro");
assert.equal(finishAttempts[1][2], false);
});
test("plugin connection status monitoring invokes Provider reconnect for retryable failures", async () => {
const ipcMain = createIpcMain();
const controls = [];
const outputs = [];
let statusIndex = 0;
let resolveFinished;
const finished = new Promise((resolve) => { resolveFinished = resolve; });
const statuses = [
{ status: "error", message: "Temporary loss", retryable: true },
{ status: "connected" },
{ status: "closed", message: "Done" },
];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params) {
return { sessionId: params.sessionId, providerId: params.providerId, status: "connecting", diagnostics: [] };
},
async control(sessionId, operation) {
controls.push([sessionId, operation]);
if (operation === "reconnect") return null;
return statuses[statusIndex++];
},
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { return { sessionId: options.sessionId }; },
async pushExternalOutput(sessionId, data, meta) { outputs.push([sessionId, data, meta]); },
async finishExternalSession(sessionId, details) { resolveFinished([sessionId, details]); return true; },
}),
connectionStatusPollMs: 0,
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 86, once() {}, isDestroyed: () => false } };
await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-retryable-status",
sessionId: "session-retryable-status",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
assert.deepEqual(await finished, [
"session-retryable-status",
{ reason: "closed", error: "Done" },
]);
assert.deepEqual(controls, [
["session-retryable-status", "getStatus"],
["session-retryable-status", "reconnect"],
["session-retryable-status", "getStatus"],
["session-retryable-status", "getStatus"],
]);
assert.equal(outputs.length, 1);
assert.equal(outputs[0][2].pluginConnectionReady, true);
});
test("plugin connection output stream failures finish the terminal as errors", async () => {
const ipcMain = createIpcMain();
let closeOutput;
const finished = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params, options) {
closeOutput = options.onOutputClose;
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
async control() { return { status: "connected" }; },
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { return { sessionId: options.sessionId }; },
async pushExternalOutput() { return true; },
async finishExternalSession(sessionId, details) { finished.push([sessionId, details]); return true; },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 82, once() {}, isDestroyed: () => false } };
await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-output-failure",
sessionId: "session-output-failure",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
await closeOutput({ code: -32013, message: "Provider output failed" });
assert.deepEqual(finished.at(-1), [
"session-output-failure",
{ reason: "error", error: "Provider output failed" },
]);
});
test("plugin connection output stream endings use supported terminal exit reasons", async () => {
const ipcMain = createIpcMain();
let closeOutput;
const finished = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params, options) {
closeOutput = options.onOutputClose;
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
async control() { return { status: "connected" }; },
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) { return { sessionId: options.sessionId }; },
async pushExternalOutput() { return true; },
async finishExternalSession(sessionId, details) { finished.push([sessionId, details]); return true; },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 90, once() {}, isDestroyed: () => false } };
const start = (requestId, sessionId) => ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId,
sessionId,
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
await start("connection-output-end", "session-output-end");
await closeOutput("end");
assert.deepEqual(finished.at(-1), [
"session-output-end",
{ reason: "exited", exitCode: 0 },
]);
await start("connection-output-cancel", "session-output-cancel");
await closeOutput("cancel");
assert.deepEqual(finished.at(-1), [
"session-output-cancel",
{ reason: "closed" },
]);
});
test("plugin connection ignores output that races with renderer session close", async () => {
const ipcMain = createIpcMain();
let externalOptions;
let providerOptions;
let outputAttempts = 0;
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params, options) {
providerOptions = options;
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
async control() { return null; },
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) {
externalOptions = options;
return { sessionId: options.sessionId };
},
async pushExternalOutput(_sessionId, data) {
if (data === "") return true;
outputAttempts += 1;
throw new Error("session no longer exists");
},
async finishExternalSession() { return true; },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 85, once() {}, isDestroyed: () => false } };
await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-output-close-race",
sessionId: "session-output-close-race",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
await providerOptions.onData(Uint8Array.from([0xe2]));
await externalOptions.onClose("renderer-close");
await assert.doesNotReject(providerOptions.onData(new TextEncoder().encode("late output")));
await assert.doesNotReject(providerOptions.onOutputClose("end"));
assert.equal(outputAttempts, 0);
});
test("destroying a plugin connection owner closes the Provider and terminal worker session", async () => {
const ipcMain = createIpcMain();
const destroyedListeners = [];
const controls = [];
const localCloses = [];
const finished = [];
let registeredOwner;
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params) {
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
async control(sessionId, operation, payload, options) {
controls.push([sessionId, operation, payload, options]);
return null;
},
closeSessionLocal(sessionId) { localCloses.push(sessionId); },
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) {
registeredOwner = options.ownerToken;
return { sessionId: options.sessionId };
},
async pushExternalOutput() { return true; },
async finishExternalSession(sessionId, details, owner) {
finished.push([sessionId, details, owner]);
return true;
},
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = {
sender: {
id: 87,
once(name, listener) {
if (name === "destroyed") destroyedListeners.push(listener);
},
isDestroyed: () => false,
},
};
await ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-owner-destroyed",
sessionId: "session-owner-destroyed",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
for (const listener of destroyedListeners) listener();
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(controls.map((call) => call.slice(0, 3)), [["session-owner-destroyed", "close", {}]]);
assert.equal(controls[0][3].sessionOwner, registeredOwner);
assert.deepEqual(localCloses, []);
assert.deepEqual(finished, [["session-owner-destroyed", { reason: "closed" }, registeredOwner]]);
});
test("a late Provider close cannot finish a same-ID replacement connection", async () => {
const ipcMain = createIpcMain();
const providerOptions = [];
const externalSessions = new Map();
const owners = [];
const finishedOwners = [];
const writes = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async openConnection(params, options) {
providerOptions.push(options);
return { sessionId: params.sessionId, providerId: params.providerId, status: "connected", diagnostics: [] };
},
async control() { return null; },
async write(_sessionId, data, owner) { writes.push([data, owner]); },
closeSessionLocal() {},
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) {
if (externalSessions.has(options.sessionId)) throw new Error("external session already registered");
owners.push(options.ownerToken);
externalSessions.set(options.sessionId, { options, owner: options.ownerToken });
return { sessionId: options.sessionId };
},
async pushExternalOutput(sessionId, _data, _meta, owner) {
return externalSessions.get(sessionId)?.owner === owner;
},
async finishExternalSession(sessionId, _details, owner) {
if (externalSessions.get(sessionId)?.owner !== owner) return false;
externalSessions.delete(sessionId);
finishedOwners.push(owner);
return true;
},
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 88, once() {}, isDestroyed: () => false } };
const start = (requestId) => ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId,
sessionId: "session-replaced",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
await start("connection-replaced-old");
const oldExternal = externalSessions.get("session-replaced");
externalSessions.delete("session-replaced");
await oldExternal.options.onClose("renderer-close");
await start("connection-replaced-new");
await assert.doesNotReject(providerOptions[0].onOutputClose("end"));
assert.equal(externalSessions.get("session-replaced")?.owner, owners[1]);
assert.deepEqual(finishedOwners, []);
await ipcMain.handlers.get(CHANNELS.connectionWrite)(event, {
sessionId: "session-replaced",
data: "new input",
});
assert.deepEqual(writes, [["new input", owners[1]]]);
});
test("closing the terminal during plugin authentication cancels the challenge before provider open", async () => {
const ipcMain = createIpcMain();
const sent = [];
const closed = [];
let externalOptions;
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
async authenticate(_params, requestChallenge, options) {
await requestChallenge({
id: "password-before-open",
kind: "password",
title: "Password",
});
if (options.signal.aborted) throw options.signal.reason;
return { status: "authenticated", credential: { kind: "credential", id: "unused" } };
},
async openConnection() {
throw new Error("provider open should not run after terminal close");
},
closeSessionLocal(sessionId) { closed.push(sessionId); },
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) {
externalOptions = options;
return { sessionId: options.sessionId };
},
async finishExternalSession() { return true; },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = {
sender: {
id: 83,
once() {},
isDestroyed: () => false,
send(channel, payload) { sent.push([channel, payload]); },
},
};
const pending = ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-auth-close-before-open",
sessionId: "session-auth-close-before-open",
providerId: "com.example.transport.connection",
authenticationProviderId: "com.example.transport.authentication",
configuration: {},
columns: 80,
rows: 24,
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(sent.length, 1);
await externalOptions.onClose("renderer-close");
await assert.rejects(pending, (error) => error?.name === "AbortError");
await assert.rejects(
ipcMain.handlers.get(CHANNELS.authenticationRespond)(event, {
requestId: sent[0][1].requestId,
challengeRequestId: sent[0][1].challengeRequestId,
challengeId: sent[0][1].challenge.id,
response: "too late",
}),
/not owned/,
);
assert.deepEqual(closed, ["session-auth-close-before-open"]);
});
test("closing the terminal while a plugin connection opens cancels the provider startup", async () => {
const ipcMain = createIpcMain();
let externalOptions;
const closed = [];
registerPluginBridge(ipcMain, {
manager: { initialize: async () => {} },
extensionProviderService: {
openConnection(_params, options) {
return new Promise((_resolve, reject) => {
const rejectCancelled = () => reject(options.signal.reason);
if (options.signal.aborted) rejectCancelled();
else options.signal.addEventListener("abort", rejectCancelled, { once: true });
});
},
closeSessionLocal(sessionId) { closed.push(sessionId); },
},
getTerminalWorkerManager: () => ({
async startExternalSession(options) {
externalOptions = options;
return { sessionId: options.sessionId };
},
async finishExternalSession() { return false; },
}),
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 76, once() {}, isDestroyed: () => false } };
const pending = ipcMain.handlers.get(CHANNELS.connectionStart)(event, {
requestId: "connection-close-during-start",
sessionId: "session-close-during-start",
providerId: "com.example.transport.connection",
configuration: {},
columns: 80,
rows: 24,
});
await new Promise((resolve) => setImmediate(resolve));
await externalOptions.onClose("renderer-close");
await assert.rejects(pending, (error) => error?.name === "AbortError");
assert.deepEqual(closed, ["session-close-during-start"]);
});
test("sync read/write shuttle Uint8Array inline and stream large writes", async () => {
const written = [];
const ipcMain = createIpcMain();
registerPluginBridge(ipcMain, {
manager: { async initialize() {} },
extensionProviderService: {
listProviders() {
return [{
pluginId: "com.example",
pluginVersion: "1.0.0",
provider: { id: "com.example.sync", kind: "sync", label: "Example Sync" },
}];
},
async readSyncObject(params) {
return {
found: true,
key: params.key,
bytes: Buffer.from("cipher-inline"),
revision: "r1",
};
},
async writeSyncObject(params) {
written.push({
key: params.key,
byteLength: params.bytes.byteLength,
preferStream: params.preferStream === true,
});
return { created: true, revision: "r2" };
},
},
secretStore: {
set(pluginId, key, value) {
return Object.freeze({ kind: "secret", id: "sec-1", key, pluginId, value });
},
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 99, once() {}, isDestroyed: () => false } };
const inlineRead = await ipcMain.handlers.get(CHANNELS.syncReadObject)(event, {
requestId: "sync-read-1",
providerId: "com.example.sync",
key: "vault",
});
assert.equal(inlineRead.found, true);
assert.equal(inlineRead.streamed, undefined);
assert.ok(inlineRead.data instanceof Uint8Array || Buffer.isBuffer(inlineRead.data));
assert.equal(Buffer.from(inlineRead.data).toString("utf8"), "cipher-inline");
const inlineWrite = await ipcMain.handlers.get(CHANNELS.syncWriteObject)(event, {
requestId: "sync-write-1",
providerId: "com.example.sync",
key: "vault",
data: Buffer.from("small"),
});
assert.deepEqual(inlineWrite, { created: true, revision: "r2" });
const begin = await ipcMain.handlers.get(CHANNELS.syncWriteBegin)(event, {
requestId: "sync-write-stream",
providerId: "com.example.sync",
key: "vault",
byteLength: 5,
});
assert.equal(typeof begin.transferId, "string");
await ipcMain.handlers.get(CHANNELS.syncWriteChunk)(event, {
requestId: "sync-write-stream",
transferId: begin.transferId,
sequence: 0,
chunk: Buffer.from("large"),
});
const committed = await ipcMain.handlers.get(CHANNELS.syncWriteCommit)(event, {
requestId: "sync-write-stream",
transferId: begin.transferId,
});
assert.deepEqual(committed, { created: true, revision: "r2" });
assert.equal(written.at(-1)?.preferStream, true);
assert.equal(written.at(-1)?.byteLength, 5);
const secret = await ipcMain.handlers.get(CHANNELS.syncPutSecret)(event, {
providerId: "com.example.sync",
key: "sync-credential",
value: "pw",
});
assert.deepEqual(secret, {
kind: "secret",
id: "sec-1",
key: "sync-credential",
pluginId: "com.example",
value: "pw",
created: true,
});
});
test("syncDeleteSecrets uses retained provider binding when contribution is missing", async () => {
const deleted = [];
const unbound = [];
const ipcMain = createIpcMain();
registerPluginBridge(ipcMain, {
manager: { async initialize() {} },
extensionProviderService: {
listProviders() {
return [];
},
},
secretStore: {
set() {
throw new Error("set should not run");
},
delete(pluginId, key) {
deleted.push([pluginId, key]);
},
deleteByKeyPrefix(pluginId, prefix) {
deleted.push([pluginId, `prefix:${prefix}`]);
return 2;
},
resolveSyncProviderPlugin(providerId) {
assert.equal(providerId, "com.example.sync");
return "com.example";
},
unbindSyncProviderPlugin(pluginId, providerId) {
unbound.push([pluginId, providerId]);
},
},
env: { NETCATTY_PLUGIN_DEV: "1" },
isTrustedSender: () => true,
});
const event = { sender: { id: 99, once() {}, isDestroyed: () => false } };
const result = await ipcMain.handlers.get(CHANNELS.syncDeleteSecrets)(event, {
providerId: "com.example.sync",
});
assert.deepEqual(result, { deleted: 2 });
assert.deepEqual(deleted, [["com.example", "prefix:sync-credential"]]);
assert.deepEqual(unbound, [["com.example", "com.example.sync"]]);
});