import test from "node:test"; import assert from "node:assert/strict"; import { createTerminalSessionStarters } from "./createTerminalSessionStarters"; const noop = () => undefined; const armSudoPrompt = ( autofill: { armForCommand: (command: string) => void } | null, ): string => { autofill?.armForCommand("sudo whoami"); return "[sudo] password for alice: "; }; const makeBackend = ( onStartEt: (options: Record) => void = noop, ) => ({ backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, etAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => "ssh-session", startTelnetSession: async () => "telnet-session", startMoshSession: async () => "mosh-session", startEtSession: async (options: Record) => { onStartEt(options); return "et-session"; }, startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }); const makeCtx = ( host: Record, resolvedChainHosts: Array>, terminalBackend: ReturnType, sinks: { setError?: (m: string) => void } = {}, ) => ({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", etEnabled: true, ...host, }, 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: sinks.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, }; test("startEt enables sudo autofill with the host saved password", async () => { let onData: ((data: string) => void) | null = null; const sent: string[] = []; const backend = { ...makeBackend(), onSessionData: (_id: string, cb: (data: string) => void) => { onData = cb; return noop; }, writeToSession: (_id: string, data: string) => sent.push(data), }; const sudoAutofillRef = { current: null }; const ctx = { ...makeCtx({ password: "saved-secret", }, [], backend), sudoAutofillRef, sudoAutofillPassword: "saved-secret", onSudoHint: () => true, }; await createTerminalSessionStarters(ctx as never).startEt(term as never); onData?.(armSudoPrompt(sudoAutofillRef.current)); sudoAutofillRef.current?.confirmFill(); assert.deepEqual(sent, ["saved-secret\n"]); }); test("startEt fails loudly when a configured jump host cannot be resolved", async () => { let started = false; let error = ""; const backend = makeBackend(() => { started = true; }); // hostChain references jump-1, but resolvedChainHosts is empty (missing). const ctx = makeCtx( { hostChain: { hostIds: ["jump-1"] } }, [], backend, { setError: (m) => { error = m; } }, ); await createTerminalSessionStarters(ctx as never).startEt(term as never); // Must NOT silently fall back to a direct connection. assert.equal(started, false); assert.match(error, /jump host is missing/i); assert.match(error, /jump-1/); }); test("startEt rejects a configured chain with more than one jump host even if under-resolved", async () => { let started = false; let error = ""; const backend = makeBackend(() => { started = true; }); // Two configured hops but only one resolved — a resolved-length check alone // would wrongly let this through. const ctx = makeCtx( { hostChain: { hostIds: ["jump-1", "jump-2"] } }, [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", }], backend, { setError: (m) => { error = m; } }, ); await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.equal(started, false); assert.match(error, /at most one jump host/i); }); test("startEt rejects missing proxy identities on the target host before unsupported proxy", async () => { let started = false; let error = ""; const backend = makeBackend(() => { started = true; }); const ctx = makeCtx( { proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "missing-identity", }, }, [], backend, { setError: (m) => { error = m; } }, ); await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.equal(started, false); assert.match(error, /Proxy identity for "Target" is missing/); }); test("startEt rejects incomplete proxy identities on the target host before unsupported proxy", async () => { let started = false; let error = ""; const backend = makeBackend(() => { started = true; }); const ctx = { ...makeCtx( { proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "identity-1", }, }, [], backend, { setError: (m) => { error = m; } }, ), identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", created: 1, }], }; await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.equal(started, false); assert.match(error, /Proxy identity for "Target" is incomplete/); }); test("startEt rejects missing saved proxy profiles on jump hosts", async () => { let started = false; let error = ""; const backend = makeBackend(() => { started = true; }); const ctx = makeCtx( { hostChain: { hostIds: ["jump-1"] } }, [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", proxyProfileId: "missing-proxy", }], backend, { setError: (m) => { error = m; } }, ); await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.equal(started, false); assert.match(error, /Saved proxy for jump host "Jump" is missing/); }); test("startEt rejects missing proxy identities on jump hosts", async () => { let started = false; let error = ""; const backend = makeBackend(() => { started = true; }); const ctx = makeCtx( { hostChain: { hostIds: ["jump-1"] } }, [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "missing-identity", }, }], backend, { setError: (m) => { error = m; } }, ); await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.equal(started, false); assert.match(error, /Proxy identity for "Jump" is missing/); }); test("startEt rejects incomplete proxy identities on jump hosts", async () => { let started = false; let error = ""; const backend = makeBackend(() => { started = true; }); const ctx = { ...makeCtx( { hostChain: { hostIds: ["jump-1"] } }, [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "identity-1", }, }], backend, { setError: (m) => { error = m; } }, ), identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", created: 1, }], }; await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.equal(started, false); assert.match(error, /Proxy identity for "Jump" is incomplete/); }); test("startEt connects with a single resolved jump host", async () => { let captured: Record | null = null; let error = ""; const backend = makeBackend((options) => { captured = options; }); const ctx = makeCtx( { hostChain: { hostIds: ["jump-1"] } }, [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", // key auth with no saved key reference → local identity file fallback authMethod: "key", identityFilePaths: ["/Users/alice/.ssh/jump_ed25519"], }], backend, { setError: (m) => { error = m; } }, ); await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.equal(error, ""); assert.ok(captured); const jumpHosts = captured.jumpHosts as Array>; assert.equal(jumpHosts.length, 1); assert.equal(jumpHosts[0]?.hostname, "jump.example.test"); // Local identity file fallback is forwarded for the hop. assert.deepEqual(jumpHosts[0]?.identityFilePaths, ["/Users/alice/.ssh/jump_ed25519"]); }); test("startEt forwards a jump host's custom ET port", async () => { let captured: Record | null = null; const backend = makeBackend((options) => { captured = options; }); const ctx = makeCtx( { hostChain: { hostIds: ["jump-1"] } }, [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", etPort: 9022, }], backend, ); await createTerminalSessionStarters(ctx as never).startEt(term as never); const jumpHosts = (captured as Record).jumpHosts as Array>; assert.equal(jumpHosts[0]?.etPort, 9022); }); test("startEt forwards a jump host reference key path as an identity file", async () => { let captured: Record | null = null; const backend = makeBackend((options) => { captured = options; }); const ctx = { ...makeCtx( { hostChain: { hostIds: ["jump-1"] } }, [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", authMethod: "key", identityFileId: "ref-key", }], backend, ), keys: [{ id: "ref-key", label: "Reference key", source: "reference", filePath: "/Users/alice/.ssh/jump_reference_ed25519", // reference keys carry no inline privateKey material }], }; await createTerminalSessionStarters(ctx as never).startEt(term as never); const jumpHosts = (captured as Record).jumpHosts as Array>; // privateKey must be omitted for a reference key, and the on-disk path // forwarded as an IdentityFile instead of being dropped. assert.equal(jumpHosts[0]?.privateKey, undefined); assert.deepEqual(jumpHosts[0]?.identityFilePaths, ["/Users/alice/.ssh/jump_reference_ed25519"]); }); test("startEt keeps a jump private key when agent filtering is unavailable", async () => { let captured: Record | null = null; const backend = makeBackend((options) => { captured = options; }); const ctx = { ...makeCtx( { hostChain: { hostIds: ["jump-1"] } }, [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", authMethod: "key", identityFileId: "jump-key", useSshAgent: true, }], backend, ), keys: [{ id: "jump-key", label: "Jump key", type: "ED25519", category: "key", source: "imported", created: 1, privateKey: "JUMP PRIVATE KEY", passphrase: "jump-passphrase", }], }; await createTerminalSessionStarters(ctx as never).startEt(term as never); const jumpHosts = (captured as Record).jumpHosts as Array>; assert.equal(jumpHosts[0]?.useSshAgent, false); assert.equal(jumpHosts[0]?.privateKey, "JUMP PRIVATE KEY"); assert.equal(jumpHosts[0]?.passphrase, "jump-passphrase"); }); test("startEt connects directly when no jump host is configured", async () => { let captured: Record | null = null; let error = ""; const backend = makeBackend((options) => { captured = options; }); const ctx = makeCtx({}, [], backend, { setError: (m) => { error = m; } }); await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.equal(error, ""); assert.ok(captured); assert.equal(captured.jumpHosts, undefined); }); test("startEt forwards known hosts and algorithm options for stats companion parity", async () => { let captured: Record | null = null; const knownHosts = [{ id: "kh-1", hostname: "target.example.test", port: 22, keyType: "ssh-ed25519", fingerprint: "SHA256:trusted", publicKey: "", discoveredAt: 1, }]; const algorithms = { cipher: ["aes128-cbc"] }; const backend = makeBackend((options) => { captured = options; }); const ctx = { ...makeCtx( { legacyAlgorithms: true, skipEcdsaHostKey: true, algorithms, }, [], backend, ), knownHosts, }; await createTerminalSessionStarters(ctx as never).startEt(term as never); assert.ok(captured); assert.equal(captured.knownHosts, knownHosts); assert.equal(captured.legacyAlgorithms, true); assert.equal(captured.skipEcdsaHostKey, true); assert.equal(captured.algorithmOverrides, algorithms); });