import test from "node:test"; import assert from "node:assert/strict"; import { createTerminalSessionStarters, } from "./createTerminalSessionStarters"; const noop = () => undefined; const ENCRYPTED_CREDENTIAL_PLACEHOLDER = "enc:v1:djEwdGVzdAAAAAAAAAAAAAAAAA=="; const armSudoPrompt = ( autofill: { armForCommand: (command: string) => void } | null, ): string => { autofill?.armForCommand("sudo whoami"); return "[sudo] password for alice: "; }; test("startMosh enables sudo autofill with the host saved password", async () => { let detected = ""; let probes = 0; let ready: (() => void) | undefined; let resolveDetected!: () => void; const detectionDone = new Promise((resolve) => { resolveDetected = resolve; }); let onData: ((data: string) => void) | null = null; const sent: string[] = []; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => "mosh-session", onMoshSessionReady: (_id: string, callback: () => void) => { ready = callback; return noop; }, getSessionDistroInfo: async () => { probes++; return { success: true, stdout: "ID=ubuntu" }; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: (_id: string, cb: (data: string) => void) => { onData = cb; return noop; }, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (_id: string, data: string) => sent.push(data), resizeSession: noop, }; const sudoAutofillRef = { current: null }; const ctx = { host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "saved-secret", }, onOsDetected: (_id: string, distro: string) => { detected = distro; resolveDetected(); }, keys: [], identities: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sudoAutofillPassword: "saved-secret", onSudoHint: () => true, sessionRef: { current: null }, hasConnectedRef: { current: true }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, sudoAutofillRef, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(probes, 0, "wait for the Mosh handshake before probing"); ready?.(); await detectionDone; assert.equal(detected, "ubuntu"); assert.equal(probes, 1); onData?.(armSudoPrompt(sudoAutofillRef.current)); sudoAutofillRef.current?.confirmFill(); assert.deepEqual(sent, ["saved-secret\n"]); }); test("startSSH accepts jump host local identity file paths with unreadable saved passwords", async () => { let capturedOptions: Record | null = null; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, startTelnetSession: async () => "telnet-session", startMoshSession: async () => "mosh-session", startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, }, keys: [], resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", authMethod: "key", password: ENCRYPTED_CREDENTIAL_PLACEHOLDER, identityFilePaths: ["/Users/alice/.ssh/jump_ed25519"], }], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: (message: string) => { error = message; }, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startSSH(term as never); assert.equal(error, ""); assert.ok(capturedOptions); const jumpHosts = capturedOptions.jumpHosts as Array>; assert.equal(jumpHosts[0]?.password, undefined); assert.deepEqual(jumpHosts[0]?.identityFilePaths, ["/Users/alice/.ssh/jump_ed25519"]); }); test("startSSH does not use stale local key paths when selected key material is unavailable", async () => { let capturedOptions: Record | null = null; let needsAuth = false; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, startTelnetSession: async () => "telnet-session", startMoshSession: async () => "mosh-session", startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", authMethod: "key", identityFileId: "bad-key", identityFilePaths: ["/Users/alice/.ssh/stale_ed25519"], }, keys: [{ id: "bad-key", label: "Imported key", source: "imported", privateKey: ENCRYPTED_CREDENTIAL_PLACEHOLDER, }], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: (value: boolean) => { needsAuth = value; }, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startSSH(term as never); assert.equal(capturedOptions, null); assert.equal(needsAuth, true); }); test("startSSH does not use stale jump host local key paths when selected key material is unavailable", async () => { let capturedOptions: Record | null = null; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, startTelnetSession: async () => "telnet-session", startMoshSession: async () => "mosh-session", startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, }, keys: [{ id: "bad-jump-key", label: "Jump key", source: "imported", privateKey: ENCRYPTED_CREDENTIAL_PLACEHOLDER, }], resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", authMethod: "key", identityFileId: "bad-jump-key", identityFilePaths: ["/Users/alice/.ssh/stale_jump_ed25519"], }], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: (message: string) => { error = message; }, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startSSH(term as never); assert.equal(capturedOptions, null); assert.match(error, /jump host has saved credentials/i); }); test("startMosh does not pass legacy configured mosh client paths to the backend", async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async (options: Record) => { capturedOptions = options; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", port: 2200, }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: { terminalEmulationType: "xterm-256color", moshClientPath: "/usr/local/bin/mosh-client", }, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null as (() => void) | null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.ok(capturedOptions); assert.equal("moshClientPath" in capturedOptions, false); assert.equal(capturedOptions.hostname, "example.test"); assert.equal(capturedOptions.port, 2200); }); test("startMosh passes the saved password to the mosh backend", async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async (options: Record) => { capturedOptions = options; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", password: "saved-secret", port: 2200, }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null as (() => void) | null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.ok(capturedOptions); assert.equal(capturedOptions.username, "alice"); assert.equal(capturedOptions.password, "saved-secret"); }); test("startMosh passes configured key material to the mosh backend", async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async (options: Record) => { capturedOptions = options; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", password: "wrong-password", authMethod: "key", identityFileId: "key-1", identityFilePaths: ["/should/not/be/used"], port: 2200, }, keys: [{ id: "key-1", label: "Deploy key", privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nkey\n-----END OPENSSH PRIVATE KEY-----", passphrase: "key-passphrase", }], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.ok(capturedOptions); assert.equal(capturedOptions.password, "wrong-password"); assert.equal(capturedOptions.privateKey, "-----BEGIN OPENSSH PRIVATE KEY-----\nkey\n-----END OPENSSH PRIVATE KEY-----"); assert.equal(capturedOptions.keyId, "key-1"); assert.equal(capturedOptions.passphrase, "key-passphrase"); assert.equal(capturedOptions.identityFilePaths, undefined); }); test("startMosh asks for credential re-entry when saved key material cannot be decrypted", async () => { let started = false; let needsAuth = false; let retryMessage: string | null = null; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { started = true; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", authMethod: "key", identityFileId: "key-1", port: 2200, }, keys: [{ id: "key-1", label: "Deploy key", privateKey: "enc:v1:djEwdGVzdAAAAAAAAAAAAAAAAA==", }], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: (value: boolean) => { needsAuth = value; }, setAuthRetryMessage: (message: string | null) => { retryMessage = message; }, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(started, false); assert.equal(needsAuth, true); assert.match(retryMessage || "", /Saved credentials cannot be decrypted/); }); test("startMosh does not use stale local key paths when selected key material is unavailable", async () => { let started = false; let needsAuth = false; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { started = true; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", authMethod: "key", identityFileId: "key-1", identityFilePaths: ["/Users/alice/.ssh/stale_ed25519"], port: 2200, }, keys: [{ id: "key-1", label: "Deploy key", privateKey: "enc:v1:djEwdGVzdAAAAAAAAAAAAAAAAA==", }], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null as (() => void) | null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: (value: boolean) => { needsAuth = value; }, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(started, false); assert.equal(needsAuth, true); }); test("startMosh omits identity file paths when password auth is explicit", async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async (options: Record) => { capturedOptions = options; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", authMethod: "password", requiresMfa: true, password: "saved-secret", identityFilePaths: ["/should/not/be/used"], port: 2200, }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.ok(capturedOptions); assert.equal(capturedOptions.password, "saved-secret"); assert.equal(capturedOptions.requiresMfa, true); assert.equal(capturedOptions.identityFilePaths, undefined); }); test("startMosh rejects missing saved proxy profiles", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { started = true; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", port: 2200, proxyProfileId: "missing-proxy", }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: (message: string) => { error = message; }, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(started, false); assert.match(error, /Saved proxy/); }); test("startMosh rejects missing proxy identities before the unsupported proxy guard", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { started = true; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", port: 2200, proxyConfig: { type: "http", host: "proxy.example.com", port: 3128, identityId: "missing-identity", }, }, keys: [], identities: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: (message: string) => { error = message; }, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(started, false); assert.match(error, /Proxy identity/); assert.match(error, /missing/); }); test("startMosh rejects incomplete proxy identities before the unsupported proxy guard", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { started = true; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", port: 2200, proxyConfig: { type: "http", host: "proxy.example.com", port: 3128, identityId: "identity-1", }, }, keys: [], identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", created: 1, }], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: (message: string) => { error = message; }, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(started, false); assert.match(error, /Proxy identity/); assert.match(error, /incomplete/); }); test("startMosh does not connect when a proxy identity password is encrypted", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { started = true; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", port: 2200, proxyConfig: { type: "http", host: "proxy.example.com", port: 3128, identityId: "identity-1", }, }, keys: [], identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", password: ENCRYPTED_CREDENTIAL_PLACEHOLDER, created: 1, }], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: (message: string) => { error = message; }, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(started, false); assert.match(error, /Mosh does not support proxy/); }); test("startMosh rejects configured proxies instead of connecting directly", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { started = true; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", port: 2200, proxyProfileId: "proxy-1", proxyConfig: { type: "http", host: "proxy.example.com", port: 3128 }, }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: (message: string) => { error = message; }, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(started, false); assert.match(error, /Mosh does not support proxy/); }); test("startMosh rejects jump host chains instead of connecting directly", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { started = true; return "mosh-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, port: 2200, }, keys: [], resolvedChainHosts: [{ id: "jump-1", hostname: "jump.example.test" }], sessionId: "session-1", terminalSettings: {}, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: (message: string) => { error = message; }, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(started, false); assert.match(error, /Mosh does not support jump host chains/); }); test("startMosh defers startup commands until mosh-client is ready", async () => { const sent: string[] = []; let readyCb: ((evt: { sessionId: string }) => void) | null = null; let readyDisposed = false; let dataCb: ((data: string, meta?: { moshHandshake?: boolean }) => void) | null = null; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => "mosh-session", startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: (_id: string, cb: (data: string, meta?: { moshHandshake?: boolean }) => void) => { dataCb = cb; return noop; }, onSessionExit: () => noop, onMoshSessionReady: (_id: string, cb: (evt: { sessionId: string }) => void) => { readyCb = cb; return () => { readyDisposed = true; }; }, onChainProgress: () => noop, writeToSession: (_id: string, data: string) => sent.push(data), resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", }, keys: [], identities: [], resolvedChainHosts: [], sessionId: "session-1", startupCommand: "echo from-snippet", terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, sessionRef: { current: null as string | null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null as (() => void) | null }, disposeExitRef: { current: null as (() => void) | null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: (status: string) => { if (status === "connected") ctx.hasConnectedRef.current = true; }, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, modes: {}, options: {}, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); // Ordinary handshake output must not dismiss the connection overlay before // the interactive Mosh client is ready. dataCb?.("ssh login banner\r\n", { moshHandshake: true }); assert.equal(ctx.hasConnectedRef.current, false); assert.deepEqual(sent, []); assert.ok(readyCb, "expected onMoshSessionReady subscription"); // Hibernate detaches exit listeners without closing the session — ready work // must survive that dispose so startup can still run after wake. ctx.disposeExitRef.current?.(); assert.equal(readyDisposed, false); readyCb?.({ sessionId: "mosh-session" }); await new Promise((resolve) => setTimeout(resolve, 5)); assert.equal(ctx.hasConnectedRef.current, true); assert.ok(sent.some((chunk) => chunk.includes("echo from-snippet"))); }); test("startMosh disposes ready subscription when startMoshSession rejects", async () => { let readyDisposed = false; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { throw new Error("mosh-client missing"); }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onMoshSessionReady: () => () => { readyDisposed = true; }, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", }, keys: [], identities: [], resolvedChainHosts: [], sessionId: "session-1", startupCommand: "echo startup", terminalSettings: {}, terminalBackend, sessionRef: { current: null as string | null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null as (() => void) | null }, disposeExitRef: { current: null as (() => void) | null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); assert.equal(readyDisposed, true); }); test("startMosh still runs startup when ready fires during startMoshSession await", async () => { const sent: string[] = []; let readyCb: ((evt: { sessionId: string }) => void) | null = null; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => { // Simulate a fast handshake that emits ready before the await resumes. readyCb?.({ sessionId: "session-1" }); return "session-1"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onMoshSessionReady: (_id: string, cb: (evt: { sessionId: string }) => void) => { readyCb = cb; return noop; }, onChainProgress: () => noop, writeToSession: (_id: string, data: string) => sent.push(data), resizeSession: noop, }; const ctx = { host: { id: "host-1", label: "Example", hostname: "example.test", username: "alice", }, keys: [], identities: [], resolvedChainHosts: [], sessionId: "session-1", startupCommand: "echo startup", terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, sessionRef: { current: null as string | null }, hasConnectedRef: { current: false }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null as (() => void) | null }, disposeExitRef: { current: null as (() => void) | null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: noop, writeln: noop, scrollToBottom: noop, modes: {}, options: {}, }; await createTerminalSessionStarters(ctx as never).startMosh(term as never); await new Promise((resolve) => setTimeout(resolve, 5)); assert.ok(sent.some((chunk) => chunk.includes("echo startup"))); });