Files
Netcatty/domain/sessionRestore.test.ts
T
陈大猫 c6baebd514 Support password in ssh:// deep links with ephemeral connect (#1963)
* Support password in ssh deep links with ephemeral connect

ssh://user:password@host URLs now connect directly using the URL
credentials without saving the host to the vault, covering bastion
one-time password flows. Password-less links keep the existing
match-or-prefill behavior.

Fixes #1890

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep ephemeral deep-link hosts out of vault update paths

Terminal-layer host updates (e.g. the SFTP follow-CWD toggle) map over
the merged terminal host list, which could persist an ephemeral host
and its plaintext password into the vault. Route terminal-layer host
updates through a splitter that applies ephemeral entries to in-memory
state only and forwards just the vault hosts to updateHosts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Exclude ephemeral deep-link sessions from session restore

Sessions backed by an in-memory ephemeral host cannot survive a
relaunch (the one-time password only lives in memory), so persisting
them would leak the ephemeral host metadata into restore storage and
restore a tab that can never reconnect. Mark ephemeral hosts and their
sessions, and filter them out of the session-restore payload.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Treat ephemeral hosts as unsaved in appearance classification

The terminal theme/font panel and pane appearance resolution decided
"saved vault host" via hostMap membership. Ephemeral deep-link hosts
are now in that map, so theme/font changes were routed to the
persisted-host updater and silently dropped. Classify via
isSavedVaultHost (ephemeral flag aware) so those changes fall back to
the session/global setting path as before.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Reuse saved host settings for password deep links and keep ephemeral marker on clones

Password deep links now run saved-host matching first: a uniquely
matched vault host contributes its non-credential settings (proxy,
jump chain, charset, group, ...) to the ephemeral host while the URL
credentials override vault identities and key references. Session
clones (split/copy/copy-to-new-window) now carry the ephemeralHost
marker so cloned tabs stay excluded from session restore.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Block group credential inheritance on password deep-link ephemeral hosts

buildSshDeepLinkEphemeralHostFromSaved now takes the group-resolved
effective host and clears group/identityFilePaths in addition to
identity references, so a later resolveEffectiveHost pass cannot
re-inherit group default credentials (identity, key, password) over
the one-time URL password. Non-credential group defaults (proxy,
jump chain, charset, ...) are preserved via the effective host.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Route ephemeral-host font zoom through the per-session font-size path

Ctrl+zoom on a non-workspace ephemeral deep-link session hit the
saved-host update path, where the vault updater silently dropped the
change (ephemeral ids are not in the persisted hosts array). Zoom and
the theme panel font-size slider/reset now store the size on the
session (rendered via applySessionFontSizeToHost) for ephemeral
hosts, matching the workspace-pane behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix ephemeral session font zoom routing

* Prevent ephemeral deep-link sudo autofill

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 22:56:32 +08:00

