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
363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
const assert = require("node:assert/strict");
|
|
const test = require("node:test");
|
|
|
|
const terminalBridge = require("./terminalBridge.cjs");
|
|
const sshBridge = require("./sshBridge.cjs");
|
|
const keyboardInteractiveHandler = require("./keyboardInteractiveHandler.cjs");
|
|
const passphraseHandler = require("./passphraseHandler.cjs");
|
|
const hostKeyVerifier = require("./hostKeyVerifier.cjs");
|
|
const sftpBridge = require("./sftpBridge.cjs");
|
|
const transferBridge = require("./transferBridge.cjs");
|
|
const compressUploadBridge = require("./compressUploadBridge.cjs");
|
|
const fileWatcherBridge = require("./fileWatcherBridge.cjs");
|
|
const { createSystemManagerBridge } = require("./systemManagerBridge.cjs");
|
|
|
|
function createFakeIpcMain() {
|
|
return {
|
|
handlers: new Map(),
|
|
listeners: new Map(),
|
|
handle(channel, handler) {
|
|
this.handlers.set(channel, handler);
|
|
},
|
|
on(channel, listener) {
|
|
this.listeners.set(channel, listener);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createFakeWorkerManager() {
|
|
const requests = [];
|
|
const sends = [];
|
|
return {
|
|
requests,
|
|
sends,
|
|
request(channel, payload, options) {
|
|
requests.push({ channel, payload, options });
|
|
return Promise.resolve({ ok: true, channel });
|
|
},
|
|
send(channel, payload, options) {
|
|
sends.push({ channel, payload, options });
|
|
},
|
|
};
|
|
}
|
|
|
|
const fakeEvent = { sender: { id: 42 } };
|
|
|
|
test("terminal worker mode proxies all terminal starts and control commands", async () => {
|
|
const ipcMain = createFakeIpcMain();
|
|
const terminalWorkerManager = createFakeWorkerManager();
|
|
|
|
terminalBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
|
|
for (const channel of [
|
|
"netcatty:local:start",
|
|
"netcatty:telnet:start",
|
|
"netcatty:mosh:start",
|
|
"netcatty:et:start",
|
|
"netcatty:serial:start",
|
|
"netcatty:close:await",
|
|
]) {
|
|
assert.equal(ipcMain.handlers.has(channel), true, `${channel} should be proxied as a request`);
|
|
await ipcMain.handlers.get(channel)(fakeEvent, { sessionId: channel });
|
|
}
|
|
|
|
for (const channel of [
|
|
"netcatty:write",
|
|
"netcatty:interrupt",
|
|
"netcatty:resize",
|
|
"netcatty:pty:clear",
|
|
"netcatty:flow",
|
|
"netcatty:flow:ack",
|
|
"netcatty:close",
|
|
]) {
|
|
assert.equal(ipcMain.listeners.has(channel), true, `${channel} should be proxied as a send`);
|
|
ipcMain.listeners.get(channel)(fakeEvent, { sessionId: channel });
|
|
}
|
|
|
|
assert.deepEqual(
|
|
terminalWorkerManager.requests.map((entry) => entry.channel),
|
|
[
|
|
"netcatty:local:start",
|
|
"netcatty:telnet:start",
|
|
"netcatty:mosh:start",
|
|
"netcatty:et:start",
|
|
"netcatty:serial:start",
|
|
"netcatty:close:await",
|
|
],
|
|
);
|
|
assert.deepEqual(
|
|
terminalWorkerManager.sends.map((entry) => entry.channel),
|
|
[
|
|
"netcatty:write",
|
|
"netcatty:interrupt",
|
|
"netcatty:resize",
|
|
"netcatty:pty:clear",
|
|
"netcatty:flow",
|
|
"netcatty:flow:ack",
|
|
"netcatty:close",
|
|
],
|
|
);
|
|
});
|
|
|
|
test("terminal worker mode proxies SSH session and remote helper requests", async () => {
|
|
const ipcMain = createFakeIpcMain();
|
|
const terminalWorkerManager = createFakeWorkerManager();
|
|
|
|
sshBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
|
|
for (const channel of [
|
|
"netcatty:start",
|
|
"netcatty:ssh:exec",
|
|
"netcatty:ssh:pwd",
|
|
"netcatty:ssh:remoteInfo",
|
|
"netcatty:ssh:distroInfo",
|
|
"netcatty:ssh:readRemoteHistory",
|
|
"netcatty:ssh:listdir",
|
|
"netcatty:ssh:stats",
|
|
"netcatty:ssh:setEncoding",
|
|
]) {
|
|
assert.equal(ipcMain.handlers.has(channel), true, `${channel} should be proxied`);
|
|
await ipcMain.handlers.get(channel)(fakeEvent, { sessionId: "ssh-1" });
|
|
}
|
|
|
|
assert.deepEqual(
|
|
terminalWorkerManager.requests.map((entry) => entry.channel),
|
|
[
|
|
"netcatty:start",
|
|
"netcatty:ssh:exec",
|
|
"netcatty:ssh:pwd",
|
|
"netcatty:ssh:remoteInfo",
|
|
"netcatty:ssh:distroInfo",
|
|
"netcatty:ssh:readRemoteHistory",
|
|
"netcatty:ssh:listdir",
|
|
"netcatty:ssh:stats",
|
|
"netcatty:ssh:setEncoding",
|
|
],
|
|
);
|
|
assert.equal(
|
|
terminalWorkerManager.requests[0].payload._macLocalNetworkMainProbed,
|
|
true,
|
|
"worker start payload should skip a second Local Network probe",
|
|
);
|
|
});
|
|
|
|
test("terminal worker mode keeps main-process keyboard-interactive responses in the main process", async () => {
|
|
const ipcMain = createFakeIpcMain();
|
|
const terminalWorkerManager = createFakeWorkerManager();
|
|
const requestId = keyboardInteractiveHandler.generateRequestId("port-forward");
|
|
let receivedResponses = null;
|
|
|
|
keyboardInteractiveHandler.storeRequest(
|
|
requestId,
|
|
(responses) => {
|
|
receivedResponses = responses;
|
|
},
|
|
fakeEvent.sender.id,
|
|
"tunnel-1",
|
|
);
|
|
sshBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
|
|
const result = await ipcMain.handlers.get("netcatty:keyboard-interactive:respond")(
|
|
fakeEvent,
|
|
{ requestId, responses: ["123456"], cancelled: false },
|
|
);
|
|
|
|
const remainedPending = keyboardInteractiveHandler.getRequests().has(requestId);
|
|
if (remainedPending) {
|
|
keyboardInteractiveHandler.handleResponse(fakeEvent, {
|
|
requestId,
|
|
responses: [],
|
|
cancelled: true,
|
|
});
|
|
}
|
|
|
|
assert.deepEqual(result, { success: true });
|
|
assert.deepEqual(receivedResponses, ["123456"]);
|
|
assert.equal(remainedPending, false);
|
|
assert.equal(terminalWorkerManager.requests.length, 0);
|
|
});
|
|
|
|
test("terminal worker mode keeps main-process passphrase responses in the main process", async () => {
|
|
const ipcMain = createFakeIpcMain();
|
|
const terminalWorkerManager = createFakeWorkerManager();
|
|
const sent = [];
|
|
const sender = {
|
|
id: fakeEvent.sender.id,
|
|
isDestroyed: () => false,
|
|
send: (channel, payload) => sent.push({ channel, payload }),
|
|
};
|
|
const responsePromise = passphraseHandler.requestPassphrase(
|
|
sender,
|
|
"/tmp/test-key",
|
|
"test-key",
|
|
"example.com",
|
|
false,
|
|
);
|
|
const requestId = sent[0].payload.requestId;
|
|
sshBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
|
|
const wrongResult = await ipcMain.handlers.get("netcatty:passphrase:respond")(
|
|
{ sender: { id: fakeEvent.sender.id + 1 } },
|
|
{ requestId, passphrase: "stolen", cancelled: false },
|
|
);
|
|
assert.deepEqual(wrongResult, { success: false, error: "Wrong sender" });
|
|
assert.equal(passphraseHandler.getRequests().has(requestId), true);
|
|
|
|
const result = await ipcMain.handlers.get("netcatty:passphrase:respond")(
|
|
fakeEvent,
|
|
{ requestId, passphrase: "secret", cancelled: false },
|
|
);
|
|
|
|
assert.deepEqual(result, { success: true });
|
|
assert.deepEqual(await responsePromise, { passphrase: "secret" });
|
|
assert.equal(passphraseHandler.getRequests().has(requestId), false);
|
|
assert.equal(terminalWorkerManager.requests.length, 0);
|
|
});
|
|
|
|
test("terminal worker mode keeps main-process host-key responses in the main process", async () => {
|
|
const ipcMain = createFakeIpcMain();
|
|
const terminalWorkerManager = createFakeWorkerManager();
|
|
const sent = [];
|
|
const sender = {
|
|
id: fakeEvent.sender.id,
|
|
isDestroyed: () => false,
|
|
send: (channel, payload) => sent.push({ channel, payload }),
|
|
};
|
|
const responsePromise = hostKeyVerifier.requestHostKeyVerification(sender, {
|
|
sessionId: "tunnel-1",
|
|
hostname: "example.com",
|
|
port: 22,
|
|
status: "unknown",
|
|
});
|
|
const requestId = sent[0].payload.requestId;
|
|
sshBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
|
|
const wrongResult = await ipcMain.handlers.get("netcatty:host-key:respond")(
|
|
{ sender: { id: fakeEvent.sender.id + 1 } },
|
|
{ requestId, accept: true, addToKnownHosts: false },
|
|
);
|
|
assert.deepEqual(wrongResult, { success: false, error: "Wrong sender" });
|
|
assert.equal(hostKeyVerifier.getRequests().has(requestId), true);
|
|
|
|
const result = await ipcMain.handlers.get("netcatty:host-key:respond")(
|
|
fakeEvent,
|
|
{ requestId, accept: true, addToKnownHosts: false },
|
|
);
|
|
|
|
assert.deepEqual(result, { success: true });
|
|
assert.deepEqual(await responsePromise, { accept: true, addToKnownHosts: false });
|
|
assert.equal(hostKeyVerifier.getRequests().has(requestId), false);
|
|
assert.equal(terminalWorkerManager.requests.length, 0);
|
|
});
|
|
|
|
test("terminal worker mode forwards worker-owned authentication responses", async () => {
|
|
const ipcMain = createFakeIpcMain();
|
|
const terminalWorkerManager = createFakeWorkerManager();
|
|
sshBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
|
|
for (const channel of [
|
|
"netcatty:keyboard-interactive:respond",
|
|
"netcatty:passphrase:respond",
|
|
"netcatty:host-key:respond",
|
|
]) {
|
|
const result = await ipcMain.handlers.get(channel)(fakeEvent, { requestId: `worker-${channel}` });
|
|
assert.deepEqual(result, { ok: true, channel });
|
|
}
|
|
|
|
assert.deepEqual(
|
|
terminalWorkerManager.requests.map((entry) => entry.channel),
|
|
[
|
|
"netcatty:keyboard-interactive:respond",
|
|
"netcatty:passphrase:respond",
|
|
"netcatty:host-key:respond",
|
|
],
|
|
);
|
|
});
|
|
|
|
test("terminal worker mode proxies SFTP and surrounding file operations", async () => {
|
|
const ipcMain = createFakeIpcMain();
|
|
const terminalWorkerManager = createFakeWorkerManager();
|
|
|
|
sftpBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
transferBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
compressUploadBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
fileWatcherBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
|
|
for (const channel of [
|
|
"netcatty:sftp:openForSession",
|
|
"netcatty:sftp:list",
|
|
"netcatty:sftp:write",
|
|
"netcatty:sftp:downloadToLocal",
|
|
"netcatty:sftp:uploadLocal",
|
|
"netcatty:sftp:close",
|
|
"netcatty:transfer:start",
|
|
"netcatty:transfer:retain-sftp-session",
|
|
"netcatty:transfer:release-sftp-session",
|
|
"netcatty:transfer:cancel",
|
|
"netcatty:compress:start",
|
|
"netcatty:compress:checkSupport",
|
|
"netcatty:filewatch:start",
|
|
"netcatty:filewatch:registerTempFile",
|
|
]) {
|
|
assert.equal(ipcMain.handlers.has(channel), true, `${channel} should be proxied`);
|
|
await ipcMain.handlers.get(channel)(fakeEvent, { sessionId: "ssh-1", sftpId: "sftp-1" });
|
|
}
|
|
|
|
assert.deepEqual(
|
|
terminalWorkerManager.requests.map((entry) => entry.channel),
|
|
[
|
|
"netcatty:sftp:openForSession",
|
|
"netcatty:sftp:list",
|
|
"netcatty:sftp:write",
|
|
"netcatty:sftp:downloadToLocal",
|
|
"netcatty:sftp:uploadLocal",
|
|
"netcatty:sftp:close",
|
|
"netcatty:transfer:start",
|
|
"netcatty:transfer:retain-sftp-session",
|
|
"netcatty:transfer:release-sftp-session",
|
|
"netcatty:transfer:cancel",
|
|
"netcatty:compress:start",
|
|
"netcatty:compress:checkSupport",
|
|
"netcatty:filewatch:start",
|
|
"netcatty:filewatch:registerTempFile",
|
|
],
|
|
);
|
|
});
|
|
|
|
test("terminal worker mode proxies system management requests", async () => {
|
|
const ipcMain = createFakeIpcMain();
|
|
const terminalWorkerManager = createFakeWorkerManager();
|
|
const systemManagerBridge = createSystemManagerBridge({
|
|
getSessions: () => new Map(),
|
|
execOnEtSession: () => {},
|
|
ensureMoshStatsConnection: () => {},
|
|
process,
|
|
});
|
|
|
|
systemManagerBridge.registerHandlers(ipcMain, { terminalWorkerManager });
|
|
|
|
for (const channel of [
|
|
"netcatty:system:probeCapabilities",
|
|
"netcatty:system:listProcesses",
|
|
"netcatty:system:setupOsc7Tracking",
|
|
"netcatty:system:listTmuxSessions",
|
|
"netcatty:system:listDockerContainers",
|
|
"netcatty:system:listAccelerators",
|
|
]) {
|
|
assert.equal(ipcMain.handlers.has(channel), true, `${channel} should be proxied`);
|
|
await ipcMain.handlers.get(channel)(fakeEvent, { sessionId: "ssh-1" });
|
|
}
|
|
|
|
assert.deepEqual(
|
|
terminalWorkerManager.requests.map((entry) => entry.channel),
|
|
[
|
|
"netcatty:system:probeCapabilities",
|
|
"netcatty:system:listProcesses",
|
|
"netcatty:system:setupOsc7Tracking",
|
|
"netcatty:system:listTmuxSessions",
|
|
"netcatty:system:listDockerContainers",
|
|
"netcatty:system:listAccelerators",
|
|
],
|
|
);
|
|
});
|