[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
260
application/state/sftp/ensureRemoteSftpSession.test.ts
Normal file
260
application/state/sftp/ensureRemoteSftpSession.test.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import type { Host } from "../../../domain/models";
|
||||
import { ensureRemoteSftpSession, probeSftpSession } from "./ensureRemoteSftpSession";
|
||||
import type { SftpPane } from "./types";
|
||||
|
||||
const host = {
|
||||
id: "host-1",
|
||||
label: "CI-Build-01",
|
||||
hostname: "ci.example",
|
||||
port: 22,
|
||||
username: "root",
|
||||
protocol: "ssh",
|
||||
} as Host;
|
||||
|
||||
const remotePane = (connectionId: string): SftpPane => ({
|
||||
id: "pane-1",
|
||||
connection: {
|
||||
id: connectionId,
|
||||
hostId: "host-1",
|
||||
hostLabel: "CI-Build-01",
|
||||
isLocal: false,
|
||||
status: "connected",
|
||||
currentPath: "/root",
|
||||
},
|
||||
files: [],
|
||||
loading: false,
|
||||
reconnecting: false,
|
||||
error: null,
|
||||
selectedFiles: new Set(),
|
||||
filter: "",
|
||||
filenameEncoding: "auto",
|
||||
showHiddenFiles: false,
|
||||
connectionLogs: [],
|
||||
} as unknown as SftpPane);
|
||||
|
||||
const noopReleaseConnection = async () => {};
|
||||
|
||||
test("SFTP session probe accepts a virtual root directory", async () => {
|
||||
const calls: Array<[string, string]> = [];
|
||||
const ok = await probeSftpSession({
|
||||
realpathSftp: async (sftpId, remotePath) => {
|
||||
calls.push([sftpId, remotePath]);
|
||||
return "/";
|
||||
},
|
||||
}, "sftp-jumpserver");
|
||||
|
||||
assert.equal(ok, true);
|
||||
assert.deepEqual(calls, [["sftp-jumpserver", "."]]);
|
||||
});
|
||||
|
||||
test("SFTP session probe does not require home discovery", async () => {
|
||||
const ok = await probeSftpSession(undefined, "sftp-live");
|
||||
assert.equal(ok, true);
|
||||
});
|
||||
|
||||
test("returns an existing mapped SFTP session without reconnecting", async () => {
|
||||
let connectCalls = 0;
|
||||
const sftpId = await ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => remotePane("conn-1"),
|
||||
sftpSessionsRef: { current: new Map([["conn-1", "sftp-live"]]) },
|
||||
lastConnectedHostRef: { current: { left: host, right: null } },
|
||||
connect: async () => { connectCalls += 1; },
|
||||
releaseConnection: noopReleaseConnection,
|
||||
});
|
||||
assert.equal(sftpId, "sftp-live");
|
||||
assert.equal(connectCalls, 0);
|
||||
});
|
||||
|
||||
test("reconnects when the mapped session is missing", async () => {
|
||||
let connectCalls = 0;
|
||||
const sessions = { current: new Map<string, string>() };
|
||||
const sftpId = await ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => {
|
||||
// After reconnect, connection id stays and mapping is filled by connect mock.
|
||||
return remotePane("conn-1");
|
||||
},
|
||||
sftpSessionsRef: sessions,
|
||||
lastConnectedHostRef: { current: { left: host, right: null } },
|
||||
connect: async () => {
|
||||
connectCalls += 1;
|
||||
sessions.current.set("conn-1", "sftp-reconnected");
|
||||
},
|
||||
releaseConnection: noopReleaseConnection,
|
||||
});
|
||||
assert.equal(connectCalls, 1);
|
||||
assert.equal(sftpId, "sftp-reconnected");
|
||||
});
|
||||
|
||||
test("resolves source session when restoring a missing browse session", async () => {
|
||||
let connectOptions: { initialPath?: string; sourceSessionId?: string } | undefined;
|
||||
let resolvedHost: Host | undefined;
|
||||
const sessions = { current: new Map<string, string>() };
|
||||
const sftpId = await ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => remotePane("conn-1"),
|
||||
sftpSessionsRef: sessions,
|
||||
lastConnectedHostRef: { current: { left: host, right: null } },
|
||||
resolveSourceSessionId: (hostId, reconnectHost) => {
|
||||
resolvedHost = reconnectHost;
|
||||
return hostId === "host-1" ? "term-1" : undefined;
|
||||
},
|
||||
connect: async (_side, _host, options) => {
|
||||
connectOptions = options;
|
||||
sessions.current.set("conn-1", "sftp-restored");
|
||||
},
|
||||
releaseConnection: noopReleaseConnection,
|
||||
});
|
||||
assert.equal(sftpId, "sftp-restored");
|
||||
assert.equal(resolvedHost?.hostname, "ci.example");
|
||||
assert.equal(connectOptions?.initialPath, "/root");
|
||||
assert.equal(connectOptions?.sourceSessionId, "term-1");
|
||||
});
|
||||
|
||||
test("forceReconnect reopens even when a mapping exists", async () => {
|
||||
let connectCalls = 0;
|
||||
const released: string[] = [];
|
||||
const sessions = { current: new Map([["conn-1", "sftp-stale"]]) };
|
||||
const sftpId = await ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => remotePane("conn-1"),
|
||||
sftpSessionsRef: sessions,
|
||||
lastConnectedHostRef: { current: { left: host, right: null } },
|
||||
forceReconnect: true,
|
||||
releaseConnection: async (connectionId) => { released.push(connectionId); },
|
||||
connect: async () => {
|
||||
connectCalls += 1;
|
||||
sessions.current.set("conn-1", "sftp-new");
|
||||
},
|
||||
});
|
||||
assert.equal(connectCalls, 1);
|
||||
assert.deepEqual(released, ["conn-1"]);
|
||||
assert.equal(sftpId, "sftp-new");
|
||||
});
|
||||
|
||||
test("probe failure triggers reconnect", async () => {
|
||||
let connectCalls = 0;
|
||||
const released: string[] = [];
|
||||
const sessions = { current: new Map([["conn-1", "sftp-dead"]]) };
|
||||
const sftpId = await ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => remotePane("conn-1"),
|
||||
sftpSessionsRef: sessions,
|
||||
lastConnectedHostRef: { current: { left: host, right: null } },
|
||||
probeSession: async () => {
|
||||
throw new Error("SFTP session not found");
|
||||
},
|
||||
releaseConnection: async (connectionId) => { released.push(connectionId); },
|
||||
connect: async () => {
|
||||
connectCalls += 1;
|
||||
sessions.current.set("conn-1", "sftp-fresh");
|
||||
},
|
||||
});
|
||||
assert.equal(connectCalls, 1);
|
||||
assert.deepEqual(released, ["conn-1"]);
|
||||
assert.equal(sftpId, "sftp-fresh");
|
||||
});
|
||||
|
||||
test("uses resolveHostById when lastConnectedHostRef is empty", async () => {
|
||||
let connectedHost: Host | "local" | null = null;
|
||||
const sessions = { current: new Map<string, string>() };
|
||||
const sftpId = await ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => remotePane("conn-1"),
|
||||
sftpSessionsRef: sessions,
|
||||
lastConnectedHostRef: { current: { left: null, right: null } },
|
||||
resolveHostById: (id) => (id === "host-1" ? host : null),
|
||||
connect: async (_side, resolved) => {
|
||||
connectedHost = resolved;
|
||||
sessions.current.set("conn-1", "sftp-vault");
|
||||
},
|
||||
releaseConnection: noopReleaseConnection,
|
||||
});
|
||||
assert.equal(sftpId, "sftp-vault");
|
||||
assert.equal((connectedHost as Host).hostname, "ci.example");
|
||||
assert.equal((connectedHost as Host).username, "root");
|
||||
});
|
||||
|
||||
test("prefers per-tab connect-time host over vault base endpoint", async () => {
|
||||
const vaultHost = {
|
||||
...host,
|
||||
hostname: "vault.example",
|
||||
port: 22,
|
||||
username: "root",
|
||||
} as Host;
|
||||
const sessionHost = {
|
||||
...host,
|
||||
hostname: "session.example",
|
||||
port: 2222,
|
||||
username: "override",
|
||||
} as Host;
|
||||
let connectedHost: Host | "local" | null = null;
|
||||
const sessions = { current: new Map<string, string>() };
|
||||
await ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => remotePane("conn-1"),
|
||||
sftpSessionsRef: sessions,
|
||||
lastConnectedHostRef: { current: { left: null, right: null } },
|
||||
resolveConnectedHost: (id) => (id === "pane-1" ? sessionHost : null),
|
||||
resolveHostById: () => vaultHost,
|
||||
connect: async (_side, resolved) => {
|
||||
connectedHost = resolved;
|
||||
sessions.current.set("conn-1", "sftp-session");
|
||||
},
|
||||
releaseConnection: noopReleaseConnection,
|
||||
});
|
||||
assert.equal((connectedHost as Host).hostname, "session.example");
|
||||
assert.equal((connectedHost as Host).port, 2222);
|
||||
assert.equal((connectedHost as Host).username, "override");
|
||||
});
|
||||
|
||||
test("does not retarget background reconnect via side-wide lastConnectedHost overrides", async () => {
|
||||
const vaultHost = {
|
||||
...host,
|
||||
hostname: "vault.example",
|
||||
port: 22,
|
||||
username: "root",
|
||||
} as Host;
|
||||
const activeTabOverrides = {
|
||||
...host,
|
||||
hostname: "other-tab.example",
|
||||
port: 2222,
|
||||
username: "other",
|
||||
} as Host;
|
||||
let connectedHost: Host | "local" | null = null;
|
||||
const sessions = { current: new Map<string, string>() };
|
||||
await ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => remotePane("conn-1"),
|
||||
sftpSessionsRef: sessions,
|
||||
lastConnectedHostRef: { current: { left: activeTabOverrides, right: null } },
|
||||
resolveHostById: () => vaultHost,
|
||||
connect: async (_side, resolved) => {
|
||||
connectedHost = resolved;
|
||||
sessions.current.set("conn-1", "sftp-vault");
|
||||
},
|
||||
releaseConnection: noopReleaseConnection,
|
||||
});
|
||||
assert.equal((connectedHost as Host).hostname, "vault.example");
|
||||
assert.equal((connectedHost as Host).port, 22);
|
||||
});
|
||||
|
||||
test("refuses synthetic root@label:22 when host metadata is missing", async () => {
|
||||
await assert.rejects(
|
||||
() => ensureRemoteSftpSession({
|
||||
side: "left",
|
||||
getActivePane: () => remotePane("conn-1"),
|
||||
sftpSessionsRef: { current: new Map() },
|
||||
lastConnectedHostRef: { current: { left: null, right: null } },
|
||||
connect: async () => {
|
||||
throw new Error("should not connect");
|
||||
},
|
||||
releaseConnection: noopReleaseConnection,
|
||||
}),
|
||||
/credentials are unavailable/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user