Files
NetMesh/electron/bridges/terminalBridge.bareMoshClient.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

423 lines
15 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { StringDecoder } = require("node:string_decoder");
const {
addBundledMoshRuntimeEnv,
resolveBareMoshClient,
} = require("./terminalBridge.cjs");
const { createMoshSessionApi } = require("./terminalBridge/moshSession.cjs");
function makeTmp() {
return fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-mosh-resolve-"));
}
function writeExecutable(filePath) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, "#!/bin/sh\nexit 0\n");
fs.chmodSync(filePath, 0o755);
}
test("resolveBareMoshClient ignores explicit local mosh-client paths", () => {
const tmp = makeTmp();
const p = path.join(tmp, "mosh-client");
writeExecutable(p);
assert.equal(resolveBareMoshClient({ moshClientPath: p }, { projectRoot: tmp, resourcesPath: path.join(tmp, "missing") }), null);
});
test("resolveBareMoshClient resolves only the bundled client", () => {
const tmp = makeTmp();
const bundled = path.join(tmp, "resources", "mosh", "linux-x64", "mosh-client");
writeExecutable(bundled);
assert.equal(
resolveBareMoshClient({}, {
platform: "linux",
arch: "x64",
projectRoot: tmp,
resourcesPath: path.join(tmp, "missing"),
}),
bundled,
);
});
test("resolveBareMoshClient rejects relative explicit paths", () => {
const tmp = makeTmp();
const got = resolveBareMoshClient({ moshClientPath: "./mosh-client" }, {
projectRoot: tmp,
resourcesPath: path.join(tmp, "missing"),
});
assert.equal(got, null);
});
test("resolveBareMoshClient ignores a non-executable explicit path", () => {
const tmp = makeTmp();
const p = path.join(tmp, "mosh-client");
fs.writeFileSync(p, "");
fs.chmodSync(p, 0o644);
const got = resolveBareMoshClient({ moshClientPath: p }, {
projectRoot: tmp,
resourcesPath: path.join(tmp, "missing"),
});
assert.equal(got, null);
});
test("resolveBareMoshClient ignores mosh-client on PATH", () => {
const tmp = makeTmp();
const p = path.join(tmp, "mosh-client");
writeExecutable(p);
assert.equal(resolveBareMoshClient({}, {
pathOverride: tmp,
projectRoot: tmp,
resourcesPath: path.join(tmp, "missing"),
}), null);
});
test("mosh fallback messages do not point users to the removed Mosh settings field", () => {
const source = fs.readFileSync(path.join(__dirname, "terminalBridge.cjs"), "utf8");
assert.equal(source.includes("Settings → Terminal → Mosh"), false);
});
test("mosh runtime does not fall back to system mosh or mosh-client", () => {
const source = fs.readFileSync(path.join(__dirname, "terminalBridge.cjs"), "utf8");
assert.equal(source.includes('resolvePosixExecutable("mosh-client"'), false);
assert.equal(source.includes('findExecutable("mosh-client"'), false);
assert.equal(source.includes('resolvePosixExecutable("mosh"'), false);
assert.equal(source.includes('findExecutable("mosh"'), false);
assert.equal(source.includes("brew install mosh"), false);
});
test("MoshCatty runtime env is a no-op (no DLL bag / terminfo)", () => {
const env = { Path: "C:\\Windows\\System32", TERM: "xterm-256color" };
const out = addBundledMoshRuntimeEnv(env, "C:\\app\\mosh-client.exe", { platform: "win32" });
assert.equal(out, env);
assert.equal(env.TERMINFO, undefined);
assert.equal(env.TERMINFO_DIRS, undefined);
assert.equal(env.Path, "C:\\Windows\\System32");
});
test("mosh UTF-8 decoder preserves fragmented Chinese output", () => {
const { createMoshUtf8Decoder } = createMoshSessionApi({
StringDecoder,
Buffer,
});
const decode = createMoshUtf8Decoder();
const fixture = Buffer.from("mosh: 连接恢复,终端输出正常\n", "utf8");
const chunks = [
fixture.subarray(0, 9),
fixture.subarray(9, 11),
fixture.subarray(11, 17),
fixture.subarray(17),
];
const decoded = chunks.map((chunk) => decode(chunk)).join("");
assert.equal(decoded, "mosh: 连接恢复,终端输出正常\n");
assert.equal(decoded.includes("\uFFFD"), false);
});
test("Mosh prepares the configured system agent before building native ssh options", async (t) => {
const calls = [];
const tempBase = makeTmp();
t.after(() => fs.rmSync(tempBase, { recursive: true, force: true }));
const api = createMoshSessionApi({
os,
path,
fs,
process,
randomUUID: () => "fixed",
tempDirBridge: { getTempFilePath: (fileName) => path.join(tempBase, fileName) },
prepareSystemSshAgentForAuth: async (options) => {
calls.push(["prepare", options.identityAgent, options.useKeychain]);
},
getAvailableAgentSocket: async (identityAgent) => {
calls.push(["resolve", identityAgent]);
return "/tmp/custom-agent.sock";
},
});
const prepared = await api.prepareMoshSshAgentOptions({
hostname: "host.example",
username: "alice",
useSshAgent: true,
identityAgent: "/tmp/custom-agent.sock",
useKeychain: true,
addKeysToAgent: "yes",
identityFilePaths: ["~/.ssh/id_work"],
});
const auth = await api.buildMoshSshAuthArgs({
...prepared,
identitiesOnly: true,
identityFilePaths: ["~/.ssh/id_work"],
}, "session-1");
const env = api.applyMoshSshAgentEnvironment({}, prepared);
assert.deepEqual(calls, [
["prepare", "/tmp/custom-agent.sock", true],
["resolve", "/tmp/custom-agent.sock"],
]);
assert.deepEqual(auth.sshArgs, [
"-i", path.join(os.homedir(), ".ssh", "id_work.pub"),
"-o", "IdentitiesOnly=yes",
"-o", "IdentityAgent=/tmp/custom-agent.sock",
"-o", "StrictHostKeyChecking=ask",
]);
assert.equal(env.SSH_AUTH_SOCK, "/tmp/custom-agent.sock");
const selected = await api.buildMoshSshAuthArgs({
...prepared,
identitiesOnly: true,
keyId: "vault-key",
agentPublicKeys: ["ssh-ed25519 AAAASELECTED"],
}, "session-selected");
const selectedPath = selected.sshArgs[1];
assert.deepEqual(selected.sshArgs.slice(0, 2), ["-i", selectedPath]);
assert.equal(fs.readFileSync(selectedPath, "utf8"), "ssh-ed25519 AAAASELECTED\n");
assert.ok(selected.sshArgs.includes("IdentitiesOnly=yes"));
api.cleanupMoshAuthTempFiles(selected.tempFiles);
});
test("Mosh injects vault known_hosts into the SSH bootstrap for key-change checks", async (t) => {
const tempBase = makeTmp();
t.after(() => fs.rmSync(tempBase, { recursive: true, force: true }));
const api = createMoshSessionApi({
os,
path,
fs,
process,
randomUUID: () => "fixed",
tempDirBridge: { getTempFilePath: (fileName) => path.join(tempBase, fileName) },
});
const auth = await api.buildMoshSshAuthArgs({
useSshAgent: false,
hostname: "host.example",
port: 22,
knownHosts: [{
hostname: "host.example",
port: 22,
keyType: "ssh-ed25519",
publicKey: "ssh-ed25519 AAAAMOSHVAULT",
}],
}, "session-vault-kh");
let trustPath = null;
for (let i = 0; i < auth.sshArgs.length - 1; i += 1) {
if (auth.sshArgs[i] === "-o" && String(auth.sshArgs[i + 1]).startsWith("UserKnownHostsFile=")) {
trustPath = auth.sshArgs[i + 1].slice("UserKnownHostsFile=".length);
break;
}
}
assert.ok(trustPath, "expected UserKnownHostsFile ssh option");
assert.ok(auth.sshArgs.includes(`GlobalKnownHostsFile=${trustPath}`));
assert.ok(fs.existsSync(trustPath));
assert.match(fs.readFileSync(trustPath, "utf8"), /host\.example ssh-ed25519 AAAAMOSHVAULT/);
// Force ask so a permissive user ssh_config cannot disable verification.
assert.ok(auth.sshArgs.includes("StrictHostKeyChecking=ask"));
api.cleanupMoshAuthTempFiles(auth.tempFiles);
});
test("Mosh disables host-key checks when verifyHostKeys is false", async (t) => {
const tempBase = makeTmp();
t.after(() => fs.rmSync(tempBase, { recursive: true, force: true }));
const api = createMoshSessionApi({
os,
path,
fs,
process,
randomUUID: () => "fixed",
tempDirBridge: { getTempFilePath: (fileName) => path.join(tempBase, fileName) },
});
const auth = await api.buildMoshSshAuthArgs({
useSshAgent: false,
verifyHostKeys: false,
knownHosts: [{
hostname: "host.example",
keyType: "ssh-ed25519",
publicKey: "ssh-ed25519 AAASTALE",
}],
}, "session-no-verify");
assert.equal(auth.sshArgs[0], "-o");
assert.equal(auth.sshArgs[1], "IdentityAgent=none");
let emptyPath = null;
for (let i = 0; i < auth.sshArgs.length - 1; i += 1) {
if (auth.sshArgs[i] === "-o" && String(auth.sshArgs[i + 1]).startsWith("UserKnownHostsFile=")) {
emptyPath = auth.sshArgs[i + 1].slice("UserKnownHostsFile=".length);
break;
}
}
assert.ok(emptyPath);
assert.ok(auth.sshArgs.includes(`GlobalKnownHostsFile=${emptyPath}`));
assert.ok(auth.sshArgs.includes("StrictHostKeyChecking=no"));
assert.equal(fs.readFileSync(emptyPath, "utf8").trim(), "");
assert.equal(auth.sshArgs.some((arg) => String(arg).includes("AAASTALE")), false);
api.cleanupMoshAuthTempFiles(auth.tempFiles);
});
test("Mosh explicitly disables native agent login after an opt-out", async () => {
const api = createMoshSessionApi({
os,
path,
fs,
process,
randomUUID: () => "fixed",
});
const auth = await api.buildMoshSshAuthArgs({ useSshAgent: false }, "session-disabled");
const env = api.applyMoshSshAgentEnvironment(
{ SSH_AUTH_SOCK: "/tmp/inherited-agent.sock" },
{ useSshAgent: false },
);
assert.deepEqual(auth.sshArgs, [
"-o", "IdentityAgent=none",
"-o", "StrictHostKeyChecking=ask",
]);
assert.equal(env.SSH_AUTH_SOCK, undefined);
const forwardingAuth = await api.buildMoshSshAuthArgs({
useSshAgent: false,
agentForwarding: true,
}, "session-forwarding");
const forwardingEnv = api.applyMoshSshAgentEnvironment(
{ SSH_AUTH_SOCK: "/tmp/forwarded-agent.sock" },
{ useSshAgent: false, agentForwarding: true },
);
assert.deepEqual(forwardingAuth.sshArgs, [
"-o", "IdentityAgent=none",
"-o", "StrictHostKeyChecking=ask",
]);
assert.equal(forwardingEnv.SSH_AUTH_SOCK, undefined);
});
test("Mosh keeps its login agent separate from the discovered forwarding agent", async () => {
const localAgent = "/private/tmp/com.apple.launchd.test/Listeners";
const forwardingAgent = "/Users/alice/.bitwarden-ssh-agent.sock";
const api = createMoshSessionApi({
os,
path,
fs,
process: { ...process, env: { SSH_AUTH_SOCK: localAgent } },
randomUUID: () => "fixed",
prepareSystemSshAgentForAuth: async () => {},
getAvailableAgentSocket: async () => localAgent,
getAvailableForwardingAgentSocket: async () => forwardingAgent,
});
for (const useSshAgent of [false, undefined, true]) {
const prepared = await api.prepareMoshSshAgentOptions({
useSshAgent,
agentForwarding: true,
});
const env = api.applyMoshSshAgentEnvironment(
{ SSH_AUTH_SOCK: "/tmp/remote-agent.sock" },
prepared,
);
const auth = await api.buildMoshSshAuthArgs(prepared, `session-forwarding-${String(useSshAgent)}`);
assert.equal(prepared._resolvedSshAgentSocket, useSshAgent === true ? localAgent : undefined);
assert.equal(prepared._resolvedForwardingAgentSocket, forwardingAgent);
assert.equal(env.SSH_AUTH_SOCK, useSshAgent === false ? undefined : localAgent);
assert.ok(auth.sshArgs.includes(`ForwardAgent=${forwardingAgent}`));
}
});
test("Mosh forwards a Windows named-pipe agent through SSH_AUTH_SOCK", async () => {
const forwardingAgent = "\\\\.\\pipe\\openssh-ssh-agent";
const processMock = Object.create(process);
Object.defineProperty(processMock, "platform", { value: "win32" });
processMock.env = {};
const api = createMoshSessionApi({
os,
path,
fs,
process: processMock,
randomUUID: () => "fixed",
});
const prepared = {
useSshAgent: false,
agentForwarding: true,
_resolvedForwardingAgentSocket: forwardingAgent,
};
const env = api.applyMoshSshAgentEnvironment({}, prepared);
const auth = await api.buildMoshSshAuthArgs(prepared, "session-windows-forwarding");
assert.equal(env.SSH_AUTH_SOCK, forwardingAgent);
assert.ok(auth.sshArgs.includes("ForwardAgent=${SSH_AUTH_SOCK}"));
assert.equal(auth.sshArgs.some((arg) => arg.includes(forwardingAgent)), false);
});
test("Mosh automatic mode discovers custom local keys in preferred order", async (t) => {
const tempBase = makeTmp();
const fakeHome = path.join(tempBase, "home");
const sshDir = path.join(fakeHome, ".ssh");
fs.mkdirSync(sshDir, { recursive: true });
fs.writeFileSync(path.join(sshDir, "id_work"), "PRIVATE KEY");
fs.writeFileSync(path.join(sshDir, "id_ed25519"), "PRIVATE KEY");
fs.writeFileSync(path.join(sshDir, "id_rsa.pub"), "PUBLIC KEY");
t.after(() => fs.rmSync(tempBase, { recursive: true, force: true }));
const api = createMoshSessionApi({
os: { ...os, homedir: () => fakeHome },
path,
fs,
process,
randomUUID: () => "fixed",
});
const auth = await api.buildMoshSshAuthArgs({ authMethod: "auto" }, "session-auto");
assert.deepEqual(auth.sshArgs, [
"-i", path.join(sshDir, "id_ed25519"),
"-i", path.join(sshDir, "id_work"),
"-o", "StrictHostKeyChecking=ask",
]);
assert.deepEqual(auth.identityFilePaths, [
path.join(sshDir, "id_ed25519"),
path.join(sshDir, "id_work"),
]);
const agentFallback = await api.buildMoshSshAuthArgs({
authMethod: "auto",
useSshAgent: true,
identitiesOnly: false,
}, "session-auto-agent");
assert.deepEqual(agentFallback.sshArgs, [
"-i", path.join(sshDir, "id_ed25519"),
"-i", path.join(sshDir, "id_work"),
"-o", "StrictHostKeyChecking=ask",
]);
assert.deepEqual(agentFallback.identityFilePaths, [
path.join(sshDir, "id_ed25519"),
path.join(sshDir, "id_work"),
]);
});
test("removed Mosh client detection APIs are not exposed to the renderer", () => {
const bridgeSource = fs.readFileSync(path.join(__dirname, "terminalBridge.cjs"), "utf8");
const preloadSource = fs.readFileSync(path.join(__dirname, "..", "preload.cjs"), "utf8");
const globalTypes = fs.readFileSync(path.join(__dirname, "..", "..", "global.d.ts"), "utf8");
for (const source of [bridgeSource, preloadSource, globalTypes]) {
assert.equal(source.includes("detectMoshClient"), false);
assert.equal(source.includes("pickMoshClient"), false);
assert.equal(source.includes("netcatty:mosh:detectClient"), false);
assert.equal(source.includes("netcatty:mosh:pickClient"), false);
}
});
test("Cygwin / terminfo helpers are gone from the mosh session module", () => {
const source = fs.readFileSync(path.join(__dirname, "terminalBridge", "moshSession.cjs"), "utf8");
assert.equal(source.includes("toCygwinPath"), false);
assert.equal(source.includes("findBundledMoshDllDir"), false);
assert.equal(source.includes("findBundledMoshTerminfoDir"), false);
assert.equal(source.includes("cygwin1"), false);
});