[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:
713
components/terminal/terminalDragDropCwd.test.ts
Normal file
713
components/terminal/terminalDragDropCwd.test.ts
Normal file
@@ -0,0 +1,713 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type { DropEntry } from "../../lib/sftpFileUtils";
|
||||
import type { Host } from "../../types";
|
||||
import { TerminalDropNeedsSudoError } from "../../domain/sftpDropElevation";
|
||||
import {
|
||||
ActiveTerminalCwdUnavailableError,
|
||||
DEFAULT_RZ_MISSING_FALLBACK_TIMEOUT_MS,
|
||||
handleTerminalDropEntries,
|
||||
resolveTerminalDropErrorMessage,
|
||||
} from "./hooks/useTerminalDragDrop";
|
||||
import { resolvePreferredTerminalCwd } from "./sftpCwd";
|
||||
|
||||
const host = {
|
||||
id: "host-1",
|
||||
label: "Host",
|
||||
hostname: "example.com",
|
||||
port: 22,
|
||||
username: "alice",
|
||||
protocol: "ssh",
|
||||
} as Host;
|
||||
|
||||
const dropEntries: DropEntry[] = [
|
||||
{
|
||||
file: null,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
},
|
||||
];
|
||||
|
||||
test("terminal drag-drop allows the full ZMODEM startup window before falling back", () => {
|
||||
assert.equal(DEFAULT_RZ_MISSING_FALLBACK_TIMEOUT_MS, 15_000);
|
||||
});
|
||||
|
||||
test("terminal drag-drop shows actionable guidance when the active directory is unknown", () => {
|
||||
const requestedKeys: string[] = [];
|
||||
const message = resolveTerminalDropErrorMessage(
|
||||
new ActiveTerminalCwdUnavailableError(),
|
||||
(key) => {
|
||||
requestedKeys.push(key);
|
||||
return `translated:${key}`;
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(message, "translated:terminal.dragDrop.destinationUnknown");
|
||||
assert.deepEqual(requestedKeys, ["terminal.dragDrop.destinationUnknown"]);
|
||||
});
|
||||
|
||||
test("terminal drag-drop asks to enable sudo when /root is not writable as the login user", () => {
|
||||
const requestedKeys: string[] = [];
|
||||
const message = resolveTerminalDropErrorMessage(
|
||||
new TerminalDropNeedsSudoError(),
|
||||
(key) => {
|
||||
requestedKeys.push(key);
|
||||
return `translated:${key}`;
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(message, "translated:terminal.dragDrop.needsSudoElevation");
|
||||
assert.deepEqual(requestedKeys, ["terminal.dragDrop.needsSudoElevation"]);
|
||||
});
|
||||
|
||||
test("remote SSH terminal drop triggers ZMODEM drag-drop upload", async () => {
|
||||
let uploadedFiles: unknown;
|
||||
let uploadedSessionId: string | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [
|
||||
{
|
||||
file: {
|
||||
name: "report.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
} as File,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
},
|
||||
],
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
resolveSftpInitialPath: async () => "/srv/app/current",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
startZmodemDragDropUpload: async (sessionId, files) => {
|
||||
uploadedSessionId = sessionId;
|
||||
uploadedFiles = files;
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(uploadedSessionId, "session-1");
|
||||
assert.equal(Array.isArray(uploadedFiles), true);
|
||||
const files = uploadedFiles as Array<{ name: string; remoteName: string; data?: ArrayBuffer }>;
|
||||
assert.equal(files.length, 1);
|
||||
assert.equal(files[0].name, "report.txt");
|
||||
assert.equal(files[0].remoteName, "report.txt");
|
||||
assert.ok(files[0].data);
|
||||
});
|
||||
|
||||
test("remote SSH terminal drop stays on ZMODEM when rz starts", async () => {
|
||||
let openedSftp = false;
|
||||
let zmodemCallback: ((event: { type: string; transferType?: string }) => void) | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [
|
||||
{
|
||||
file: {
|
||||
name: "report.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
} as File,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
},
|
||||
],
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: () => {
|
||||
openedSftp = true;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/srv/app/current",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
cancelZmodem: () => {},
|
||||
onSessionData: () => () => {},
|
||||
onZmodemEvent: (_sessionId, cb) => {
|
||||
zmodemCallback = cb;
|
||||
return () => {
|
||||
zmodemCallback = undefined;
|
||||
};
|
||||
},
|
||||
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||||
assert.match(uploadCommand ?? "", /NetcattyRzMissing=/);
|
||||
zmodemCallback?.({ type: "detect", transferType: "upload" });
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(openedSftp, false);
|
||||
});
|
||||
|
||||
test("remote SSH terminal drop still waits for rz after the old 2.5 second deadline", async () => {
|
||||
let openedSftp = false;
|
||||
let zmodemCallback: ((event: { type: string; transferType?: string }) => void) | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [{
|
||||
file: {
|
||||
name: "report.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
} as File,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
}],
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: () => { openedSftp = true; },
|
||||
resolveSftpInitialPath: async () => "/srv/app/current",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
cancelZmodem: () => {},
|
||||
onSessionData: () => () => {},
|
||||
onZmodemEvent: (_sessionId, cb) => {
|
||||
zmodemCallback = cb;
|
||||
return () => { zmodemCallback = undefined; };
|
||||
},
|
||||
startZmodemDragDropUpload: async () => {
|
||||
setTimeout(() => {
|
||||
zmodemCallback?.({ type: "detect", transferType: "upload" });
|
||||
}, 2_600);
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(openedSftp, false);
|
||||
});
|
||||
|
||||
test("serial terminal drop does not wrap rz with an SSH shell fallback", async () => {
|
||||
let uploadCommandSeen: string | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [
|
||||
{
|
||||
file: {
|
||||
name: "report.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
} as File,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
},
|
||||
],
|
||||
host: { ...host, protocol: "serial" } as Host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: () => {},
|
||||
resolveSftpInitialPath: async () => "/srv/app/current",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
cancelZmodem: () => {},
|
||||
onSessionData: () => () => {},
|
||||
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||||
uploadCommandSeen = uploadCommand;
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(uploadCommandSeen, undefined);
|
||||
});
|
||||
|
||||
test("telnet terminal drop does not wrap rz with an SSH shell fallback", async () => {
|
||||
let uploadCommandSeen: string | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [
|
||||
{
|
||||
file: {
|
||||
name: "report.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
} as File,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
},
|
||||
],
|
||||
host: { ...host, protocol: "telnet" } as Host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: () => {},
|
||||
resolveSftpInitialPath: async () => "/srv/app/current",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
cancelZmodem: () => {},
|
||||
onSessionData: () => () => {},
|
||||
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||||
uploadCommandSeen = uploadCommand;
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(uploadCommandSeen, undefined);
|
||||
});
|
||||
|
||||
test("network device drop falls back to SFTP upload with a freshly resolved cwd", async () => {
|
||||
let receivedOptions: { preferFreshBackend?: boolean } | undefined;
|
||||
let openedPath: string | undefined;
|
||||
let openedEntries: DropEntry[] | undefined;
|
||||
let openedSessionId: string | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries,
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
isNetworkDevice: true,
|
||||
onOpenSftp: (_host, initialPath, pendingUploadEntries, sourceSessionId) => {
|
||||
openedPath = initialPath;
|
||||
openedEntries = pendingUploadEntries;
|
||||
openedSessionId = sourceSessionId;
|
||||
},
|
||||
resolveSftpInitialPath: async (options) => {
|
||||
receivedOptions = options;
|
||||
return "/srv/app/current";
|
||||
},
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.deepEqual(receivedOptions, {
|
||||
preferFreshBackend: true,
|
||||
requireActiveShellCwd: true,
|
||||
});
|
||||
assert.equal(openedPath, "/srv/app/current");
|
||||
assert.equal(openedEntries, dropEntries);
|
||||
assert.equal(openedSessionId, "session-1");
|
||||
});
|
||||
|
||||
test("remote SSH terminal drop falls back to SFTP when rz is unavailable", async () => {
|
||||
let receivedOptions: { preferFreshBackend?: boolean } | undefined;
|
||||
let openedPath: string | undefined;
|
||||
let openedEntries: DropEntry[] | undefined;
|
||||
let openedSessionId: string | undefined;
|
||||
let dataCallback: ((chunk: string) => void) | undefined;
|
||||
let cancelled: { sessionId: string; interrupt?: boolean } | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [
|
||||
{
|
||||
file: {
|
||||
name: "report.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
} as File,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
},
|
||||
],
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (_host, initialPath, pendingUploadEntries, sourceSessionId) => {
|
||||
openedPath = initialPath;
|
||||
openedEntries = pendingUploadEntries;
|
||||
openedSessionId = sourceSessionId;
|
||||
},
|
||||
resolveSftpInitialPath: async (options) => {
|
||||
receivedOptions = options;
|
||||
return "/srv/app/current";
|
||||
},
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
onSessionData: (_sessionId: string, cb: (chunk: string) => void) => {
|
||||
dataCallback = cb;
|
||||
return () => {
|
||||
dataCallback = undefined;
|
||||
};
|
||||
},
|
||||
cancelZmodem: (sessionId: string, options?: { interrupt?: boolean }) => {
|
||||
cancelled = { sessionId, interrupt: options?.interrupt };
|
||||
},
|
||||
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||||
assert.match(uploadCommand ?? "", /NetcattyRzMissing=/);
|
||||
assert.equal((uploadCommand ?? "").includes("\u001b]1337;NetcattyRzMissing="), false);
|
||||
const token = uploadCommand?.match(/NetcattyRzMissing=([A-Za-z0-9_-]+)/)?.[1];
|
||||
assert.ok(token);
|
||||
dataCallback?.(`\u001b]1337;NetcattyRzMissing=${token}\u0007`);
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.deepEqual(receivedOptions, {
|
||||
preferFreshBackend: true,
|
||||
requireActiveShellCwd: true,
|
||||
});
|
||||
assert.equal(openedPath, "/srv/app/current");
|
||||
assert.equal(openedEntries?.length, 1);
|
||||
assert.equal(openedEntries?.[0].relativePath, "report.txt");
|
||||
assert.equal(openedSessionId, "session-1");
|
||||
assert.deepEqual(cancelled, { sessionId: "session-1", interrupt: false });
|
||||
});
|
||||
|
||||
test("remote SSH terminal drop falls back to SFTP when rz never starts", async () => {
|
||||
let receivedOptions: { preferFreshBackend?: boolean } | undefined;
|
||||
let openedPath: string | undefined;
|
||||
let openedEntries: DropEntry[] | undefined;
|
||||
let cancelled: { sessionId: string; interrupt?: boolean } | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [
|
||||
{
|
||||
file: {
|
||||
name: "report.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
} as File,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
},
|
||||
],
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (_host, initialPath, pendingUploadEntries) => {
|
||||
openedPath = initialPath;
|
||||
openedEntries = pendingUploadEntries;
|
||||
},
|
||||
resolveSftpInitialPath: async (options) => {
|
||||
receivedOptions = options;
|
||||
return "/srv/app/current";
|
||||
},
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
onSessionData: () => () => {},
|
||||
cancelZmodem: (sessionId: string, options?: { interrupt?: boolean }) => {
|
||||
cancelled = { sessionId, interrupt: options?.interrupt };
|
||||
},
|
||||
startZmodemDragDropUpload: async () => ({ success: true }),
|
||||
},
|
||||
rzMissingFallbackTimeoutMs: 1,
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.deepEqual(receivedOptions, {
|
||||
preferFreshBackend: true,
|
||||
requireActiveShellCwd: true,
|
||||
});
|
||||
assert.equal(openedPath, "/srv/app/current");
|
||||
assert.equal(openedEntries?.length, 1);
|
||||
assert.deepEqual(cancelled, { sessionId: "session-1", interrupt: true });
|
||||
});
|
||||
|
||||
test("remote SSH folder drop uses SFTP to preserve directory structure", async () => {
|
||||
let openedEntries: DropEntry[] | undefined;
|
||||
let originSessionId: string | undefined;
|
||||
let sourceSessionId: string | undefined;
|
||||
let zmodemStarted = false;
|
||||
|
||||
const folderEntries: DropEntry[] = [
|
||||
{
|
||||
file: null,
|
||||
relativePath: "docs",
|
||||
isDirectory: true,
|
||||
},
|
||||
{
|
||||
file: {
|
||||
name: "guide.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
} as File,
|
||||
relativePath: "docs/guide.txt",
|
||||
isDirectory: false,
|
||||
},
|
||||
];
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: folderEntries,
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (_host, _initialPath, pendingUploadEntries, origin, source) => {
|
||||
openedEntries = pendingUploadEntries;
|
||||
originSessionId = origin;
|
||||
sourceSessionId = source;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/srv/app/current",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
startZmodemDragDropUpload: async () => {
|
||||
zmodemStarted = true;
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(zmodemStarted, false);
|
||||
assert.equal(openedEntries, folderEntries);
|
||||
assert.equal(originSessionId, "session-1");
|
||||
assert.equal(sourceSessionId, "session-1");
|
||||
});
|
||||
|
||||
test("Mosh folder drop keeps its origin but opens a fresh SFTP route", async () => {
|
||||
let originSessionId: string | undefined;
|
||||
let sourceSessionId: string | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||||
host: { ...host, moshEnabled: true },
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (_host, _path, _entries, origin, source) => {
|
||||
originSessionId = origin;
|
||||
sourceSessionId = source;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/srv/app/current",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "mosh-session",
|
||||
sessionRef: { current: "mosh-session" },
|
||||
terminalBackend: { writeToSession: () => {} },
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(originSessionId, "mosh-session");
|
||||
assert.equal(sourceSessionId, undefined);
|
||||
});
|
||||
|
||||
test("ET rz fallback keeps its origin but opens a fresh SFTP route", async () => {
|
||||
let originSessionId: string | undefined;
|
||||
let sourceSessionId: string | undefined;
|
||||
let dataCallback: ((chunk: string) => void) | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [{
|
||||
file: {
|
||||
name: "report.txt",
|
||||
arrayBuffer: async () => new Uint8Array([1]).buffer,
|
||||
} as File,
|
||||
relativePath: "report.txt",
|
||||
isDirectory: false,
|
||||
}],
|
||||
host: { ...host, etEnabled: true },
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (_host, _path, _entries, origin, source) => {
|
||||
originSessionId = origin;
|
||||
sourceSessionId = source;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/srv/app/current",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "et-session",
|
||||
sessionRef: { current: "et-session" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
onSessionData: (_sessionId, callback) => {
|
||||
dataCallback = callback;
|
||||
return () => { dataCallback = undefined; };
|
||||
},
|
||||
cancelZmodem: () => {},
|
||||
startZmodemDragDropUpload: async (_sessionId, _files, uploadCommand) => {
|
||||
const token = uploadCommand?.match(/NetcattyRzMissing=([A-Za-z0-9_-]+)/)?.[1];
|
||||
assert.ok(token);
|
||||
dataCallback?.(`\u001b]1337;NetcattyRzMissing=${token}\u0007`);
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(originSessionId, "et-session");
|
||||
assert.equal(sourceSessionId, undefined);
|
||||
});
|
||||
|
||||
test("remote SSH folder drop to /root reuses the saved host password for sudo SFTP", async () => {
|
||||
let openedHost: Host | undefined;
|
||||
let openedPath: string | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||||
host: { ...host, password: "secret" },
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (nextHost, initialPath) => {
|
||||
openedHost = nextHost;
|
||||
openedPath = initialPath;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/root",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: { writeToSession: () => {} },
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(openedPath, "/root");
|
||||
assert.equal(openedHost?.sftpSudo, true);
|
||||
assert.equal(openedHost?.password, "secret");
|
||||
assert.equal(host.sftpSudo, undefined);
|
||||
});
|
||||
|
||||
test("remote SSH folder drop to /root uses a resolved identity username over a stale host username", async () => {
|
||||
let openedHost: Host | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||||
host: { ...host, username: "root", password: "secret" },
|
||||
resolvedLoginUsername: "alice",
|
||||
resolvedSudoPassword: "secret",
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (nextHost) => {
|
||||
openedHost = nextHost;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/root",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: { writeToSession: () => {} },
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(openedHost?.sftpSudo, true);
|
||||
});
|
||||
|
||||
test("remote SSH folder drop to /root uses a resolved identity password", async () => {
|
||||
let openedHost: Host | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||||
host: { ...host, identityId: "id-1" },
|
||||
resolvedSudoPassword: "identity-secret",
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (nextHost) => {
|
||||
openedHost = nextHost;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/root",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: { writeToSession: () => {} },
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(openedHost?.sftpSudo, true);
|
||||
assert.equal(openedHost?.password, undefined);
|
||||
assert.equal(openedHost?.identityId, "id-1");
|
||||
});
|
||||
|
||||
test("remote SSH folder drop to /root fails closed without a saved sudo password", async () => {
|
||||
let openedSftp = false;
|
||||
|
||||
await assert.rejects(
|
||||
handleTerminalDropEntries({
|
||||
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: () => {
|
||||
openedSftp = true;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/root",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: { writeToSession: () => {} },
|
||||
termRef: { current: null },
|
||||
}),
|
||||
TerminalDropNeedsSudoError,
|
||||
);
|
||||
|
||||
assert.equal(openedSftp, false);
|
||||
});
|
||||
|
||||
test("remote SSH folder drop to the login home does not enable sudo", async () => {
|
||||
let openedHost: Host | undefined;
|
||||
|
||||
await handleTerminalDropEntries({
|
||||
dropEntries: [{ file: null, relativePath: "docs", isDirectory: true }],
|
||||
host: { ...host, password: "secret" },
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: (nextHost) => {
|
||||
openedHost = nextHost;
|
||||
},
|
||||
resolveSftpInitialPath: async () => "/home/alice",
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: { writeToSession: () => {} },
|
||||
termRef: { current: null },
|
||||
});
|
||||
|
||||
assert.equal(openedHost?.sftpSudo, undefined);
|
||||
});
|
||||
|
||||
test("remote SSH folder drop refuses to guess a destination when the active shell cwd is unknown", async () => {
|
||||
let openedSftp = false;
|
||||
let receivedOptions: unknown;
|
||||
|
||||
await assert.rejects(
|
||||
handleTerminalDropEntries({
|
||||
dropEntries: [
|
||||
{
|
||||
file: null,
|
||||
relativePath: "docs",
|
||||
isDirectory: true,
|
||||
},
|
||||
],
|
||||
host,
|
||||
isLocalConnection: false,
|
||||
onOpenSftp: () => {
|
||||
openedSftp = true;
|
||||
},
|
||||
resolveSftpInitialPath: async (options) => {
|
||||
receivedOptions = options;
|
||||
return undefined;
|
||||
},
|
||||
scrollToBottomAfterProgrammaticInput: () => {},
|
||||
sessionId: "session-1",
|
||||
sessionRef: { current: "session-1" },
|
||||
terminalBackend: {
|
||||
writeToSession: () => {},
|
||||
},
|
||||
termRef: { current: null },
|
||||
}),
|
||||
/Could not determine the active terminal directory/,
|
||||
);
|
||||
|
||||
assert.deepEqual(receivedOptions, {
|
||||
preferFreshBackend: true,
|
||||
requireActiveShellCwd: true,
|
||||
});
|
||||
assert.equal(openedSftp, false);
|
||||
});
|
||||
|
||||
test("fresh cwd resolution falls back to the renderer cwd when backend probe has no real cwd", async () => {
|
||||
const cwd = await resolvePreferredTerminalCwd({
|
||||
rendererCwd: "/srv/app/current",
|
||||
sessionId: "session-1",
|
||||
preferFreshBackend: true,
|
||||
getSessionPwd: async (_sessionId, options) => {
|
||||
assert.deepEqual(options, {
|
||||
allowHomeFallback: false,
|
||||
allowLoginShellFallback: true,
|
||||
});
|
||||
return { success: false, error: "Could not determine cwd" };
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(cwd, "/srv/app/current");
|
||||
});
|
||||
Reference in New Issue
Block a user