[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:
240
electron/bridges/portForwardingBridge.worker.test.cjs
Normal file
240
electron/bridges/portForwardingBridge.worker.test.cjs
Normal file
@@ -0,0 +1,240 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const EventEmitter = require("node:events");
|
||||
const test = require("node:test");
|
||||
|
||||
const { registerHandlers } = require("./portForwardingBridge.cjs");
|
||||
|
||||
function createIpcMain() {
|
||||
const handlers = new Map();
|
||||
return {
|
||||
handlers,
|
||||
handle(channel, handler) {
|
||||
handlers.set(channel, handler);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSender(id) {
|
||||
const sender = new EventEmitter();
|
||||
sender.id = id;
|
||||
sender.isDestroyed = () => false;
|
||||
sender.sent = [];
|
||||
sender.send = (channel, payload) => sender.sent.push({ channel, payload });
|
||||
return sender;
|
||||
}
|
||||
|
||||
test("worker mode routes every port-forward operation through the terminal worker", async () => {
|
||||
const ipcMain = createIpcMain();
|
||||
const requests = [];
|
||||
const terminalWorkerManager = {
|
||||
request(channel, payload, options) {
|
||||
requests.push({ channel, payload, options });
|
||||
if (channel === "netcatty:portforward:start") {
|
||||
return Promise.resolve({ tunnelId: payload.tunnelId, success: true });
|
||||
}
|
||||
if (channel === "netcatty:portforward:subscribe") {
|
||||
return Promise.resolve({ tunnelId: payload.tunnelId, status: "active" });
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
onWorkerExit() {
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
registerHandlers(ipcMain, { terminalWorkerManager });
|
||||
const sender = createSender(17);
|
||||
const event = { sender };
|
||||
const channels = [
|
||||
"netcatty:portforward:start",
|
||||
"netcatty:portforward:stop",
|
||||
"netcatty:portforward:status",
|
||||
"netcatty:portforward:subscribe",
|
||||
"netcatty:portforward:list",
|
||||
"netcatty:portforward:snapshot",
|
||||
"netcatty:portforward:subscribeRuntime",
|
||||
"netcatty:portforward:unsubscribeRuntime",
|
||||
"netcatty:portforward:stopAll",
|
||||
"netcatty:portforward:stopByRuleId",
|
||||
];
|
||||
|
||||
for (const channel of channels) {
|
||||
await ipcMain.handlers.get(channel)(event, { tunnelId: "pf-worker", ruleId: "rule-worker" });
|
||||
}
|
||||
|
||||
assert.deepEqual(requests.map((entry) => entry.channel), channels);
|
||||
assert.ok(requests.every((entry) => entry.options.webContentsId === 17));
|
||||
});
|
||||
|
||||
test("legacy mode keeps port-forward handlers in the main process", async () => {
|
||||
const ipcMain = createIpcMain();
|
||||
registerHandlers(ipcMain);
|
||||
|
||||
assert.deepEqual(
|
||||
await ipcMain.handlers.get("netcatty:portforward:status")(
|
||||
{ sender: createSender(19) },
|
||||
{ tunnelId: "pf-legacy-missing" },
|
||||
),
|
||||
{ tunnelId: "pf-legacy-missing", status: "inactive" },
|
||||
);
|
||||
assert.deepEqual(await ipcMain.handlers.get("netcatty:portforward:list")(), []);
|
||||
});
|
||||
|
||||
test("worker mode drops renderer subscriptions when their window is destroyed", async () => {
|
||||
const ipcMain = createIpcMain();
|
||||
const requests = [];
|
||||
const terminalWorkerManager = {
|
||||
request(channel, payload, options) {
|
||||
requests.push({ channel, payload, options });
|
||||
return Promise.resolve({ tunnelId: payload.tunnelId, status: "active" });
|
||||
},
|
||||
onWorkerExit() {
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
registerHandlers(ipcMain, { terminalWorkerManager });
|
||||
const sender = createSender(23);
|
||||
|
||||
await ipcMain.handlers.get("netcatty:portforward:subscribe")(
|
||||
{ sender },
|
||||
{ tunnelId: "pf-destroyed" },
|
||||
);
|
||||
sender.emit("destroyed");
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.equal(requests.at(-1).channel, "netcatty:portforward:unsubscribeSender");
|
||||
assert.deepEqual(requests.at(-1).payload, { webContentsId: 23 });
|
||||
});
|
||||
|
||||
test("worker mode drops a renderer destroyed while its start request is pending", async () => {
|
||||
const ipcMain = createIpcMain();
|
||||
const requests = [];
|
||||
let finishStart;
|
||||
const startResult = new Promise((resolve) => { finishStart = resolve; });
|
||||
const terminalWorkerManager = {
|
||||
request(channel, payload, options) {
|
||||
requests.push({ channel, payload, options });
|
||||
if (channel === "netcatty:portforward:start") return startResult;
|
||||
return Promise.resolve({ removed: 1 });
|
||||
},
|
||||
onWorkerExit() {
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
registerHandlers(ipcMain, { terminalWorkerManager });
|
||||
const sender = createSender(29);
|
||||
let destroyed = false;
|
||||
sender.isDestroyed = () => destroyed;
|
||||
const pending = ipcMain.handlers.get("netcatty:portforward:start")(
|
||||
{ sender },
|
||||
{ tunnelId: "pf-pending-destroy", ruleId: "rule-pending-destroy" },
|
||||
);
|
||||
|
||||
destroyed = true;
|
||||
sender.emit("destroyed");
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(requests.at(-1).channel, "netcatty:portforward:unsubscribeSender");
|
||||
finishStart({ tunnelId: "pf-pending-destroy", success: true });
|
||||
await pending;
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(requests.at(-1).channel, "netcatty:portforward:unsubscribeSender");
|
||||
});
|
||||
|
||||
test("worker mode does not retain renderer lifecycle after a failed start", async () => {
|
||||
const ipcMain = createIpcMain();
|
||||
const terminalWorkerManager = {
|
||||
request(_channel, payload) {
|
||||
return Promise.resolve({
|
||||
tunnelId: payload.tunnelId,
|
||||
success: false,
|
||||
error: "bind failed",
|
||||
});
|
||||
},
|
||||
onWorkerExit() {
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
registerHandlers(ipcMain, { terminalWorkerManager });
|
||||
const sender = createSender(30);
|
||||
|
||||
await ipcMain.handlers.get("netcatty:portforward:start")(
|
||||
{ sender },
|
||||
{ tunnelId: "pf-failed", ruleId: "rule-failed" },
|
||||
);
|
||||
|
||||
assert.equal(sender.listenerCount("destroyed"), 0);
|
||||
});
|
||||
|
||||
test("worker error terminal status releases the renderer subscription", async () => {
|
||||
const ipcMain = createIpcMain();
|
||||
let workerRendererEventListener;
|
||||
const terminalWorkerManager = {
|
||||
request(_channel, payload) {
|
||||
return Promise.resolve({ tunnelId: payload.tunnelId, success: true });
|
||||
},
|
||||
onWorkerRendererEvent(listener) {
|
||||
workerRendererEventListener = listener;
|
||||
return { dispose() {} };
|
||||
},
|
||||
onWorkerExit() {
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
registerHandlers(ipcMain, { terminalWorkerManager });
|
||||
const sender = createSender(32);
|
||||
|
||||
await ipcMain.handlers.get("netcatty:portforward:start")(
|
||||
{ sender },
|
||||
{ tunnelId: "pf-error", ruleId: "rule-error" },
|
||||
);
|
||||
assert.equal(sender.listenerCount("destroyed"), 1);
|
||||
|
||||
workerRendererEventListener({
|
||||
channel: "netcatty:portforward:status",
|
||||
payload: { tunnelId: "pf-error", status: "error", error: "remote closed" },
|
||||
});
|
||||
|
||||
assert.equal(sender.listenerCount("destroyed"), 0);
|
||||
});
|
||||
|
||||
test("worker crashes notify subscribed renderers and permit a replacement worker start", async () => {
|
||||
const ipcMain = createIpcMain();
|
||||
let workerExitListener;
|
||||
let starts = 0;
|
||||
const terminalWorkerManager = {
|
||||
request(channel, payload) {
|
||||
if (channel === "netcatty:portforward:start") {
|
||||
starts += 1;
|
||||
return Promise.resolve({ tunnelId: payload.tunnelId, success: true });
|
||||
}
|
||||
return Promise.resolve({ tunnelId: payload.tunnelId, status: "active" });
|
||||
},
|
||||
onWorkerExit(listener) {
|
||||
workerExitListener = listener;
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
registerHandlers(ipcMain, { terminalWorkerManager });
|
||||
const sender = createSender(31);
|
||||
const event = { sender };
|
||||
|
||||
await ipcMain.handlers.get("netcatty:portforward:start")(
|
||||
event,
|
||||
{ tunnelId: "pf-crash", ruleId: "rule-crash" },
|
||||
);
|
||||
workerExitListener(new Error("Terminal worker exited with code 9"));
|
||||
|
||||
assert.deepEqual(sender.sent, [{
|
||||
channel: "netcatty:portforward:status",
|
||||
payload: {
|
||||
tunnelId: "pf-crash",
|
||||
status: "error",
|
||||
error: "Terminal worker exited with code 9",
|
||||
},
|
||||
}]);
|
||||
|
||||
await ipcMain.handlers.get("netcatty:portforward:start")(
|
||||
event,
|
||||
{ tunnelId: "pf-recovered", ruleId: "rule-crash" },
|
||||
);
|
||||
assert.equal(starts, 2);
|
||||
});
|
||||
Reference in New Issue
Block a user