Files
NetMesh/components/terminal/runtime/createTerminalSessionStarters.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

4680 lines
146 KiB
TypeScript

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<string, unknown> = {}) => ({
cols: 120,
rows: 32,
write: (_data: string, callback?: () => void) => callback?.(),
writeln: noop,
scrollToBottom: noop,
...overrides,
});
const createStarterContext = (overrides: Record<string, unknown> = {}) => ({
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<void>((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<string, unknown> }> = [];
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<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown>[] = [];
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string | undefined> = [];
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<string, unknown>[] = [];
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown>[] = [];
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string>((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<string, unknown>[] = [];
let requiresFreshConnection = false;
let commitCount = 0;
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<void>((resolve) => { releaseStartEntered = resolve; });
const needsAuthCalls: boolean[] = [];
const statuses: string[] = [];
const errors: Array<string | null> = [];
const isBootActiveRef = { current: true };
const bootEpochRef = { current: 1 };
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async () => {
releaseStartEntered?.();
return await new Promise<string>((_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<void>((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<string>((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<void>((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<string>((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<void>((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<string>((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<void>((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<string>((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<void>((resolve) => { releaseStartEntered = resolve; });
const awaitingUserInput: boolean[] = [];
const pastTcpDial: boolean[] = [];
const chainProgress: Array<unknown> = [];
let unsubscribed = false;
const isBootActiveRef = { current: true };
const bootEpochRef = { current: 1 };
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async () => {
releaseStartEntered?.();
return await new Promise<string>((_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<void>((resolve) => { releaseStartEntered = resolve; });
const startCalls: Array<Record<string, unknown>> = [];
const progressLogs: string[] = [];
const isBootActiveRef = { current: true };
const bootEpochRef = { current: 1 };
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
startCalls.push(options);
releaseStartEntered?.();
return await new Promise<string>((_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<void>((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<string>(() => {});
},
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<string, unknown>[] = [];
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
let needsAuth = false;
let authRetryMessage: string | null = null;
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
moshAvailable: () => true,
etAvailable: () => true,
startMoshSession: async (options: Record<string, unknown>) => {
capturedOptions = options;
return "mosh-session";
},
startEtSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
moshAvailable: () => true,
etAvailable: () => true,
startMoshSession: async (options: Record<string, unknown>) => {
capturedOptions = options;
return "mosh-session";
},
startEtSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
let needsAuth = false;
const terminalBackend = {
backendAvailable: () => true,
moshAvailable: () => true,
etAvailable: () => true,
startMoshSession: async (options: Record<string, unknown>) => {
capturedOptions = options;
return "mosh-session";
},
startEtSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
etAvailable: () => true,
startEtSession: async (options: Record<string, unknown>) => {
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<Record<string, unknown>> | 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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<Record<string, unknown>>;
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<string, unknown> | null = null;
let startCalls = 0;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown>[] = [];
const tcpDialState: boolean[] = [];
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | 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<string, unknown>) => {
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<string, unknown> | null = null;
let error = "";
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<Record<string, unknown>>;
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<string, unknown> | 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<string, unknown>) => {
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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<Record<string, unknown>>;
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<string, unknown> | null = null;
const terminalBackend = {
backendAvailable: () => true,
telnetAvailable: () => true,
moshAvailable: () => true,
localAvailable: () => true,
serialAvailable: () => true,
execAvailable: () => true,
startSSHSession: async (options: Record<string, unknown>) => {
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"]);
});