import test from "node:test"; import assert from "node:assert/strict"; import { createTerminalSessionStarters, getMissingChainHostIds, } from "./createTerminalSessionStarters"; import { createPromptLineBreakState } from "./promptLineBreak"; import { resolveStartupCommand } from "./terminalStartupCommands"; import { pasteTextIntoTerminal } from "./terminalUserPaste"; import { shouldSuppressHostStartupCommandOnReconnect } from "../restoredSessionGate"; const noop = () => undefined; const ENCRYPTED_CREDENTIAL_PLACEHOLDER = "enc:v1:djEwdGVzdAAAAAAAAAAAAAAAAA=="; const armSudoPrompt = ( autofill: { armForCommand: (command: string) => void } | null, command = "sudo whoami", ): string => { autofill?.armForCommand(command); return "[sudo] password for alice: "; }; const createTermStub = (overrides: Record = {}) => ({ cols: 120, rows: 32, write: (_data: string, callback?: () => void) => callback?.(), writeln: noop, scrollToBottom: noop, ...overrides, }); const createStarterContext = (overrides: Record = {}) => ({ onSudoHint: () => true, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", }, keys: [], identities: [], knownHosts: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: {}, sessionRef: { current: null }, hasConnectedRef: { current: true }, hasRunStartupCommandRef: { current: false }, disposeDataRef: { current: null }, disposeExitRef: { current: null }, fitAddonRef: { current: null }, serializeAddonRef: { current: null }, pendingAuthRef: { current: null }, promptLineBreakStateRef: { current: createPromptLineBreakState() }, sudoAutofillRef: { current: null }, updateStatus: noop, setStatus: noop, setError: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, ...overrides, }); test("getMissingChainHostIds reports unresolved jump hosts", () => { assert.deepEqual( getMissingChainHostIds( { id: "host-1", label: "Example", hostname: "example.test", username: "alice", hostChain: { hostIds: ["jump-1", "jump-2"] }, } as never, [{ id: "jump-1" }] as never, ), ["jump-2"], ); }); test("startPluginConnection preserves the namespaced provider configuration and attaches the host session", async () => { let captured: NetcattyPluginConnectionStartRequest | null = null; const attached: string[] = []; const statuses: string[] = []; let progressLogs: string[] = []; const terminalBackend = { pluginConnectionAvailable: () => true, startPluginConnection: async (options: NetcattyPluginConnectionStartRequest) => { captured = options; return { sessionId: options.sessionId, providerId: options.providerId, status: "connected" as const, diagnostics: [{ severity: "warning" as const, message: "Provider warning" }], }; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-plugin", label: "Custom protocol", hostname: "opaque.example", username: "", protocol: "plugin:com.example.transport.connection", pluginConnection: { providerId: "com.example.transport.connection", configuration: { endpoint: "opaque.example", secure: true }, authenticationProviderId: "com.example.transport.auth", credentialId: "credential-reference-1234", }, }, terminalBackend, sessionLog: { enabled: true, directory: "/logs", format: "html", timestampsEnabled: true, }, onSessionAttached: (id: string) => attached.push(id), updateStatus: (status: string) => statuses.push(status), setProgressLogs: (update: string[] | ((previous: string[]) => string[])) => { progressLogs = typeof update === "function" ? update(progressLogs) : update; }, }); await createTerminalSessionStarters(ctx as never).startPluginConnection(createTermStub() as never); assert.ok(captured?.requestId?.startsWith("plugin-connection-")); assert.equal(captured?.signal instanceof AbortSignal, true); const { requestId: _requestId, signal: _signal, ...capturedRequest } = captured as NetcattyPluginConnectionStartRequest & { signal?: AbortSignal; }; assert.deepEqual(capturedRequest, { sessionId: "session-1", protocol: "plugin:com.example.transport.connection", hostLabel: "Custom protocol", hostname: "opaque.example", providerId: "com.example.transport.connection", configuration: { endpoint: "opaque.example", secure: true }, columns: 120, rows: 32, sessionLog: { enabled: true, directory: "/logs", format: "html", timestampsEnabled: true, }, authenticationProviderId: "com.example.transport.auth", credential: { kind: "credential", id: "credential-reference-1234" }, }); assert.deepEqual(attached, ["session-1"]); assert.deepEqual(statuses, ["connected"]); assert.deepEqual(progressLogs, ["[Plugin warning] Provider warning"]); }); test("startPluginConnection cancels a pending Provider request when the terminal unmounts before cleanup runs", async () => { let captured: (NetcattyPluginConnectionStartRequest & { signal?: AbortSignal }) | null = null; let resolveStartEntered: (() => void) | null = null; const startEntered = new Promise((resolve) => { resolveStartEntered = resolve; }); const cancelledRequests: string[] = []; const attached: string[] = []; const terminalWrites: string[] = []; const isBootActiveRef = { current: true }; const terminalBackend = { pluginConnectionAvailable: () => true, startPluginConnection: async (options: NetcattyPluginConnectionStartRequest & { signal?: AbortSignal }) => { captured = options; resolveStartEntered?.(); await new Promise((_resolve, reject) => { options.signal?.addEventListener("abort", () => { reject(options.signal?.reason ?? new DOMException("Aborted", "AbortError")); }, { once: true }); }); throw new Error("pending start should have been aborted"); }, cancelPluginExtensionRequest: async (requestId: string) => { cancelledRequests.push(requestId); return true; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-plugin", label: "Custom protocol", hostname: "opaque.example", username: "", protocol: "plugin:com.example.transport.connection", pluginConnection: { providerId: "com.example.transport.connection", configuration: { endpoint: "opaque.example" }, authenticationProviderId: "com.example.transport.auth", }, }, terminalBackend, isBootActiveRef, onSessionAttached: (id: string) => attached.push(id), }); const term = createTermStub({ write: (data: string, callback?: () => void) => { terminalWrites.push(data); callback?.(); }, }); const start = createTerminalSessionStarters(ctx as never).startPluginConnection(term as never); await startEntered; const capturedRequest = captured; assert.ok(capturedRequest); assert.ok(capturedRequest.requestId); assert.equal(typeof ctx.disposeExitRef.current, "function"); isBootActiveRef.current = false; await start; assert.equal(capturedRequest.signal?.aborted, true); assert.deepEqual(cancelledRequests, [capturedRequest.requestId]); assert.deepEqual(attached, []); assert.deepEqual(terminalWrites, []); }); test("startPluginConnection waits for explicit Provider connected readiness before startup commands", async () => { let onData: ((data: string, meta?: { pluginPipelineIngressBytes?: number; pluginConnectionReady?: boolean }) => void) | null = null; const writes: Array<{ id: string; data: string; options?: Record }> = []; const statuses: string[] = []; const hasConnectedRef = { current: false }; const terminalBackend = { pluginConnectionAvailable: () => true, startPluginConnection: async (options: NetcattyPluginConnectionStartRequest) => ({ sessionId: options.sessionId, providerId: options.providerId, status: "connecting" as const, diagnostics: [], }), onSessionData: ( _id: string, cb: (data: string, meta?: { pluginPipelineIngressBytes?: number; pluginConnectionReady?: boolean }) => void, ) => { onData = cb; return noop; }, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: Record) => { writes.push({ id, data, options }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-plugin", label: "Custom protocol", hostname: "opaque.example", username: "", protocol: "plugin:com.example.transport.connection", pluginConnection: { providerId: "com.example.transport.connection", configuration: { endpoint: "opaque.example" }, }, startupCommand: "echo ready", }, terminalBackend, terminalSettings: { startupCommandDelayMs: 0 }, noAutoRun: true, hasConnectedRef, updateStatus: (status: string) => { statuses.push(status); if (status === "connected") hasConnectedRef.current = true; }, }); await createTerminalSessionStarters(ctx as never).startPluginConnection(createTermStub() as never); assert.equal(ctx.hasRunStartupCommandRef.current, false); onData?.("Provider banner before authentication completes\r\n", { pluginPipelineIngressBytes: 48 }); assert.deepEqual(statuses, []); assert.equal(ctx.hasConnectedRef.current, false); assert.equal(ctx.hasRunStartupCommandRef.current, false); onData?.("", { pluginPipelineIngressBytes: 0, pluginConnectionReady: true }); assert.deepEqual(statuses, ["connected"]); assert.equal(ctx.hasConnectedRef.current, true); assert.equal(ctx.hasRunStartupCommandRef.current, true); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(writes, [ { id: "session-1", data: "echo ready", options: { automated: true } }, ]); }); test("startPluginConnection displays status diagnostics when a Provider exits without an error message", async () => { let onExit: ((evt: { reason?: "error"; diagnostics?: Array<{ severity: "error" | "warning"; message: string }>; }) => void) | null = null; const terminalWrites: string[] = []; const terminalBackend = { pluginConnectionAvailable: () => true, startPluginConnection: async (options: NetcattyPluginConnectionStartRequest) => ({ sessionId: options.sessionId, providerId: options.providerId, status: "connected" as const, diagnostics: [], }), onSessionData: () => noop, onSessionExit: ( _id: string, cb: (evt: { reason?: "error"; diagnostics?: Array<{ severity: "error" | "warning"; message: string }>; }) => void, ) => { onExit = cb; return noop; }, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-plugin", label: "Custom protocol", hostname: "opaque.example", username: "", protocol: "plugin:com.example.transport.connection", pluginConnection: { providerId: "com.example.transport.connection", configuration: { endpoint: "opaque.example" }, }, }, terminalBackend, }); const term = createTermStub({ write: (data: string, callback?: () => void) => { terminalWrites.push(data); callback?.(); }, }); await createTerminalSessionStarters(ctx as never).startPluginConnection(term as never); assert.ok(onExit); onExit?.({ reason: "error", diagnostics: [ { severity: "error", message: "Host key mismatch" }, { severity: "warning", message: "Retry with a different credential" }, ], }); assert.deepEqual(terminalWrites, [ "\r\n[Plugin connection closed]\r\n[Plugin error] Host key mismatch\r\n[Plugin warning] Retry with a different credential\r\n", ]); }); test("startSSH forwards imported system agent authentication settings", async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "aws-sg", hostname: "1.1.1.1", username: "root", port: 2222, useSshAgent: true, identityAgent: "$SSH_AUTH_SOCK", identityFilePaths: ["~/.ssh/aws_root"], identitiesOnly: true, addKeysToAgent: "yes", useKeychain: true, }, terminalBackend, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.deepEqual( capturedOptions && { useSshAgent: capturedOptions.useSshAgent, identityAgent: capturedOptions.identityAgent, identityFilePaths: capturedOptions.identityFilePaths, identitiesOnly: capturedOptions.identitiesOnly, addKeysToAgent: capturedOptions.addKeysToAgent, useKeychain: capturedOptions.useKeychain, }, { useSshAgent: true, identityAgent: "$SSH_AUTH_SOCK", identityFilePaths: ["~/.ssh/aws_root"], identitiesOnly: true, addKeysToAgent: "yes", useKeychain: true, }, ); }); test("startSSH tells the bridge to skip shell discovery for network devices", async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ isNetworkDevice: true, reuseConnectionFromSessionIdRef: { current: "source-session" }, terminalBackend, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(capturedOptions?.sourceSessionId, "source-session"); assert.equal(capturedOptions?.skipShellPidDiscovery, true); }); test("startSSH requests a fresh transport for ordinary opens with connection automation", async () => { const captured: Record[] = []; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { captured.push(options); return `ssh-session-${captured.length}`; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const reuseConnectionFromSessionIdRef = { current: "source-session" as string | undefined }; const reuseAttempts: Array = []; let requiresFreshConnection = true; let committedAutomationSnapshots = 0; const automatedStarters = createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => requiresFreshConnection, onConnectAutomationSnapshotCommitted: () => { committedAutomationSnapshots += 1; }, reuseConnectionFromSessionIdRef, setConnectionReuseAttemptSourceId: (sourceSessionId: string | undefined) => { reuseAttempts.push(sourceSessionId); }, terminalBackend, }) as never); await automatedStarters.startSSH(createTermStub() as never); await automatedStarters.startSSH(createTermStub() as never); assert.equal(committedAutomationSnapshots, 0); requiresFreshConnection = false; await automatedStarters.startSSH(createTermStub() as never); assert.equal(committedAutomationSnapshots, 1); await createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => false, terminalBackend, }) as never).startSSH(createTermStub() as never); assert.equal(captured[0].reuseTransport, undefined); assert.equal(captured[0].sourceSessionId, "source-session"); assert.equal(captured[1].reuseTransport, false); assert.equal(captured[1].sourceSessionId, undefined); assert.equal(captured[2].reuseTransport, false); assert.equal(captured[2].sourceSessionId, undefined); assert.equal(captured[3].reuseTransport, false); assert.deepEqual(reuseAttempts, ["source-session", undefined, undefined]); }); test("startSSH requests a fresh transport for Duplicate Session clones", async () => { const captured: Record[] = []; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { captured.push(options); return `ssh-session-${captured.length}`; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; // Ordinary opens and Duplicate Session both require a new login. await createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => false, terminalBackend, }) as never).startSSH(createTermStub() as never); await createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => false, requireFreshConnection: true, terminalBackend, }) as never).startSSH(createTermStub() as never); const duplicateStarters = createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => false, requireFreshConnection: true, terminalBackend, }) as never); await duplicateStarters.startSSH(createTermStub() as never); await duplicateStarters.startSSH(createTermStub() as never); assert.equal(captured[0].reuseTransport, false); assert.equal(captured[0].sourceSessionId, undefined); assert.equal(captured[1].reuseTransport, false); assert.equal(captured[2].reuseTransport, false); }); test("startSSH dials a fresh transport once a pane has reconnected (#3293)", async () => { const captured: Record[] = []; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { captured.push(options); return `ssh-session-${captured.length}`; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; // An ordinary new tab must refresh groups even before its first reconnect. await createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => false, requireFreshConnectionOnReconnectRef: { current: false }, terminalBackend, }) as never).startSSH(createTermStub() as never); assert.equal(captured[0].reuseTransport, false); // After a reconnect (manual retry / auto-reconnect) every attempt must dial // a brand-new connection so the server performs a fresh login and picks up // remote supplementary-group changes (e.g. `usermod -aG`). const reconnectedStarters = createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => false, requireFreshConnectionOnReconnectRef: { current: true }, terminalBackend, }) as never); await reconnectedStarters.startSSH(createTermStub() as never); await reconnectedStarters.startSSH(createTermStub() as never); assert.equal(captured[1].reuseTransport, false); assert.equal(captured[2].reuseTransport, false); // Copy/Split may still have an unconsumed source while credentials load. // A reconnect must discard that intent before the next backend attempt. const pendingSource = { current: "source-session" as string | undefined }; const reuseAttempts: (string | undefined)[] = []; await createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => false, requireFreshConnectionOnReconnectRef: { current: true }, reuseConnectionFromSessionIdRef: pendingSource, setConnectionReuseAttemptSourceId: (id: string | undefined) => reuseAttempts.push(id), terminalBackend, }) as never).startSSH(createTermStub() as never); assert.equal(captured[3].reuseTransport, false); assert.equal(captured[3].sourceSessionId, undefined); assert.equal(pendingSource.current, undefined); assert.deepEqual(reuseAttempts, [undefined]); }); test("startSSH commits an empty automation snapshot only after the backend session succeeds", async () => { let resolveStart: ((sessionId: string) => void) | undefined; let commitCount = 0; const terminalBackend = { backendAvailable: () => true, startSSHSession: () => new Promise((resolve) => { resolveStart = resolve; }), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const startPromise = createTerminalSessionStarters(createStarterContext({ shouldUseFreshSshConnection: () => false, onConnectAutomationSnapshotCommitted: () => { commitCount += 1; }, terminalBackend, }) as never).startSSH(createTermStub() as never); await Promise.resolve(); assert.equal(commitCount, 0); assert.ok(resolveStart); resolveStart("ssh-session"); await startPromise; assert.equal(commitCount, 1); }); test("startSSH rechecks the live automation policy before password fallback", async () => { const captured: Record[] = []; let requiresFreshConnection = false; let commitCount = 0; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { captured.push(options); if (captured.length === 1) { requiresFreshConnection = true; throw new Error("Authentication failed"); } return "ssh-session"; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", authMethod: "key", identityFileId: "key-1", password: "login-secret", }, keys: [{ id: "key-1", name: "Key", privateKey: "plain-private-key", publicKey: "", source: "embedded", }], shouldUseFreshSshConnection: () => requiresFreshConnection, onConnectAutomationSnapshotCommitted: () => { commitCount += 1; }, terminalBackend, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(captured.length, 2); assert.equal(captured[0].reuseTransport, false); assert.equal(captured[1].reuseTransport, false); assert.equal(commitCount, 0); }); test("startSSH auth failure after disconnect does not reopen credential UI", async () => { let resolveStart: ((error: Error) => void) | null = null; let releaseStartEntered: (() => void) | null = null; const started = new Promise((resolve) => { releaseStartEntered = resolve; }); const needsAuthCalls: boolean[] = []; const statuses: string[] = []; const errors: Array = []; const isBootActiveRef = { current: true }; const bootEpochRef = { current: 1 }; const terminalBackend = { backendAvailable: () => true, startSSHSession: async () => { releaseStartEntered?.(); return await new Promise((_resolve, reject) => { resolveStart = reject; }); }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "secret", authMethod: "password", }, isBootActiveRef, bootEpochRef, setNeedsAuth: (value: boolean) => { needsAuthCalls.push(value); }, setStatus: (value: string) => { statuses.push(value); }, setError: (value: string | null) => { errors.push(value); }, updateStatus: (value: string) => { statuses.push(value); }, terminalBackend, }); const startPromise = createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await started; isBootActiveRef.current = false; bootEpochRef.current += 1; resolveStart?.(new Error("Authentication failed")); await startPromise; assert.deepEqual(needsAuthCalls, []); assert.deepEqual(statuses, []); assert.deepEqual(errors, []); }); test("startSSH stale attempt cannot attach after disconnect then reconnect", async () => { let resolveStart: ((id: string) => void) | null = null; let releaseStartEntered: (() => void) | null = null; const started = new Promise((resolve) => { releaseStartEntered = resolve; }); const attached: string[] = []; const closed: string[] = []; const isBootActiveRef = { current: true }; const bootEpochRef = { current: 1 }; const terminalBackend = { backendAvailable: () => true, startSSHSession: async () => { releaseStartEntered?.(); return await new Promise((resolve) => { resolveStart = resolve; }); }, closeSession: (id: string, opts?: { bootEpoch?: number }) => { if (opts?.bootEpoch !== undefined && opts.bootEpoch !== bootEpochRef.current) return; closed.push(id); }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "secret", authMethod: "password", }, isBootActiveRef, bootEpochRef, onSessionAttached: (id: string) => { attached.push(id); }, terminalBackend, }); const startPromise = createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await started; // Disconnect then immediately reconnect: boot becomes active again on a new epoch. bootEpochRef.current += 1; isBootActiveRef.current = false; bootEpochRef.current += 1; isBootActiveRef.current = true; resolveStart?.("stale-ssh-session"); await startPromise; assert.deepEqual(attached, []); // Shared sessionId must not be closed while the replacement reconnect owns it. assert.deepEqual(closed, []); }); test("stale startSSH closes orphan only when boot is fully inactive", async () => { let resolveStart: ((id: string) => void) | null = null; let releaseStartEntered: (() => void) | null = null; const started = new Promise((resolve) => { releaseStartEntered = resolve; }); const closed: string[] = []; const isBootActiveRef = { current: true }; const bootEpochRef = { current: 1 }; const terminalBackend = { backendAvailable: () => true, startSSHSession: async () => { releaseStartEntered?.(); return await new Promise((resolve) => { resolveStart = resolve; }); }, // No newer registry owner exists; closing by the attempt's bootEpoch succeeds. closeSession: (id: string) => { closed.push(id); }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "secret", authMethod: "password", }, isBootActiveRef, bootEpochRef, terminalBackend, }); const startPromise = createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await started; bootEpochRef.current += 1; isBootActiveRef.current = false; resolveStart?.("orphan-ssh-session"); await startPromise; assert.deepEqual(closed, ["orphan-ssh-session"]); }); test("stale startSSH success does not clear replacement MFA wait state", async () => { let resolveStart: ((id: string) => void) | null = null; let releaseStartEntered: (() => void) | null = null; const started = new Promise((resolve) => { releaseStartEntered = resolve; }); const awaitingUserInput: boolean[] = []; const closed: string[] = []; const isBootActiveRef = { current: true }; const bootEpochRef = { current: 1 }; const terminalBackend = { backendAvailable: () => true, startSSHSession: async () => { releaseStartEntered?.(); return await new Promise((resolve) => { resolveStart = resolve; }); }, closeSession: (id: string, opts?: { bootEpoch?: number }) => { if (opts?.bootEpoch !== undefined && opts.bootEpoch !== bootEpochRef.current) return; closed.push(id); }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "secret", authMethod: "password", }, isBootActiveRef, bootEpochRef, setIsConnectionAwaitingUserInput: (value: boolean) => { awaitingUserInput.push(value); }, terminalBackend, }); const startPromise = createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await started; awaitingUserInput.length = 0; bootEpochRef.current += 1; isBootActiveRef.current = false; bootEpochRef.current += 1; isBootActiveRef.current = true; resolveStart?.("stale-ssh-session"); await startPromise; assert.deepEqual(awaitingUserInput, []); assert.deepEqual(closed, []); }); test("stale startSSH attach failure does not disconnect a newer reconnect", async () => { let resolveStart: ((id: string) => void) | null = null; let releaseStartEntered: (() => void) | null = null; const started = new Promise((resolve) => { releaseStartEntered = resolve; }); const statuses: string[] = []; const isBootActiveRef = { current: true }; const bootEpochRef = { current: 1 }; const terminalBackend = { backendAvailable: () => true, startSSHSession: async () => { releaseStartEntered?.(); return await new Promise((resolve) => { resolveStart = resolve; }); }, closeSession: noop, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "secret", authMethod: "password", }, isBootActiveRef, bootEpochRef, updateStatus: (value: string) => { statuses.push(value); }, setStatus: (value: string) => { statuses.push(value); }, terminalBackend, }); const startPromise = createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await started; bootEpochRef.current += 1; isBootActiveRef.current = false; bootEpochRef.current += 1; isBootActiveRef.current = true; statuses.length = 0; resolveStart?.("stale-ssh-session"); await startPromise; assert.deepEqual(statuses, []); }); test("stale startSSH failure does not reset replacement reconnect UI state", async () => { let rejectStart: ((error: Error) => void) | null = null; let releaseStartEntered: (() => void) | null = null; const started = new Promise((resolve) => { releaseStartEntered = resolve; }); const awaitingUserInput: boolean[] = []; const pastTcpDial: boolean[] = []; const chainProgress: Array = []; let unsubscribed = false; const isBootActiveRef = { current: true }; const bootEpochRef = { current: 1 }; const terminalBackend = { backendAvailable: () => true, startSSHSession: async () => { releaseStartEntered?.(); return await new Promise((_resolve, reject) => { rejectStart = reject; }); }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => () => { unsubscribed = true; }, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "secret", authMethod: "password", }, isBootActiveRef, bootEpochRef, setIsConnectionAwaitingUserInput: (value: boolean) => { awaitingUserInput.push(value); }, setIsConnectionPastTcpDial: (value: boolean) => { pastTcpDial.push(value); }, setChainProgress: (value: unknown) => { chainProgress.push(value); }, terminalBackend, }); const startPromise = createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await started; // Replacement reconnect is waiting for keyboard-interactive input. awaitingUserInput.length = 0; pastTcpDial.length = 0; chainProgress.length = 0; bootEpochRef.current += 1; isBootActiveRef.current = false; bootEpochRef.current += 1; isBootActiveRef.current = true; rejectStart?.(new Error("Authentication failed")); await startPromise; assert.equal(unsubscribed, true); assert.deepEqual(awaitingUserInput, []); assert.deepEqual(pastTcpDial, []); assert.deepEqual(chainProgress, []); }); test("stale startSSH key auth failure does not launch password fallback", async () => { let rejectStart: ((error: Error) => void) | null = null; let releaseStartEntered: (() => void) | null = null; const started = new Promise((resolve) => { releaseStartEntered = resolve; }); const startCalls: Array> = []; const progressLogs: string[] = []; const isBootActiveRef = { current: true }; const bootEpochRef = { current: 1 }; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { startCalls.push(options); releaseStartEntered?.(); return await new Promise((_resolve, reject) => { rejectStart = reject; }); }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", authMethod: "key", identityFileId: "key-1", password: "login-secret", }, keys: [{ id: "key-1", name: "Key", privateKey: "plain-private-key", publicKey: "", source: "embedded", }], isBootActiveRef, bootEpochRef, setProgressLogs: (updater: string[] | ((prev: string[]) => string[])) => { const next = typeof updater === "function" ? updater(progressLogs) : updater; progressLogs.splice(0, progressLogs.length, ...next); }, terminalBackend, }); const startPromise = createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await started; bootEpochRef.current += 1; isBootActiveRef.current = false; rejectStart?.(new Error("Authentication failed")); await startPromise; assert.equal(startCalls.length, 1); assert.equal(startCalls[0].password, "login-secret"); assert.equal(startCalls[0].bootEpoch, 1); assert.equal( progressLogs.includes("Key auth failed. Trying password..."), false, ); }); test("stale startSSH chain progress does not overwrite replacement reconnect UI", async () => { let chainProgressListener: | ((sessionId: string, hop: number, total: number, label: string, status: string, error?: string) => void) | null = null; let releaseStartEntered: (() => void) | null = null; const started = new Promise((resolve) => { releaseStartEntered = resolve; }); const awaitingUserInput: boolean[] = []; const chainProgress: unknown[] = []; const isBootActiveRef = { current: true }; const bootEpochRef = { current: 1 }; const terminalBackend = { backendAvailable: () => true, startSSHSession: async () => { releaseStartEntered?.(); return await new Promise(() => {}); }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: ( listener: (sessionId: string, hop: number, total: number, label: string, status: string, error?: string) => void, ) => { chainProgressListener = listener; return noop; }, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "secret", authMethod: "password", hostChain: { hostIds: ["jump-1"] }, }, resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jump", password: "jump-secret", }], isBootActiveRef, bootEpochRef, setIsConnectionAwaitingUserInput: (value: boolean) => { awaitingUserInput.push(value); }, setChainProgress: (value: unknown) => { chainProgress.push(value); }, terminalBackend, }); void createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await started; awaitingUserInput.length = 0; chainProgress.length = 0; bootEpochRef.current += 1; isBootActiveRef.current = false; bootEpochRef.current += 1; isBootActiveRef.current = true; chainProgressListener?.( "session-1", 1, 2, "jump.example.test", "auth-attempt", "waiting for user input...", ); assert.deepEqual(awaitingUserInput, []); assert.deepEqual(chainProgress, []); }); test("startSSH keeps interactive source auth retries off unrelated pooled transports", async () => { const captured: Record[] = []; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { captured.push(options); if (captured.length === 1) throw new Error("Authentication failed"); return "ssh-session"; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const reuseConnectionFromSessionIdRef = { current: "source-session" as string | undefined }; const reuseConnectionSourceAttemptedRef = { current: false }; const firstCtx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", authMethod: "key", identityFileId: "key-1", }, keys: [{ id: "key-1", name: "Key", privateKey: "plain-private-key", publicKey: "", source: "embedded", }], reuseConnectionFromSessionIdRef, reuseConnectionSourceAttemptedRef, shouldUseFreshSshConnection: () => false, terminalBackend, }); await createTerminalSessionStarters(firstCtx as never).startSSH(createTermStub() as never); assert.equal(reuseConnectionSourceAttemptedRef.current, true); const retryCtx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", authMethod: "password", password: "corrected-secret", }, reuseConnectionFromSessionIdRef, reuseConnectionSourceAttemptedRef, shouldUseFreshSshConnection: () => false, terminalBackend, }); await createTerminalSessionStarters(retryCtx as never).startSSH(createTermStub() as never); assert.equal(captured.length, 2); assert.equal(captured[0].sourceSessionId, "source-session"); assert.equal(captured[0].reuseTransport, undefined); assert.equal(captured[1].sourceSessionId, undefined); assert.equal(captured[1].reuseTransport, false); assert.equal(reuseConnectionSourceAttemptedRef.current, false); }); test("startSSH uses the system agent when a synced vault key cannot be decrypted", async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Agent host", hostname: "agent.example.test", username: "root", authMethod: "key", identityFileId: "key-1", useSshAgent: true, }, keys: [{ id: "key-1", label: "Synced key", type: "ED25519", publicKey: "ssh-ed25519 AAAASELECTED", privateKey: "enc:v1:djEwdGVzdAAAAAAAAAAAAAAAAA==", source: "imported", category: "key", created: 1, }], terminalBackend, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(capturedOptions?.useSshAgent, true); assert.deepEqual(capturedOptions?.agentPublicKeys, ["ssh-ed25519 AAAASELECTED"]); assert.equal(capturedOptions?.privateKey, undefined); }); test("startSSH waits for stored-key decrypt before sending private key material", async (t) => { const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window"); let decryptAttempts = 0; Object.defineProperty(globalThis, "window", { configurable: true, value: { netcatty: { credentialsDecrypt: async (value: string) => { decryptAttempts += 1; if (decryptAttempts < 3) return value; return "-----BEGIN OPENSSH PRIVATE KEY-----\nhydrated\n-----END OPENSSH PRIVATE KEY-----"; }, }, }, }); t.after(() => { if (previousWindow) Object.defineProperty(globalThis, "window", previousWindow); else delete (globalThis as { window?: unknown }).window; }); let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Stored key host", hostname: "key.example.test", username: "alice", authMethod: "key", identityFileId: "key-1", }, keys: [{ id: "key-1", label: "Imported key", type: "ED25519", privateKey: ENCRYPTED_CREDENTIAL_PLACEHOLDER, source: "imported", category: "key", created: 1, }], terminalBackend, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.ok(decryptAttempts >= 3); assert.equal( capturedOptions?.privateKey, "-----BEGIN OPENSSH PRIVATE KEY-----\nhydrated\n-----END OPENSSH PRIVATE KEY-----", ); assert.doesNotMatch(String(capturedOptions?.privateKey ?? ""), /^enc:v1:/); }); test("startSSH rejects encrypted stored-key material instead of sending ciphertext", async () => { let capturedOptions: Record | null = null; let needsAuth = false; let authRetryMessage: string | null = null; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Stored key host", hostname: "key.example.test", username: "alice", authMethod: "key", identityFileId: "key-1", }, keys: [{ id: "key-1", label: "Imported key", type: "ED25519", privateKey: ENCRYPTED_CREDENTIAL_PLACEHOLDER, source: "imported", category: "key", created: 1, }], terminalBackend, setNeedsAuth: (value: boolean) => { needsAuth = value; }, setAuthRetryMessage: (value: string | null) => { authRetryMessage = value; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(capturedOptions, null); assert.equal(needsAuth, true); assert.match(authRetryMessage ?? "", /cannot be decrypted/); }); test("startSSH does not wait to hydrate unrelated encrypted vault keys", async (t) => { const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window"); let decryptAttempts = 0; Object.defineProperty(globalThis, "window", { configurable: true, value: { netcatty: { credentialsDecrypt: async (value: string) => { decryptAttempts += 1; await new Promise((resolve) => setTimeout(resolve, 50)); return value; }, }, }, }); t.after(() => { if (previousWindow) Object.defineProperty(globalThis, "window", previousWindow); else delete (globalThis as { window?: unknown }).window; }); let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, startSSHSession: async (options: Record) => { capturedOptions = options; return "ssh-session"; }, onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const started = Date.now(); const ctx = createStarterContext({ host: { id: "host-1", label: "Password host", hostname: "password.example.test", username: "alice", authMethod: "password", password: "secret", }, keys: [{ id: "unused-key", label: "Foreign key", type: "ED25519", privateKey: ENCRYPTED_CREDENTIAL_PLACEHOLDER, source: "imported", category: "key", created: 1, }], terminalBackend, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(decryptAttempts, 0); assert.equal(capturedOptions?.password, "secret"); assert.ok(Date.now() - started < 200); }); for (const protocol of ["Mosh", "ET"] as const) { test(`${protocol} keeps certificate signing material when the system agent toggle is also enabled`, async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, moshAvailable: () => true, etAvailable: () => true, startMoshSession: async (options: Record) => { capturedOptions = options; return "mosh-session"; }, startEtSession: async (options: Record) => { capturedOptions = options; return "et-session"; }, onSessionData: () => noop, onSessionExit: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Certificate host", hostname: "cert.example.test", username: "alice", authMethod: "certificate", identityFileId: "cert-key", useSshAgent: true, }, keys: [{ id: "cert-key", label: "Certificate key", type: "ED25519", category: "key", source: "imported", created: 1, privateKey: "PRIVATE KEY", certificate: "ssh-ed25519-cert-v01@openssh.com AAAATEST", }], terminalBackend, }); const starters = createTerminalSessionStarters(ctx as never); if (protocol === "Mosh") await starters.startMosh(createTermStub() as never); else await starters.startEt(createTermStub() as never); assert.equal(capturedOptions?.privateKey, "PRIVATE KEY"); assert.equal(capturedOptions?.certificate, "ssh-ed25519-cert-v01@openssh.com AAAATEST"); assert.equal(capturedOptions?.useSshAgent, false); }); } for (const protocol of ["Mosh", "ET"] as const) { test(`${protocol} keeps an imported private key when agent filtering is unavailable`, async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, moshAvailable: () => true, etAvailable: () => true, startMoshSession: async (options: Record) => { capturedOptions = options; return "mosh-session"; }, startEtSession: async (options: Record) => { capturedOptions = options; return "et-session"; }, onSessionData: () => noop, onSessionExit: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Imported key host", hostname: "key.example.test", username: "alice", authMethod: "key", identityFileId: "key-1", useSshAgent: true, }, keys: [{ id: "key-1", label: "Imported key", type: "ED25519", category: "key", source: "imported", created: 1, privateKey: "PRIVATE KEY", passphrase: "key-passphrase", }], terminalBackend, }); const starters = createTerminalSessionStarters(ctx as never); if (protocol === "Mosh") await starters.startMosh(createTermStub() as never); else await starters.startEt(createTermStub() as never); assert.equal(capturedOptions?.useSshAgent, false); assert.equal(capturedOptions?.privateKey, "PRIVATE KEY"); assert.equal(capturedOptions?.passphrase, "key-passphrase"); }); } for (const protocol of ["Mosh", "ET"] as const) { test(`${protocol} keeps automatic key discovery available with an unreadable saved password`, async () => { let capturedOptions: Record | null = null; let needsAuth = false; const terminalBackend = { backendAvailable: () => true, moshAvailable: () => true, etAvailable: () => true, startMoshSession: async (options: Record) => { capturedOptions = options; return "mosh-session"; }, startEtSession: async (options: Record) => { capturedOptions = options; return "et-session"; }, onSessionData: () => noop, onSessionExit: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Automatic host", hostname: "auto.example.test", username: "alice", authMethod: "auto", password: ENCRYPTED_CREDENTIAL_PLACEHOLDER, }, terminalBackend, setNeedsAuth: (value: boolean) => { needsAuth = value; }, }); const starters = createTerminalSessionStarters(ctx as never); if (protocol === "Mosh") await starters.startMosh(createTermStub() as never); else await starters.startEt(createTermStub() as never); assert.equal(needsAuth, false); assert.equal(capturedOptions?.authMethod, "auto"); assert.equal(capturedOptions?.password, undefined); }); } test("ET keeps automatic jump discovery available with an unreadable saved password", async () => { let capturedOptions: Record | null = null; const terminalBackend = { backendAvailable: () => true, etAvailable: () => true, startEtSession: async (options: Record) => { capturedOptions = options; return "et-session"; }, onSessionData: () => noop, onSessionExit: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", authMethod: "auto", }, resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "ops", authMethod: "auto", password: ENCRYPTED_CREDENTIAL_PLACEHOLDER, }], terminalBackend, }); await createTerminalSessionStarters(ctx as never).startEt(createTermStub() as never); const jumpHosts = capturedOptions?.jumpHosts as Array> | undefined; assert.equal(jumpHosts?.[0]?.authMethod, "auto"); assert.equal(jumpHosts?.[0]?.password, undefined); }); test("startSSH forwards custom ProxyCommand to the SSH bridge", async () => { let capturedOptions: Record | null = null; 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", port: 2200, proxyConfig: { type: "command", host: "", port: 0, command: "cloudflared access ssh --hostname %h", }, }, 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: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: (_data: string, callback?: () => void) => callback?.(), writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startSSH(term as never); assert.deepEqual(capturedOptions?.proxy, { type: "command", host: "", port: 0, command: "cloudflared access ssh --hostname %h", username: undefined, password: undefined, }); }); test("startSSH resolves target proxy credentials from an identity", async () => { let capturedOptions: Record | null = null; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "identity-1", }, }, identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", password: "proxy-secret", created: 1, }], }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.deepEqual(capturedOptions?.proxy, { type: "http", host: "proxy.example.test", port: 3128, username: "proxy-user", password: "proxy-secret", }); }); test("startSSH resolves jump host proxy credentials from an identity", async () => { let capturedOptions: Record | null = null; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", sshTcpConnectTimeoutSeconds: 50, sshAuthReadyTimeoutSeconds: 240, hostChain: { hostIds: ["jump-1"] }, }, resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jump", requiresMfa: true, sshTcpConnectTimeoutSeconds: 75, sshAuthReadyTimeoutSeconds: 360, proxyConfig: { type: "socks5", host: "jump-proxy.example.test", port: 1080, identityId: "identity-1", }, }], identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", password: "proxy-secret", created: 1, }], }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); const jumpHosts = capturedOptions?.jumpHosts as Array>; assert.deepEqual(jumpHosts[0]?.proxy, { type: "socks5", host: "jump-proxy.example.test", port: 1080, username: "proxy-user", password: "proxy-secret", }); assert.equal(capturedOptions?.sshTcpConnectTimeoutMs, 50_000); assert.equal(capturedOptions?.sshAuthReadyTimeoutMs, 240_000); assert.equal(jumpHosts[0]?.requiresMfa, true); assert.equal(jumpHosts[0]?.sshTcpConnectTimeoutMs, 75_000); assert.equal(jumpHosts[0]?.sshAuthReadyTimeoutMs, 360_000); }); test("startSSH shows jump-host auth failures without opening target auth retry", async () => { let error = ""; let needsAuth = false; let retryMessage: string | null = "previous retry"; let status = ""; const progressLogs: string[] = []; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { const err = new Error('Jump host authentication failed for "Bastion": All configured authentication methods failed'); (err as Error & { isJumpHostAuthError?: boolean }).isJumpHostAuthError = true; throw err; }, 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, }, resolvedChainHosts: [{ id: "jump-1", label: "Bastion", hostname: "bastion.example.test", username: "jump", password: "wrong-secret", }], setError: (message: string) => { error = message; }, setNeedsAuth: (value: boolean) => { needsAuth = value; }, setAuthRetryMessage: (message: string | null) => { retryMessage = message; }, setStatus: (next: string) => { status = next; }, updateStatus: (next: string) => { status = next; }, setProgressLogs: (next: string[] | ((prev: string[]) => string[])) => { if (typeof next === "function") { progressLogs.splice(0, progressLogs.length, ...next(progressLogs)); } else { progressLogs.splice(0, progressLogs.length, ...next); } }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(needsAuth, false); assert.equal(retryMessage, null); assert.equal(status, "disconnected"); assert.match(error, /Jump host authentication failed for "Bastion"/); assert.equal(progressLogs.some((line) => /Authentication failed\. Please try again/.test(line)), false); }); test("startSSH recognizes Electron-prefixed jump-host auth failures", async () => { let error = ""; let needsAuth = false; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { throw new Error( 'Error invoking remote method "netcatty:start": Error: Jump host authentication failed for "Bastion": All configured authentication methods failed', ); }, 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 = createStarterContext({ terminalBackend, setNeedsAuth: (value: boolean) => { needsAuth = value; }, setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(needsAuth, false); assert.match(error, /Jump host authentication failed for "Bastion"/); }); test("startSSH does not open auth retry for socket errors mentioning auth in hostnames", async () => { let error = ""; let needsAuth = false; let retryMessage: string | null = "previous retry"; let status = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { const err = new Error("Connection reset by auth-bastion.example.com"); throw err; }, 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, }, resolvedChainHosts: [{ id: "jump-1", label: "Auth Bastion", hostname: "auth-bastion.example.com", username: "jump", password: "secret", }], setError: (message: string) => { error = message; }, setNeedsAuth: (value: boolean) => { needsAuth = value; }, setAuthRetryMessage: (message: string | null) => { retryMessage = message; }, setStatus: (next: string) => { status = next; }, updateStatus: (next: string) => { status = next; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(needsAuth, false); assert.equal(retryMessage, null); assert.equal(status, "disconnected"); assert.equal(error, "Connection reset by auth-bastion.example.com"); }); test("startSSH does not open auth retry for non-login permission denied errors", async () => { let needsAuth = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { throw new Error("Permission denied opening channel to auth-bastion.example.com"); }, 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 = createStarterContext({ terminalBackend, setNeedsAuth: (value: boolean) => { needsAuth = value; }, setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(needsAuth, false); assert.equal(error, "Permission denied opening channel to auth-bastion.example.com"); }); test("startSSH rejects missing saved proxy profiles before connecting", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", proxyProfileId: "missing-proxy", }, setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /Saved proxy for host "Target" is missing/); }); test("startSSH rejects missing saved proxy profiles on jump hosts before connecting", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, }, resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jump", proxyProfileId: "missing-proxy", }], setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /Saved proxy for jump host "Jump" is missing/); }); test("startSSH rejects missing proxy identities before connecting", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "missing-identity", }, }, setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /Proxy identity/); assert.match(error, /Target/); }); test("startSSH rejects incomplete proxy identities before connecting", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "identity-1", }, }, identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", created: 1, }], setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /Proxy identity/); assert.match(error, /incomplete/); }); test("startSSH rejects proxy identities with blank usernames even when passwords are encrypted", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "identity-1", }, }, identities: [{ id: "identity-1", label: "Proxy login", username: " ", authMethod: "password", password: ENCRYPTED_CREDENTIAL_PLACEHOLDER, created: 1, }], setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /Proxy identity/); assert.match(error, /incomplete/); }); test("startSSH rejects target proxy identity passwords that cannot be decrypted", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "identity-1", }, }, identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", password: ENCRYPTED_CREDENTIAL_PLACEHOLDER, created: 1, }], setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /Proxy credentials cannot be decrypted/); }); test("startSSH rejects missing jump host proxy identities before connecting", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, }, resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jump", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "missing-identity", }, }], setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /Proxy identity/); assert.match(error, /Jump/); }); test("startSSH rejects incomplete jump host proxy identities before connecting", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, }, identities: [{ id: "identity-1", label: "Proxy login", username: "", authMethod: "password", password: ENCRYPTED_CREDENTIAL_PLACEHOLDER, created: 1, }], resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jump", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "identity-1", }, }], setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /Proxy identity/); assert.match(error, /incomplete/); assert.match(error, /Jump/); }); test("startSSH rejects jump host proxy identity passwords that cannot be decrypted", async () => { let started = false; let error = ""; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { started = true; 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 = createStarterContext({ terminalBackend, host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", hostChain: { hostIds: ["jump-1"] }, }, identities: [{ id: "identity-1", label: "Proxy login", username: "proxy-user", authMethod: "password", password: ENCRYPTED_CREDENTIAL_PLACEHOLDER, created: 1, }], resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jump", proxyConfig: { type: "http", host: "proxy.example.test", port: 3128, identityId: "identity-1", }, }], setError: (message: string) => { error = message; }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(started, false); assert.match(error, /cannot be decrypted/); assert.match(error, /Jump/); }); test("startSSH sends key and password together in one connection for publickey+password MFA hosts", async () => { let capturedOptions: Record | null = null; let startCalls = 0; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async (options: Record) => { startCalls += 1; 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", port: 22, authMethod: "key", identityFilePaths: ["/Users/me/.ssh/key"], password: "login-secret", savePassword: true, }, 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: noop, setNeedsAuth: noop, setAuthRetryMessage: noop, setAuthPassword: noop, setProgressLogs: noop, setProgressValue: noop, setChainProgress: noop, }; const term = { cols: 120, rows: 32, write: (_data: string, callback?: () => void) => callback?.(), writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startSSH(term as never); assert.equal(startCalls, 1, "credentials must go in a single connection, not separate per-factor attempts"); assert.equal(capturedOptions?.password, "login-secret"); assert.deepEqual(capturedOptions?.identityFilePaths, ["/Users/me/.ssh/key"]); }); test("startSSH resets the TCP dial timeout state before password fallback", async () => { let chainProgressListener: ( sessionId: string, hop: number, total: number, label: string, status: string, error?: string, ) => void = noop; let startCalls = 0; const startOptions: Record[] = []; const tcpDialState: boolean[] = []; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async (options: Record) => { startCalls += 1; startOptions.push(options); if (startCalls === 1) { chainProgressListener("session-1", 1, 1, "target.example.test", "tcp-connected"); throw new Error("Authentication failed"); } 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: (listener: typeof chainProgressListener) => { chainProgressListener = listener; return noop; }, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", port: 22, authMethod: "key", identityFileId: "key-1", password: "login-secret", useSshAgent: true, }, keys: [{ id: "key-1", name: "Key", privateKey: "plain-private-key", publicKey: "", source: "embedded", }], terminalBackend, setIsConnectionPastTcpDial: (value: boolean) => { tcpDialState.push(value); }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(startCalls, 2); assert.equal(startOptions[0]?.useSshAgent, false); assert.equal(startOptions[1]?.useSshAgent, false); assert.deepEqual(tcpDialState, [false, false, true, false]); }); test("startSSH resets the TCP dial timeout state when a jump host starts forwarding", async () => { let chainProgressListener: ( sessionId: string, hop: number, total: number, label: string, status: string, error?: string, ) => void = noop; const tcpDialState: boolean[] = []; const terminalBackend = { backendAvailable: () => true, telnetAvailable: () => true, moshAvailable: () => true, localAvailable: () => true, serialAvailable: () => true, execAvailable: () => true, startSSHSession: async () => { chainProgressListener("session-1", 1, 2, "bastion.example.test", "tcp-connected"); chainProgressListener("session-1", 1, 2, "bastion.example.test", "forwarding"); chainProgressListener("session-1", 2, 2, "target.example.test", "tcp-connected"); 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: (listener: typeof chainProgressListener) => { chainProgressListener = listener; return noop; }, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "login-secret", hostChain: { hostIds: ["jump-1"] }, }, resolvedChainHosts: [{ id: "jump-1", label: "Bastion", hostname: "bastion.example.test", username: "alice", password: "jump-secret", }], terminalBackend, setIsConnectionPastTcpDial: (value: boolean) => { tcpDialState.push(value); }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.deepEqual(tcpDialState, [false, false, true, false, true]); }); test("startSSH forwards the saved sudo autofill password to the SSH bridge", async () => { let capturedOptions: Record | null = null; 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 = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "login-secret", }, terminalBackend, sudoAutofillPassword: "sudo-secret", }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); assert.equal(capturedOptions?.sudoAutofillPassword, "sudo-secret"); }); test("startSSH enables sudo autofill only with the host saved password", async () => { 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", 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 ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "saved-secret", }, terminalBackend, sudoAutofillPassword: "saved-secret", }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); onData?.(armSudoPrompt(ctx.sudoAutofillRef.current)); ctx.sudoAutofillRef.current?.confirmFill(); assert.deepEqual(sent, ["saved-secret\n"]); }); test("startSSH does not use unsaved retry passwords for sudo autofill", async () => { 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", 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 ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", }, pendingAuthRef: { current: { authMethod: "password", username: "alice", password: "temporary-secret", savedToHost: false, }, }, terminalBackend, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); ctx.sudoAutofillRef.current?.armForCommand("sudo whoami"); onData?.("[sudo] password for alice: "); assert.deepEqual(sent, []); }); test("startSSH uses pending saved auth for sudo autofill on the first saved connection", async () => { 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", 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 ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", }, pendingAuthRef: { current: { authMethod: "password", username: "alice", password: "pending-secret", savedToHost: true, }, }, terminalBackend, sudoAutofillPasswordRef: { current: "stale-secret" }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); ctx.sudoAutofillRef.current?.armForCommand("sudo whoami"); onData?.("[sudo] password for alice: "); ctx.sudoAutofillRef.current?.confirmFill(); assert.deepEqual(sent, ["pending-secret\n"]); }); test("startSSH does not use merged group default passwords for sudo autofill", async () => { 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", 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 ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", password: "group-default-secret", }, terminalBackend, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); ctx.sudoAutofillRef.current?.armForCommand("sudo whoami"); onData?.("[sudo] password for alice: "); assert.deepEqual(sent, []); }); test("startSSH uses the provided sudo autofill password", async () => { 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", 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 ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", }, terminalBackend, sudoAutofillPassword: "host-secret", }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); onData?.(armSudoPrompt(ctx.sudoAutofillRef.current)); ctx.sudoAutofillRef.current?.confirmFill(); assert.deepEqual(sent, ["host-secret\n"]); }); test("startSerial captures direct connected banner in terminal log data", async () => { const capturedLogData: string[] = []; const writtenData: 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", startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "serial-host", label: "Serial", hostname: "COM3", username: "", protocol: "serial", }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: { verifyHostKeys: false, }, terminalBackend, serialConfig: { path: "COM3", baudRate: 9600, dataBits: 8, stopBits: 1, parity: "none", flowControl: "none", }, 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, onTerminalLogData: (data: string) => capturedLogData.push(data), }; const term = { cols: 120, rows: 32, write: (data: string, callback?: () => void) => { writtenData.push(data); callback?.(); }, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startSerial(term as never); const banner = "[Connected to COM3 at 9600 baud]"; assert.deepEqual(writtenData, [`${banner}\r\n`]); assert.deepEqual(capturedLogData, [`${banner}\r\n`]); }); test("local session captures paste cleanup writes in terminal log data", async () => { const capturedLogData: string[] = []; const writes: string[] = []; let onData: ((data: 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 () => "mosh-session", 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: noop, resizeSession: noop, }; const ctx = { host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, 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, onTerminalLogData: (data: string) => capturedLogData.push(data), }; const term = { cols: 20, rows: 4, paste: noop, write: (data: string, callback?: () => void) => { writes.push(data); callback?.(); }, writeln: noop, scrollToBottom: noop, }; const longPaste = Array.from({ length: 20 }, (_, index) => `line ${index} with enough content`).join("\n"); pasteTextIntoTerminal(term, longPaste, { scrollOnPaste: false }); await createTerminalSessionStarters(ctx as never).startLocal(term as never); assert.notEqual(onData, null); onData?.("\x1b[7mline 3 with enough content\x1b[27m"); assert.deepEqual(writes, ["line 3 with enough content", "\x1b[K"]); assert.deepEqual(capturedLogData, ["line 3 with enough content", "\x1b[K"]); }); test("local session acknowledges metadata-only plugin output immediately", async () => { let onData: ((data: string, meta?: { pluginPipelineIngressBytes?: number }) => void) | null = null; const acknowledgements: Array<{ sessionId: string; bytes: number }> = []; const paused: Array<{ sessionId: string; paused: boolean }> = []; const terminalBackend = { localAvailable: () => true, startLocalSession: async () => "local-session", onSessionData: ( _id: string, cb: (data: string, meta?: { pluginPipelineIngressBytes?: number }) => void, ) => { onData = cb; return noop; }, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, ackSessionFlow: (sessionId: string, bytes: number) => { acknowledgements.push({ sessionId, bytes }); }, setSessionFlowPaused: (sessionId: string, isPaused: boolean) => { paused.push({ sessionId, paused: isPaused }); }, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, terminalBackend, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); onData?.("", { pluginPipelineIngressBytes: 12 }); assert.deepEqual(acknowledgements, [{ sessionId: "local-session", bytes: 12 }]); assert.deepEqual(paused.at(-1), { sessionId: "local-session", paused: false }); }); test("local session runs startup command after attaching", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const attached: 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", startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: "docker logs -f --tail 200 abc123", promptLineBreakStateRef: undefined, onSessionAttached: (id: string) => attached.push(id), }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(attached, ["local-session"]); assert.deepEqual(sessionWrites, [{ id: "local-session", data: "docker logs -f --tail 200 abc123\r", automated: true, }]); }); test("local session sends multi-line startup snippets in one write by default", async () => { const attached: string[] = []; const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const terminalBackend = { localAvailable: () => true, startLocalSession: async () => "local-session", onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: 'sudo apt install gconf2-common -y\necho "123456"', promptLineBreakStateRef: undefined, onSessionAttached: (id: string) => attached.push(id), }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(attached, ["local-session"]); assert.deepEqual(sessionWrites, [{ id: "local-session", data: 'sudo apt install gconf2-common -y\necho "123456"\r', automated: true, }]); }); test("local session wraps multi-line startup paste when bracketed paste is active", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const terminalBackend = { localAvailable: () => true, startLocalSession: async () => "local-session", onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: "sudo apt install gconf2-common -y\necho done", promptLineBreakStateRef: undefined, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub({ modes: { bracketedPasteMode: true }, }) as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, [{ id: "local-session", data: "\x1b[200~sudo apt install gconf2-common -y\necho done\x1b[201~\r", automated: true, }]); }); test("local session respects disabled bracketed paste for startup paste", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const terminalBackend = { localAvailable: () => true, startLocalSession: async () => "local-session", onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: "first\nsecond", promptLineBreakStateRef: undefined, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub({ modes: { bracketedPasteMode: true }, options: { ignoreBracketedPasteMode: true }, }) as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, [{ id: "local-session", data: "first\nsecond\r", automated: true, }]); }); test("local session can send multi-line startup snippets line by line", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const terminalBackend = { localAvailable: () => true, startLocalSession: async () => "local-session", onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: "first cmd\nsecond cmd", multiLineRunMode: "lineDelay", promptLineBreakStateRef: undefined, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, [{ id: "local-session", data: "first cmd\r", automated: true }]); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, [ { id: "local-session", data: "first cmd\r", automated: true }, { id: "local-session", data: "second cmd\r", automated: true }, ]); }); test("local session sends host startup commands in one write by default", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const terminalBackend = { localAvailable: () => true, startLocalSession: async () => "local-session", onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", startupCommand: "enter prompt\nrun command", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: undefined, promptLineBreakStateRef: undefined, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, [{ id: "local-session", data: "enter prompt\nrun command\r", automated: true, }]); }); test("local session can send host startup commands line by line", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const terminalBackend = { localAvailable: () => true, startLocalSession: async () => "local-session", onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", startupCommand: "first host cmd\nsecond host cmd", startupCommandRunMode: "lineDelay", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: undefined, promptLineBreakStateRef: undefined, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, [{ id: "local-session", data: "first host cmd\r", automated: true }]); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, [ { id: "local-session", data: "first host cmd\r", automated: true }, { id: "local-session", data: "second host cmd\r", automated: true }, ]); }); test("startup command suppression is consumed only when scheduling", () => { const suppressHostStartupCommandRef = { current: true }; const ctx = createStarterContext({ host: { id: "host-1", label: "Target", hostname: "target.example.test", username: "alice", startupCommand: "echo host-startup", }, startupCommand: undefined, suppressHostStartupCommandRef, }); assert.equal(resolveStartupCommand(ctx as never), undefined); assert.equal(suppressHostStartupCommandRef.current, true); assert.equal( resolveStartupCommand(ctx as never, { consumeSuppressHostStartupCommand: true }), undefined, ); assert.equal(suppressHostStartupCommandRef.current, false); assert.equal(resolveStartupCommand(ctx as never), "echo host-startup"); }); test("restored local reconnect runs the host startup command while automatic retry suppresses it", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; 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: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", startupCommand: "echo host-startup", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: undefined, suppressHostStartupCommandRef: { current: shouldSuppressHostStartupCommandOnReconnect("restored"), }, promptLineBreakStateRef: undefined, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, [ { id: "local-session", data: "echo host-startup\r", automated: true }, ]); sessionWrites.length = 0; const automaticReconnectCtx = createStarterContext({ ...ctx, sessionRef: { current: null }, hasRunStartupCommandRef: { current: false }, suppressHostStartupCommandRef: { current: shouldSuppressHostStartupCommandOnReconnect("automatic"), }, }); await createTerminalSessionStarters(automaticReconnectCtx as never).startLocal(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(sessionWrites, []); }); test("local session start uses per-session directory before global default", 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 () => "mosh-session", startLocalSession: async (options: Record) => { capturedOptions = options; return "local-session"; }, startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", localStartDir: "/Users/alice/project", }, terminalSettings: { localStartDir: "/Users/alice/default" }, terminalBackend, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); assert.equal(capturedOptions?.cwd, "/Users/alice/project"); }); test("local session restores cwd before startup command after attaching", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const executedCommands: string[] = []; const progressLogs: 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", startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const restoreCwdIntentRef = { current: { cwd: "/srv/app dir", command: "cd -- '/srv/app dir'" }, }; const ctx = createStarterContext({ host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: "pwd", promptLineBreakStateRef: undefined, restoreCwdIntentRef, setProgressLogs: (updater: (prev: string[]) => string[]) => { progressLogs.splice(0, progressLogs.length, ...updater(progressLogs)); }, onCommandExecuted: (command: string) => { executedCommands.push(command); }, }); await createTerminalSessionStarters(ctx as never).startLocal(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(restoreCwdIntentRef.current, null); assert.deepEqual(sessionWrites, [ { id: "local-session", data: "cd -- '/srv/app dir'\r", automated: true }, { id: "local-session", data: "pwd\r", automated: true }, ]); assert.deepEqual(executedCommands, ["pwd"]); assert.deepEqual(progressLogs, ["Restoring working directory: /srv/app dir"]); }); test("ssh session restores cwd before startup command after attaching", async () => { const sessionWrites: Array<{ id: string; data: string; automated?: boolean }> = []; const executedCommands: string[] = []; const progressLogs: string[] = []; const restoredCwds: 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", startLocalSession: async () => "local-session", startSerialSession: async () => "serial-session", execCommand: async () => ({}), onSessionData: () => noop, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: (id: string, data: string, options?: { automated?: boolean }) => { sessionWrites.push({ id, data, automated: options?.automated }); }, resizeSession: noop, }; const restoreCwdIntentRef = { current: { cwd: "/srv/app dir", command: "cd -- '/srv/app dir'" }, }; const ctx = createStarterContext({ terminalSettings: { startupCommandDelayMs: 0 }, terminalBackend, startupCommand: "pwd", promptLineBreakStateRef: undefined, restoreCwdIntentRef, setProgressLogs: (updater: (prev: string[]) => string[]) => { progressLogs.splice(0, progressLogs.length, ...updater(progressLogs)); }, onRestoreCwdIntentConsumed: (cwd: string) => { restoredCwds.push(cwd); }, onCommandExecuted: (command: string) => { executedCommands.push(command); }, }); await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(restoreCwdIntentRef.current, null); assert.deepEqual(restoredCwds, ["/srv/app dir"]); assert.deepEqual(sessionWrites, [ { id: "ssh-session", data: "cd -- '/srv/app dir'\r", automated: true }, { id: "ssh-session", data: "pwd\r", automated: true }, ]); assert.deepEqual(executedCommands, ["pwd"]); assert.deepEqual(progressLogs, ["Restoring working directory: /srv/app dir"]); }); test("local session keeps timestamp anchors for preserved scrollback when reusing a terminal", async () => { const writes: string[] = []; const markerLines: number[] = []; const disposedMarkerLines: number[] = []; let onData: ((data: string) => void) | null = null; let cursorLine = 0; 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) => void) => { onData = cb; return noop; }, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", showLineTimestamps: true, }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: { showLineTimestamps: true, scrollOnOutput: false, forcePromptNewLine: false, }, terminalBackend, sessionRef: { current: null }, hasConnectedRef: { current: true }, 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: 20, rows: 4, buffer: { active: { type: "normal" } }, write: (data: string, callback?: () => void) => { writes.push(data); for (const char of data) { if (char === "\n") { cursorLine += 1; } } callback?.(); }, registerMarker: (offset: number) => { const line = cursorLine + offset; markerLines.push(line); const marker = { line, isDisposed: false, dispose() { marker.isDisposed = true; disposedMarkerLines.push(line); }, }; return marker; }, writeln: noop, scrollToBottom: noop, }; const starters = createTerminalSessionStarters(ctx as never); await starters.startLocal(term as never); onData?.("unfinished"); await starters.startLocal(term as never); onData?.("fresh"); assert.equal(writes.length, 2); assert.equal(writes[0], "unfinished"); assert.equal(writes[1], "fresh"); // Reconnect preserves the buffer, so the pre-restart stamp stays anchored // (not disposed) and the fresh output records its own stamp. assert.deepEqual(markerLines, [0, 0]); assert.deepEqual(disposedMarkerLines, []); }); test("session data waits for prior terminal writes before evaluating prompt line breaks", async () => { const writes: string[] = []; const writeCallbacks: Array<() => void> = []; let onData: ((data: string) => void) | null = null; let cursorX = 0; let lineText = ""; 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) => void) => { onData = cb; return noop; }, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const promptState = createPromptLineBreakState(); promptState.lastPromptText = "$ "; promptState.pendingCommand = true; const ctx = { host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: { forcePromptNewLine: true }, terminalBackend, promptLineBreakStateRef: { current: promptState }, 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 = { get buffer() { return { active: { get cursorX() { return cursorX; }, cursorY: 0, baseY: 0, getLine(line: number) { if (line !== 0) return undefined; return { isWrapped: false, translateToString() { return lineText; }, }; }, }, }; }, write: (data: string, callback?: () => void) => { writes.push(data); if (callback) writeCallbacks.push(callback); }, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startLocal(term as never); assert.notEqual(onData, null); onData?.("hello"); onData?.("$ "); assert.deepEqual(writes, ["hello"]); cursorX = 5; lineText = "hello"; writeCallbacks.shift()?.(); assert.deepEqual(writes, ["hello", "\r\n$ "]); }); test("prompt line break display insertion does not mutate captured session log data", async () => { const writes: string[] = []; const capturedLogData: string[] = []; const writeCallbacks: Array<() => void> = []; let onData: ((data: string) => void) | null = null; let cursorX = 0; let lineText = ""; 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) => void) => { onData = cb; return noop; }, onSessionExit: () => noop, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const promptState = createPromptLineBreakState(); promptState.lastPromptText = "$ "; promptState.pendingCommand = true; const ctx = { host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, keys: [], resolvedChainHosts: [], sessionId: "session-1", terminalSettings: { forcePromptNewLine: true }, terminalBackend, promptLineBreakStateRef: { current: promptState }, 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, onTerminalLogData: (data: string) => capturedLogData.push(data), }; const term = { get buffer() { return { active: { get cursorX() { return cursorX; }, cursorY: 0, baseY: 0, getLine(line: number) { if (line !== 0) return undefined; return { isWrapped: false, translateToString() { return lineText; }, }; }, }, }; }, write: (data: string, callback?: () => void) => { writes.push(data); if (callback) writeCallbacks.push(callback); }, writeln: noop, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startLocal(term as never); assert.notEqual(onData, null); onData?.("hello"); onData?.("$ "); cursorX = 5; lineText = "hello"; writeCallbacks.shift()?.(); assert.deepEqual(writes, ["hello", "\r\n$ "]); assert.deepEqual(capturedLogData, ["hello", "$ "]); }); test("local session exit text waits for pending terminal output writes", async () => { const writes: string[] = []; const writeCallbacks: Array<() => void> = []; let onData: ((data: string) => void) | null = null; let onExit: ((evt: { reason?: "closed" }) => 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) => void) => { onData = cb; return noop; }, onSessionExit: (_id: string, cb: (evt: { reason?: "closed" }) => void) => { onExit = cb; return noop; }, onChainProgress: () => noop, writeToSession: noop, resizeSession: noop, }; const ctx = { host: { id: "local-host", label: "Local", hostname: "local", username: "", protocol: "local", }, 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: 20, rows: 4, write: (data: string, callback?: () => void) => { writes.push(data); if (callback) writeCallbacks.push(callback); }, writeln: (data: string) => { writes.push(`${data}\r\n`); }, scrollToBottom: noop, }; await createTerminalSessionStarters(ctx as never).startLocal(term as never); assert.notEqual(onData, null); assert.notEqual(onExit, null); onData?.("partial output"); onExit?.({ reason: "closed" }); assert.deepEqual(writes, ["partial output"]); writeCallbacks.shift()?.(); assert.deepEqual(writes, ["partial output", "\r\n[session closed]\r\n"]); }); test("startSSH allows jump hosts that use reference key files with unavailable saved passphrases", 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"] }, port: 2200, }, keys: [{ id: "jump-key", label: "Jump key", source: "reference", privateKey: "", filePath: "/Users/alice/.ssh/id_ed25519", passphrase: ENCRYPTED_CREDENTIAL_PLACEHOLDER, }], resolvedChainHosts: [{ id: "jump-1", label: "Jump", hostname: "jump.example.test", username: "jumper", authMethod: "key", identityFileId: "jump-key", }], 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.deepEqual(jumpHosts[0]?.identityFilePaths, ["/Users/alice/.ssh/id_ed25519"]); assert.equal(jumpHosts[0]?.privateKey, undefined); assert.equal(jumpHosts[0]?.passphrase, undefined); }); test("startSSH forwards per-host SSH settings to the native bridge", async () => { let capturedOptions: Record | null = null; 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", port: 22, password: "pw", requiresMfa: true, sshTcpConnectTimeoutSeconds: 45, sshAuthReadyTimeoutSeconds: 300, }, keys: [], knownHosts: [], resolvedChainHosts: [], sessionId: "session-1", terminalBackend, sshDebugLogEnabled: true, terminalSettings: { keepaliveInterval: 30, keepaliveCountMax: 10, }, 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, onSessionAttached: noop, }; const term = { cols: 120, rows: 32, write: (_data: string, cb?: () => void) => cb?.(), loadAddon: noop, }; await createTerminalSessionStarters(ctx as unknown as TerminalSessionStartersContext).startSSH(term); assert.equal(capturedOptions?.sshDebugLogEnabled, true); assert.equal(capturedOptions?.requiresMfa, true); assert.equal(capturedOptions?.sshTcpConnectTimeoutMs, 45_000); assert.equal(capturedOptions?.sshAuthReadyTimeoutMs, 300_000); }); test("startSSH omits identity file paths when password auth is selected", async () => { let capturedOptions: Record | null = null; 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: "password", password: "secret", useSshAgent: true, identityFilePaths: ["/Users/alice/.ssh/id_ed25519"], }, 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).startSSH(term as never); assert.ok(capturedOptions); assert.equal(capturedOptions.password, "secret"); assert.equal(capturedOptions.identityFilePaths, undefined); }); test("startSSH passes known host records to the SSH bridge", async () => { let capturedOptions: Record | null = null; const knownHosts = [{ id: "kh-1", hostname: "target.example.test", port: 22, keyType: "ssh-ed25519", publicKey: "SHA256:trusted-key", discoveredAt: 1, }]; 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: "password", password: "secret", }, keys: [], knownHosts, resolvedChainHosts: [], sessionId: "session-1", terminalSettings: { verifyHostKeys: false, }, 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).startSSH(term as never); assert.ok(capturedOptions); assert.equal(capturedOptions.knownHosts, knownHosts); assert.equal(capturedOptions.verifyHostKeys, false); }); test("startSSH omits jump host identity file paths when password auth is selected", async () => { let capturedOptions: Record | null = null; 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: "password", password: "secret", useSshAgent: true, 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: 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).startSSH(term as never); assert.ok(capturedOptions); const jumpHosts = capturedOptions.jumpHosts as Array>; assert.equal(jumpHosts[0]?.password, "secret"); assert.equal(jumpHosts[0]?.identityFilePaths, undefined); }); test("startSSH sends local identity file paths with saved passwords for key auth", async () => { let capturedOptions: Record | null = null; 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", password: "saved-password", identityFilePaths: ["/Users/alice/.ssh/id_ed25519"], }, 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).startSSH(term as never); assert.ok(capturedOptions); assert.equal(capturedOptions.password, "saved-password"); assert.deepEqual(capturedOptions.identityFilePaths, ["/Users/alice/.ssh/id_ed25519"]); });