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

241 lines
7.5 KiB
JavaScript

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);
});