613 lines
19 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
buildSessionRestorePayload,
isRestoredDisconnectedSession,
quoteRestoreCwdForShell,
resolveRestoredActiveTabId,
resolveRestoreCwdIntent,
sanitizeSessionRestorePayload,
shouldAttemptRestoreCwd,
} from "./sessionRestore.ts";
import type { TerminalSession, Workspace } from "./models.ts";
const session = (id: string, workspaceId?: string): TerminalSession => ({
id,
hostId: `host-${id}`,
hostLabel: `Host ${id}`,
hostname: `${id}.example.test`,
username: "root",
status: "connected",
protocol: "ssh",
workspaceId,
});
test("buildSessionRestorePayload stores restored sessions as disconnected and drops reuse pointers", () => {
const payload = buildSessionRestorePayload({
sessions: [{ ...session("s1"), reuseConnectionFromSessionId: "source" }],
workspaces: [],
tabOrder: ["s1"],
activeTabId: "s1",
now: 123,
});
assert.equal(payload.version, 1);
assert.equal(payload.savedAt, 123);
assert.equal(payload.sessions[0].status, "disconnected");
assert.equal(payload.sessions[0].reuseConnectionFromSessionId, undefined);
assert.equal(payload.sessions[0].restoreState, "restored-disconnected");
});
test("buildSessionRestorePayload excludes ephemeral-host sessions and their tabs", () => {
const payload = buildSessionRestorePayload({
sessions: [
session("s1"),
{ ...session("s2"), ephemeralHost: true },
],
workspaces: [],
tabOrder: ["s1", "s2"],
activeTabId: "s2",
now: 123,
});
assert.deepEqual(payload.sessions.map((entry) => entry.id), ["s1"]);
assert.deepEqual(payload.tabOrder, ["s1"]);
assert.notEqual(payload.activeTabId, "s2");
});
test("buildSessionRestorePayload only stores allowlisted session fields", () => {
const payload = buildSessionRestorePayload({
sessions: [{
...session("s1"),
terminalData: "do-not-store",
reuseConnectionFromSessionId: "source",
} as TerminalSession & { terminalData: string }],
workspaces: [],
tabOrder: ["s1"],
activeTabId: "s1",
now: 123,
});
assert.deepEqual(Object.keys(payload.sessions[0]).sort(), [
"hostId",
"hostLabel",
"hostname",
"id",
"protocol",
"restoreState",
"status",
"username",
].sort());
});
test("buildSessionRestorePayload preserves local terminal start directory", () => {
const payload = buildSessionRestorePayload({
sessions: [{
...session("s1"),
protocol: "local",
localStartDir: "/Users/alice/project",
}],
workspaces: [],
tabOrder: ["s1"],
activeTabId: "s1",
now: 123,
});
assert.equal(payload.sessions[0].localStartDir, "/Users/alice/project");
});
test("buildSessionRestorePayload deeply allowlists serial config fields", () => {
const payload = buildSessionRestorePayload({
sessions: [{
...session("s1"),
protocol: "serial",
serialConfig: {
path: "/dev/tty.usbserial",
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: "none",
flowControl: "none",
localEcho: true,
lineMode: true,
password: "do-not-store",
},
} as TerminalSession & { serialConfig: TerminalSession["serialConfig"] & { password: string } }],
workspaces: [],
tabOrder: ["s1"],
activeTabId: "s1",
now: 123,
});
assert.deepEqual(payload.sessions[0].serialConfig, {
path: "/dev/tty.usbserial",
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: "none",
flowControl: "none",
localEcho: true,
lineMode: true,
});
});
test("buildSessionRestorePayload preserves serial sessions with empty usernames", () => {
const payload = buildSessionRestorePayload({
sessions: [{
...session("s1"),
username: "",
protocol: "serial",
serialConfig: {
path: "/dev/tty.usbserial",
baudRate: 115200,
},
}],
workspaces: [],
tabOrder: ["s1"],
activeTabId: "s1",
now: 123,
});
assert.equal(payload.sessions.length, 1);
assert.equal(payload.sessions[0].username, "");
assert.equal(payload.sessions[0].protocol, "serial");
});
test("buildSessionRestorePayload preserves serial-only workspaces", () => {
const payload = buildSessionRestorePayload({
sessions: [{
...session("s1", "ws-1"),
username: "",
protocol: "serial",
serialConfig: {
path: "/dev/tty.usbserial",
baudRate: 115200,
},
}],
workspaces: [{
id: "ws-1",
title: "Serial workspace",
root: { id: "pane-1", type: "pane", sessionId: "s1" },
}],
tabOrder: ["ws-1"],
activeTabId: "ws-1",
now: 123,
});
assert.equal(payload.sessions.length, 1);
assert.equal(payload.workspaces.length, 1);
assert.equal(payload.activeTabId, "ws-1");
});
test("buildSessionRestorePayload drops startup commands from restored sessions", () => {
const payload = buildSessionRestorePayload({
sessions: [{
...session("s1"),
startupCommand: "rm -rf /tmp/example",
noAutoRun: false,
}],
workspaces: [],
tabOrder: ["s1"],
activeTabId: "s1",
now: 123,
});
assert.equal(payload.sessions[0].startupCommand, undefined);
assert.equal(payload.sessions[0].noAutoRun, undefined);
});
test("sanitizeSessionRestorePayload prunes invalid workspace panes and drops empty workspaces", () => {
const workspace: Workspace = {
id: "ws-1",
title: "Workspace",
root: {
id: "split-1",
type: "split",
direction: "vertical",
children: [
{ id: "pane-1", type: "pane", sessionId: "s1" },
{ id: "pane-2", type: "pane", sessionId: "missing" },
],
sizes: [0.25, 0.75],
},
focusedSessionId: "missing",
focusSessionOrder: ["missing", "s1"],
};
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "missing",
tabOrder: ["missing", "ws-1", "s1"],
sessions: [session("s1", "ws-1")],
workspaces: [workspace],
});
assert.equal(sanitized.sessions.length, 1);
assert.equal(sanitized.workspaces.length, 1);
assert.equal(sanitized.workspaces[0].root.type, "pane");
assert.equal(sanitized.workspaces[0].focusedSessionId, "s1");
assert.deepEqual(sanitized.workspaces[0].focusSessionOrder, ["s1"]);
assert.deepEqual(sanitized.tabOrder, ["ws-1"]);
assert.equal(sanitized.activeTabId, "ws-1");
});
test("sanitizeSessionRestorePayload drops malformed session and workspace records", () => {
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "ws-1",
tabOrder: ["ws-1", "s1"],
sessions: [null, session("s1")],
workspaces: [{ id: "ws-1" }],
} as unknown);
assert.deepEqual(sanitized.sessions.map((item) => item.id), ["s1"]);
assert.deepEqual(sanitized.workspaces, []);
assert.deepEqual(sanitized.tabOrder, ["s1"]);
assert.equal(sanitized.activeTabId, "s1");
});
test("sanitizeSessionRestorePayload preserves local terminal start directory", () => {
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "s1",
tabOrder: ["s1"],
sessions: [{
...session("s1"),
protocol: "local",
localStartDir: "/Users/alice/project",
}],
workspaces: [],
});
assert.equal(sanitized.sessions[0].localStartDir, "/Users/alice/project");
});
test("sanitizeSessionRestorePayload enforces workspace session ownership", () => {
const workspace: Workspace = {
id: "ws-1",
title: "Workspace",
root: {
id: "split-1",
type: "split",
direction: "horizontal",
children: [
{ id: "pane-1", type: "pane", sessionId: "owned" },
{ id: "pane-2", type: "pane", sessionId: "standalone" },
{ id: "pane-3", type: "pane", sessionId: "other-workspace" },
],
sizes: [0.2, 0.3, 0.5],
},
focusedSessionId: "standalone",
focusSessionOrder: ["standalone", "owned", "other-workspace"],
};
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "ws-1",
tabOrder: ["ws-1", "standalone", "ws-2"],
sessions: [
session("owned", "ws-1"),
session("standalone"),
session("other-workspace", "ws-2"),
],
workspaces: [
workspace,
{
id: "ws-2",
title: "Other",
root: { id: "pane-4", type: "pane", sessionId: "other-workspace" },
},
],
});
assert.equal(sanitized.workspaces[0].root.type, "pane");
assert.equal(sanitized.workspaces[0].root.sessionId, "owned");
assert.equal(sanitized.workspaces[0].focusedSessionId, "owned");
assert.deepEqual(sanitized.workspaces[0].focusSessionOrder, ["owned"]);
assert.deepEqual(sanitized.tabOrder, ["ws-1", "standalone", "ws-2"]);
});
test("sanitizeSessionRestorePayload drops workspace sessions missing from the restored root", () => {
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "ws-1",
tabOrder: ["ws-1"],
sessions: [
session("s1", "ws-1"),
session("s2", "ws-1"),
],
workspaces: [{
id: "ws-1",
title: "Workspace",
root: { id: "pane-1", type: "pane", sessionId: "s1" },
focusedSessionId: "s2",
focusSessionOrder: ["s2", "s1"],
}],
});
assert.deepEqual(sanitized.sessions.map((entry) => entry.id), ["s1"]);
assert.equal(sanitized.workspaces[0].focusedSessionId, "s1");
assert.deepEqual(sanitized.workspaces[0].focusSessionOrder, ["s1"]);
assert.deepEqual(sanitized.tabOrder, ["ws-1"]);
});
test("sanitizeSessionRestorePayload allowlists workspace node fields", () => {
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "ws-1",
tabOrder: ["ws-1"],
sessions: [session("s1", "ws-1"), session("s2", "ws-1")],
workspaces: [{
id: "ws-1",
title: "Workspace",
root: {
id: "split-1",
type: "split",
direction: "horizontal",
terminalData: "do-not-store",
children: [
{ id: "pane-1", type: "pane", sessionId: "s1", terminalData: "do-not-store" },
{ id: "pane-2", type: "pane", sessionId: "s2", secret: "do-not-store" },
],
sizes: [0.4, 0.6],
},
}],
});
const root = sanitized.workspaces[0].root;
assert.equal("terminalData" in root, false);
assert.equal(root.type, "split");
assert.equal("terminalData" in root.children[0], false);
assert.equal("secret" in root.children[1], false);
});
test("sanitizeSessionRestorePayload deeply allowlists unknown serial config fields", () => {
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "s1",
tabOrder: ["s1"],
sessions: [{
...session("s1"),
protocol: "serial",
serialConfig: {
path: "/dev/tty.usbserial",
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: "none",
flowControl: "none",
localEcho: true,
lineMode: true,
secret: "do-not-store",
},
}],
workspaces: [],
});
assert.deepEqual(sanitized.sessions[0].serialConfig, {
path: "/dev/tty.usbserial",
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: "none",
flowControl: "none",
localEcho: true,
lineMode: true,
});
});
test("sanitizeSessionRestorePayload drops invalid protocol and shell type enum values", () => {
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "s1",
tabOrder: ["s1"],
sessions: [{
...session("s1"),
protocol: "ftp",
shellType: "bash",
}],
workspaces: [],
});
assert.equal(sanitized.sessions[0].protocol, undefined);
assert.equal(sanitized.sessions[0].shellType, undefined);
});
test("sanitizeSessionRestorePayload falls back to equal split sizes when saved sizes are non-positive", () => {
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "ws-1",
tabOrder: ["ws-1"],
sessions: [session("s1", "ws-1"), session("s2", "ws-1")],
workspaces: [{
id: "ws-1",
title: "Workspace",
root: {
id: "split-1",
type: "split",
direction: "horizontal",
children: [
{ id: "pane-1", type: "pane", sessionId: "s1" },
{ id: "pane-2", type: "pane", sessionId: "s2" },
],
sizes: [-1, 2],
},
}],
});
assert.equal(sanitized.workspaces[0].root.type, "split");
if (sanitized.workspaces[0].root.type === "split") {
assert.deepEqual(sanitized.workspaces[0].root.sizes, [0.5, 0.5]);
}
});
test("sanitizeSessionRestorePayload preserves split sizes when nested splits collapse", () => {
const sanitized = sanitizeSessionRestorePayload({
version: 1,
savedAt: 1,
activeTabId: "ws-1",
tabOrder: ["ws-1"],
sessions: [session("s1", "ws-1"), session("s3", "ws-1")],
workspaces: [{
id: "ws-1",
title: "Workspace",
root: {
id: "root-split",
type: "split",
direction: "horizontal",
sizes: [0.7, 0.3],
children: [
{
id: "left-split",
type: "split",
direction: "vertical",
sizes: [0.4, 0.6],
children: [
{ id: "pane-1", type: "pane", sessionId: "s1" },
{ id: "pane-missing", type: "pane", sessionId: "missing" },
],
},
{ id: "pane-3", type: "pane", sessionId: "s3" },
],
},
}],
});
const root = sanitized.workspaces[0].root;
assert.equal(root.type, "split");
if (root.type === "split") {
assert.deepEqual(root.children.map((child) => child.type === "pane" ? child.sessionId : "split"), ["s1", "s3"]);
assert.deepEqual(root.sizes, [0.7, 0.3]);
assert.ok(root.sizes?.every((size) => size > 0));
}
});
test("resolveRestoredActiveTabId falls back to vault when no restored tab is valid", () => {
assert.equal(resolveRestoredActiveTabId("missing", [], [], []), "vault");
});
test("resolveRestoredActiveTabId does not restore sftp as an active startup tab", () => {
assert.equal(resolveRestoredActiveTabId("sftp", ["sftp"], [], []), "vault");
});
test("isRestoredDisconnectedSession detects only explicit restored sessions", () => {
assert.equal(
isRestoredDisconnectedSession({
...session("s1"),
status: "disconnected",
restoreState: "restored-disconnected",
}),
true,
);
assert.equal(isRestoredDisconnectedSession({ ...session("s1"), status: "disconnected" }), false);
});
test("shouldAttemptRestoreCwd allows restored local and unix ssh sessions only when enabled", () => {
assert.equal(shouldAttemptRestoreCwd({
enabled: true,
session: { ...session("s1"), status: "disconnected", restoreState: "restored-disconnected", lastCwd: "/srv/app" },
isNetworkDevice: false,
}), true);
assert.equal(shouldAttemptRestoreCwd({
enabled: false,
session: { ...session("s1"), status: "disconnected", restoreState: "restored-disconnected", lastCwd: "/srv/app" },
isNetworkDevice: false,
}), false);
});
test("shouldAttemptRestoreCwd skips ineligible protocols and network devices", () => {
const base = { ...session("s1"), status: "disconnected" as const, restoreState: "restored-disconnected" as const, lastCwd: "/srv/app" };
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, protocol: "telnet" }, isNetworkDevice: false }), false);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, protocol: "serial" }, isNetworkDevice: false }), false);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, protocol: "ssh" }, isNetworkDevice: true }), false);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, protocol: "ssh", moshEnabled: true }, isNetworkDevice: false }), false);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, protocol: "ssh", etEnabled: true }, isNetworkDevice: false }), false);
});
test("shouldAttemptRestoreCwd skips local Windows-like shell types", () => {
const base = {
...session("s1"),
status: "disconnected" as const,
restoreState: "restored-disconnected" as const,
protocol: "local" as const,
lastCwd: "/srv/app",
};
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, shellType: "posix" }, isNetworkDevice: false }), true);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, shellType: "fish" }, isNetworkDevice: false }), true);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, shellType: "powershell" }, isNetworkDevice: false }), false);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, shellType: "cmd" }, isNetworkDevice: false }), false);
});
test("shouldAttemptRestoreCwd skips missing and windows-like cwd values", () => {
const base = { ...session("s1"), status: "disconnected" as const, restoreState: "restored-disconnected" as const };
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base }, isNetworkDevice: false }), false);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, lastCwd: "C:\\Users\\alice" }, isNetworkDevice: false }), false);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, lastCwd: "relative/path" }, isNetworkDevice: false }), false);
assert.equal(shouldAttemptRestoreCwd({ enabled: true, session: { ...base, lastCwd: "~alice/project" }, isNetworkDevice: false }), false);
});
test("quoteRestoreCwdForShell treats cwd as shell data", () => {
assert.equal(quoteRestoreCwdForShell("/srv/app"), "'/srv/app'");
assert.equal(quoteRestoreCwdForShell("/srv/app dir"), "'/srv/app dir'");
assert.equal(quoteRestoreCwdForShell("/tmp/it's ok; $(rm -rf ~)"), "'/tmp/it'\\''s ok; $(rm -rf ~)'");
});
test("resolveRestoreCwdIntent captures a one-shot restore command", () => {
assert.deepEqual(resolveRestoreCwdIntent({
enabled: true,
session: { ...session("s1"), status: "disconnected", restoreState: "restored-disconnected", lastCwd: "/srv/app dir" },
isNetworkDevice: false,
}), {
cwd: "/srv/app dir",
command: "cd -- '/srv/app dir'",
});
});
test("resolveRestoreCwdIntent does not emit POSIX cd for local Windows-like shells", () => {
assert.equal(resolveRestoreCwdIntent({
enabled: true,
session: {
...session("s1"),
status: "disconnected",
restoreState: "restored-disconnected",
protocol: "local",
shellType: "powershell",
lastCwd: "/srv/app",
},
isNetworkDevice: false,
}), null);
});
test("resolveRestoreCwdIntent keeps home-relative cwd expandable", () => {
assert.deepEqual(resolveRestoreCwdIntent({
enabled: true,
session: { ...session("s1"), status: "disconnected", restoreState: "restored-disconnected", lastCwd: "~/project dir" },
isNetworkDevice: false,
}), {
cwd: "~/project dir",
command: "cd -- ~/'project dir'",
});
assert.deepEqual(resolveRestoreCwdIntent({
enabled: true,
session: { ...session("s1"), status: "disconnected", restoreState: "restored-disconnected", lastCwd: "~" },
isNetworkDevice: false,
}), {
cwd: "~",
command: "cd -- ~",
});
});