"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { EventEmitter } = require("node:events"); const { setImmediate: nextTurn } = require("node:timers/promises"); const { LatencyMonitor, measurePing, parsePing } = require("../src/main/sessions/latencyMonitor"); const { SessionManager } = require("../src/main/sessions/sessionManager"); test("ICMP reply parser reads every reply line on macOS, Linux and localized Windows, never statistics lines", () => { const { parsePingReplies } = require("../src/main/sessions/latencyMonitor"); for (const reply of ["64 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=24.1 ms", "Antwort von 192.0.2.1: Bytes=32 Zeit=24,1ms TTL=64", "16 bytes from 2001:db8::1, icmp_seq=0 hlim=64 time=24.1 ms"]) assert.equal(parsePing(reply), 24.1); for (const reply of ["100% packet loss", "Request timed out.", "Minimum = 5ms, Maximum = 6ms, Average = 5ms", "Reply from 192.0.2.1: bytes=32 time<1ms TTL=128"]) assert.equal(parsePing(reply), null); const windowsCzech = "Odpověď od 192.0.2.1: bajty=32 čas=5ms TTL=128\r\nOdpověď od 192.0.2.1: bajty=32 čas=7ms TTL=128\r\nVypršel časový limit žádosti.\r\n Minimum = 5ms, Maximum = 7ms, Průměr = 6ms"; assert.deepEqual(parsePingReplies(windowsCzech).map((reply) => reply.ms), [5, 7]); assert.deepEqual(parsePingReplies("Reply from 192.0.2.1: bytes=32 time<1ms TTL=128"), [{ ms: 1, lessThan: true }]); }); test("ICMP runs 3 probes with fixed arguments and no shell; reports median, loss and concrete failure reasons", async () => { for (const platform of ["win32", "darwin", "linux"]) { for (const address of ["192.0.2.1", "2001:db8::1"]) { let call; const result = await measurePing(address, { platform, run(executable, args, options, callback) { call = { executable, args, options }; callback(null, "64 bytes: ttl=64 time=31 ms\n64 bytes: ttl=64 time=40 ms\n64 bytes: ttl=64 time=20 ms\n"); } }); assert.equal(result.ms, 31); assert.equal(result.loss, 0); assert.equal(result.status, "ok"); assert.equal(result.family, address.includes(":") ? 6 : 4); assert.equal(call.args.at(-1), address); assert.equal(call.options.shell, false); assert.ok(call.args.includes("3")); if (platform === "darwin") assert.equal(call.executable, address.includes(":") ? "/sbin/ping6" : "/sbin/ping"); } } const partial = await measurePing("192.0.2.1", { platform: "linux", run(_e, _a, _o, cb) { cb(Object.assign(new Error("exit 1"), { code: 1 }), "64 bytes: ttl=64 time=10 ms\n"); } }); assert.equal(partial.ms, 10); assert.ok(Math.abs(partial.loss - 2 / 3) < 1e-9); const untrusted = await measurePing("host;touch /tmp/unsafe", { run() { assert.fail("Untrusted command executed"); } }); assert.equal(untrusted.ms, null); assert.equal(untrusted.status, "error"); const cases = [ [Object.assign(new Error("exit 2"), { code: 2 }), "", "timeout"], [Object.assign(new Error("spawn ping ENOENT"), { code: "ENOENT" }), "", "unsupported"], [Object.assign(new Error("exit 68"), { code: 68 }), "ping: sendto: Operation not permitted", "denied"], [Object.assign(new Error("exit 1"), { code: 1 }), "Reply from 192.0.2.254: Destination host unreachable.", "unreachable"] ]; for (const [error, output, status] of cases) { const result = await measurePing("192.0.2.1", { platform: "darwin", run(_e, _a, _o, cb) { cb(error, output, ""); } }); assert.equal(result.status, status); assert.equal(result.ms, null); assert.ok(result.reason.length > 10); } }); test("monitor takes several monotonic SSH samples, keeps ICMP independent, never overlaps and stops", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); let now = 100, calls = 0; const values = []; const durations = [31.25, 10, 20]; const monitor = new LatencyMonitor({ now: () => now, ping: async () => ({ ms: null, status: "timeout", reason: "blocked" }), intervalMs: 10000 }); const stop = monitor.start({ address: "192.0.2.1" }, async () => { now += durations[calls % 3]; calls++; }, (v) => values.push(v)); for (let index = 0; index < 5; index += 1) await nextTurn(); assert.equal(calls, 3); assert.equal(values[0].sshRttMs, 20, "median of three samples"); assert.equal(values[0].sshRttMethod, "command"); assert.equal(values[0].pingMs, null); assert.equal(values[0].pingStatus, "timeout"); t.mock.timers.tick(10000); for (let index = 0; index < 5; index += 1) await nextTurn(); assert.equal(calls, 6); stop(); t.mock.timers.tick(60000); await nextTurn(); assert.equal(calls, 6); assert.equal(values.length, 2); }); test("transport measurements are used directly and a failed SSH round trip does not hide ICMP", async () => { const values = [], stops = []; const monitor = new LatencyMonitor({ ping: async (address) => address === "192.0.2.1" ? { ms: 24, status: "ok", loss: 0, family: 4 } : { ms: null, status: "timeout" } }); stops.push(monitor.start({ address: "192.0.2.1" }, async () => { throw new Error("Timeout"); }, (v) => values.push(["a", v]))); stops.push(monitor.start({ address: "192.0.2.2" }, async () => ({ ms: 3.5, method: "transport" }), (v) => values.push(["b", v]))); for (let index = 0; index < 6; index += 1) await nextTurn(); stops.forEach((stop) => stop()); const a = values.find(([id]) => id === "a")[1]; const b = values.find(([id]) => id === "b")[1]; assert.equal(a.pingMs, 24); assert.equal(a.sshRttMs, null); assert.match(a.sshRttError, /Timeout/); assert.equal(b.pingMs, null); assert.equal(b.sshRttMs, 3.5); assert.equal(b.sshRttMethod, "transport"); }); test("SSH round trip uses the transport keepalive request and falls back to a shell-neutral no-op", async () => { const manager = new SessionManager({}); let pings = 0; const client = { _callbacks: [], _protocol: { ping() { pings++; setTimeout(() => client._callbacks.shift()(true), 5); } } }; const result = await manager._sshRoundTrip({ client }); assert.equal(result.method, "transport"); assert.ok(result.ms >= 0); assert.equal(pings, 1); let command; const channel = new EventEmitter(); channel.stderr = new EventEmitter(); const fallback = manager._sshRoundTrip({ client: { exec(cmd, cb) { command = cmd; cb(null, channel); setImmediate(() => channel.emit("close", 0)); } } }); assert.equal((await fallback).method, "command"); assert.equal(command, "exit 0", "valid in sh, cmd.exe and PowerShell"); }); test("connected sessions start/stop monitoring and reject stale results after reconnect", async () => { const fingerprint = "ab".repeat(32), callbacks = [], stops = []; const manager = new SessionManager({ profileService: { get: () => ({ id: "p", host: "192.0.2.1", port: 22, username: "user", authMethod: "password" }), getHostKey: () => fingerprint, markUsed() {} }, latencyMonitor: { start(record, request, publish) { callbacks.push({ record, request, publish }); const stop = { count: 0 }; stops.push(stop); return () => { stop.count++; }; } }, clientFactory() { const client = new EventEmitter(); client.connect = () => setImmediate(() => client.emit("ready")); client.end = () => {}; client.shell = (_opts, cb) => { const stream = new EventEmitter(); stream.end = () => {}; cb(null, stream); }; client.exec = (command, cb) => { assert.equal(command, "exit 0"); const stream = new EventEmitter(); stream.stderr = new EventEmitter(); cb(null, stream); setImmediate(() => stream.emit("close", 0)); }; return client; } }); try { await manager.connect({ sessionId: "s", profileId: "p", password: "secret" }); assert.equal(callbacks.length, 1); await callbacks[0].request(); callbacks[0].publish({ pingMs: 24, sshRttMs: 31 }); assert.equal(manager.list()[0].latency.sshRttMs, 31); await manager.connect({ sessionId: "s", profileId: "p", password: "secret" }); assert.equal(stops[0].count, 1); assert.equal(manager.list()[0].latency.sshRttMs, null); callbacks[0].publish({ pingMs: 999, sshRttMs: 999 }); assert.equal(manager.list()[0].latency.sshRttMs, null); callbacks[1].publish({ pingMs: null, sshRttMs: 12 }); assert.equal(manager.list()[0].state, "connected"); } finally { await manager.disconnectAll(); } assert.equal(stops[1].count, 1); }); test("SSH request timeout closes the channel, releases pending work and accepts no late result", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); let closed = 0; const channel = new EventEmitter(); channel.stderr = new EventEmitter(); channel.close = () => { closed++; }; const record = { client: { exec(_cmd, cb) { cb(null, channel); } } }; const manager = new SessionManager({}); const result = assert.rejects(manager._execFixed(record, "true", 5000), { code: "REMOTE_COMMAND_TIMEOUT" }); t.mock.timers.tick(5000); await result; assert.equal(closed, 1); assert.equal(record.pendingExec.size, 0); channel.emit("close", 0); }); test("terminal latency displays milliseconds, concrete unavailable reasons and resets on disconnect", () => { const fs = require("node:fs"), vm = require("node:vm"); const source = fs.readFileSync(require.resolve("../src/renderer/app.js"), "utf8"); const fn = source.slice(source.indexOf("function renderTerminalLatency()"), source.indexOf("async function connectTerminal")); const element = {}, state = { terminalStateKind: "connected", terminalLatency: { pingMs: 24, pingStatus: "ok", pingLoss: 1 / 3, pingFamily: 6, sshRttMs: 31.25, sshRttMethod: "transport", measuredAt: "x" } }; const { createI18n } = require("../src/renderer/i18n"); for (const language of ["cs", "de", "en"]) { const i18n = createI18n(language); vm.runInNewContext(fn + "\nrenderTerminalLatency();", { state, $: () => element, t: (key, values) => i18n.t(key, values) }); assert.match(element.textContent, /24\.0 ms IPv6/); assert.match(element.textContent, /33 %/); assert.match(element.textContent, /31\.3 ms/); assert.equal(element.hidden, false); } const en = createI18n("en"); state.terminalLatency = { pingMs: null, pingStatus: "timeout", sshRttMs: 4.2, measuredAt: "x" }; vm.runInNewContext(fn + "\nrenderTerminalLatency();", { state, $: () => element, t: (key, values) => en.t(key, values) }); assert.match(element.textContent, /firewall may block ICMP/); assert.match(element.textContent, /4\.2 ms/, "SSH RTT stays visible when ICMP is blocked"); state.terminalLatency = null; vm.runInNewContext(fn + "\nrenderTerminalLatency();", { state, $: () => element, t: (key, values) => en.t(key, values) }); assert.match(element.textContent, /measuring/); state.terminalStateKind = "disconnected"; vm.runInNewContext(fn + "\nrenderTerminalLatency();", { state, $: () => element, t: (key, values) => en.t(key, values) }); assert.equal(element.hidden, true); });