[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:
341
application/state/sftp/utils.test.ts
Normal file
341
application/state/sftp/utils.test.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
getParentPath,
|
||||
getFileName,
|
||||
getSftpBreadcrumbSegments,
|
||||
getSftpFilterAfterPathChange,
|
||||
getSftpFilterAfterPathChangeError,
|
||||
getSftpPathRoot,
|
||||
isConcreteTransferTargetPath,
|
||||
isSftpDescendantPath,
|
||||
isWindowsRoot,
|
||||
joinTransferTargetPath,
|
||||
normalizeSftpNavigationPath,
|
||||
normalizeSftpPaneNavigationPath,
|
||||
resolveSftpWindowsPathOptions,
|
||||
shouldClearSftpFilterForPathChange,
|
||||
} from "./utils";
|
||||
|
||||
test("directory targets cannot escape the selected local root", () => {
|
||||
const windowsRoot = "C:\\Users\\alice\\Downloads\\folder";
|
||||
for (const relativePath of [
|
||||
"..\\outside.txt",
|
||||
"../outside.txt",
|
||||
"C:\\outside.txt",
|
||||
"C:/outside.txt",
|
||||
"\\\\server\\share\\outside.txt",
|
||||
"/absolute/outside.txt",
|
||||
".. ",
|
||||
". ",
|
||||
"safe.txt:alternate-stream",
|
||||
]) {
|
||||
assert.throws(
|
||||
() => joinTransferTargetPath(windowsRoot, relativePath),
|
||||
/unsafe transfer path/i,
|
||||
relativePath,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
joinTransferTargetPath(windowsRoot, "nested/report.txt"),
|
||||
"C:\\Users\\alice\\Downloads\\folder\\nested\\report.txt",
|
||||
);
|
||||
assert.throws(
|
||||
() => joinTransferTargetPath("/Users/alice/Downloads/folder", "../outside.txt"),
|
||||
/unsafe transfer path/i,
|
||||
);
|
||||
|
||||
const uncRoot = "\\\\server\\share\\Downloads\\folder";
|
||||
assert.throws(
|
||||
() => joinTransferTargetPath(uncRoot, "..\\outside.txt"),
|
||||
/unsafe transfer path/i,
|
||||
);
|
||||
assert.equal(
|
||||
joinTransferTargetPath(uncRoot, "nested/report.txt"),
|
||||
"\\\\server\\share\\Downloads\\folder\\nested\\report.txt",
|
||||
);
|
||||
|
||||
// Forward-slash UNC destinations stay POSIX-shaped for remote panes, but
|
||||
// transfer joins must still apply Windows relative-path guards so a
|
||||
// server-controlled `\` / ADS name cannot escape the selected folder.
|
||||
const forwardSlashUncRoot = "//server/share/Downloads/folder";
|
||||
for (const relativePath of [
|
||||
"..\\outside.txt",
|
||||
"safe.txt:alternate-stream",
|
||||
"nested\\escape.txt",
|
||||
"trailing. ",
|
||||
]) {
|
||||
assert.throws(
|
||||
() => joinTransferTargetPath(forwardSlashUncRoot, relativePath),
|
||||
/unsafe transfer path/i,
|
||||
relativePath,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
joinTransferTargetPath(forwardSlashUncRoot, "nested/report.txt"),
|
||||
"//server/share/Downloads/folder/nested/report.txt",
|
||||
);
|
||||
assert.equal(
|
||||
getParentPath("\\\\server\\share\\Downloads\\folder"),
|
||||
"\\\\server\\share\\Downloads",
|
||||
);
|
||||
assert.equal(
|
||||
getParentPath("\\\\server\\share"),
|
||||
"\\\\server\\share",
|
||||
);
|
||||
});
|
||||
|
||||
test("path bar keeps WSL UNC paths instead of forcing a Unix root", () => {
|
||||
const windowsPane = { acceptForwardSlashUnc: true } as const;
|
||||
assert.equal(
|
||||
normalizeSftpNavigationPath("\\\\wsl.localhost\\Ubuntu-22.04\\home\\aaa"),
|
||||
"\\\\wsl.localhost\\Ubuntu-22.04\\home\\aaa",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeSftpNavigationPath("//wsl.localhost/Ubuntu-22.04/home/aaa", windowsPane),
|
||||
"\\\\wsl.localhost\\Ubuntu-22.04\\home\\aaa",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeSftpNavigationPath("\\\\wsl$\\Ubuntu-22.04\\home\\aaa"),
|
||||
"\\\\wsl$\\Ubuntu-22.04\\home\\aaa",
|
||||
);
|
||||
assert.equal(normalizeSftpNavigationPath("C:"), "C:\\");
|
||||
assert.equal(normalizeSftpNavigationPath("c:/"), "C:\\");
|
||||
assert.equal(normalizeSftpNavigationPath("home/aaa"), "/home/aaa");
|
||||
assert.equal(normalizeSftpNavigationPath("/var/log"), "/var/log");
|
||||
assert.equal(normalizeSftpNavigationPath(" "), "/");
|
||||
});
|
||||
|
||||
test("path bar keeps POSIX double-slash paths outside Windows panes", () => {
|
||||
assert.equal(normalizeSftpNavigationPath("//srv/share"), "//srv/share");
|
||||
assert.equal(
|
||||
normalizeSftpNavigationPath("//srv/share/logs"),
|
||||
"//srv/share/logs",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeSftpNavigationPath("//wsl.localhost/Ubuntu-22.04/home/aaa"),
|
||||
"//wsl.localhost/Ubuntu-22.04/home/aaa",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeSftpNavigationPath("//srv/share\\logs"),
|
||||
"//srv/share\\logs",
|
||||
);
|
||||
});
|
||||
|
||||
test("getParentPath preserves POSIX double-slash prefixes", () => {
|
||||
assert.equal(getParentPath("//srv/share/logs"), "//srv/share");
|
||||
assert.equal(getParentPath("//srv/share"), "//srv");
|
||||
assert.equal(getParentPath("//srv"), "/");
|
||||
assert.equal(getParentPath("//"), "/");
|
||||
assert.equal(getParentPath("/srv/share/logs"), "/srv/share");
|
||||
});
|
||||
|
||||
test("POSIX paths with three or more leading slashes stay in the root namespace", () => {
|
||||
assert.equal(getParentPath("///srv/share/logs"), "/srv/share");
|
||||
assert.deepEqual(
|
||||
getSftpBreadcrumbSegments("///srv/share/logs"),
|
||||
{
|
||||
segments: [
|
||||
{ label: "srv", path: "/srv" },
|
||||
{ label: "share", path: "/srv/share" },
|
||||
{ label: "logs", path: "/srv/share/logs" },
|
||||
],
|
||||
isWindowsDrive: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("getFileName preserves backslashes in POSIX path components", () => {
|
||||
assert.equal(getFileName("//srv/share\\logs"), "share\\logs");
|
||||
assert.equal(getFileName("\\\\server\\share\\logs"), "logs");
|
||||
});
|
||||
|
||||
test("descendant checks use the path kind instead of literal backslashes", () => {
|
||||
assert.equal(isSftpDescendantPath("//srv/share\\logs/child", "//srv/share\\logs"), true);
|
||||
assert.equal(isSftpDescendantPath("//srv/share\\logs-old", "//srv/share\\logs"), false);
|
||||
assert.equal(isSftpDescendantPath("C:\\Users\\alice\\child", "C:\\Users\\alice"), true);
|
||||
});
|
||||
|
||||
test("Windows UNC share roots are treated as path roots", () => {
|
||||
assert.equal(isWindowsRoot("\\\\wsl.localhost\\Ubuntu-22.04"), true);
|
||||
assert.equal(isWindowsRoot("\\\\wsl.localhost\\Ubuntu-22.04\\"), true);
|
||||
assert.equal(isWindowsRoot("\\\\wsl.localhost\\Ubuntu-22.04\\home"), false);
|
||||
assert.equal(isWindowsRoot("C:\\"), true);
|
||||
assert.equal(isWindowsRoot("C:\\Users"), false);
|
||||
});
|
||||
|
||||
test("Windows panes treat forward-slash UNC share roots as roots for Up navigation", () => {
|
||||
const windowsPane = { acceptForwardSlashUnc: true } as const;
|
||||
assert.equal(isWindowsRoot("//server/share"), false);
|
||||
assert.equal(isWindowsRoot("//server/share", windowsPane), true);
|
||||
assert.equal(isWindowsRoot("//server/share/", windowsPane), true);
|
||||
assert.equal(isWindowsRoot("//server/share/logs", windowsPane), false);
|
||||
assert.equal(getParentPath("//server/share"), "//server");
|
||||
assert.equal(getParentPath("//server/share", windowsPane), "\\\\server\\share");
|
||||
assert.equal(
|
||||
getParentPath("//server/share/logs/app", windowsPane),
|
||||
"\\\\server\\share\\logs",
|
||||
);
|
||||
});
|
||||
|
||||
test("pane context normalizes bookmark and initialPath forward-slash UNC ingress", () => {
|
||||
assert.deepEqual(resolveSftpWindowsPathOptions("C:\\Users\\alice"), {
|
||||
acceptForwardSlashUnc: true,
|
||||
});
|
||||
assert.deepEqual(resolveSftpWindowsPathOptions("/home/alice"), {});
|
||||
assert.deepEqual(
|
||||
resolveSftpWindowsPathOptions("//server/share", "C:\\Users\\alice"),
|
||||
{ acceptForwardSlashUnc: true },
|
||||
);
|
||||
assert.equal(
|
||||
normalizeSftpPaneNavigationPath("//server/share", "C:\\Users\\alice"),
|
||||
"\\\\server\\share",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeSftpPaneNavigationPath("//server/share", "/home/alice"),
|
||||
"//server/share",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeSftpPaneNavigationPath("//wsl.localhost/Ubuntu-22.04/home", "\\\\wsl.localhost\\Ubuntu-22.04"),
|
||||
"\\\\wsl.localhost\\Ubuntu-22.04\\home",
|
||||
);
|
||||
});
|
||||
|
||||
test("pane navigation preserves trailing whitespace; path bar still trims", () => {
|
||||
assert.equal(
|
||||
normalizeSftpPaneNavigationPath("/home/alice/dir ", "/home/alice"),
|
||||
"/home/alice/dir ",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeSftpPaneNavigationPath("//server/share/logs ", "C:\\Users\\alice"),
|
||||
"\\\\server\\share\\logs ",
|
||||
);
|
||||
assert.equal(normalizeSftpNavigationPath("/home/alice/dir "), "/home/alice/dir");
|
||||
assert.equal(
|
||||
normalizeSftpNavigationPath("/home/alice/dir ", { trimWhitespace: false }),
|
||||
"/home/alice/dir ",
|
||||
);
|
||||
});
|
||||
|
||||
test("breadcrumb segments keep WSL UNC roots intact", () => {
|
||||
assert.deepEqual(
|
||||
getSftpBreadcrumbSegments("\\\\wsl.localhost\\Ubuntu-22.04\\home\\aaa"),
|
||||
{
|
||||
segments: [
|
||||
{ label: "\\\\wsl.localhost\\Ubuntu-22.04", path: "\\\\wsl.localhost\\Ubuntu-22.04" },
|
||||
{ label: "home", path: "\\\\wsl.localhost\\Ubuntu-22.04\\home" },
|
||||
{ label: "aaa", path: "\\\\wsl.localhost\\Ubuntu-22.04\\home\\aaa" },
|
||||
],
|
||||
isWindowsDrive: false,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
getSftpBreadcrumbSegments("C:\\Users\\alice"),
|
||||
{
|
||||
segments: [
|
||||
{ label: "C:", path: "C:\\" },
|
||||
{ label: "Users", path: "C:\\Users" },
|
||||
{ label: "alice", path: "C:\\Users\\alice" },
|
||||
],
|
||||
isWindowsDrive: true,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
getSftpBreadcrumbSegments("/home/aaa"),
|
||||
{
|
||||
segments: [
|
||||
{ label: "home", path: "/home" },
|
||||
{ label: "aaa", path: "/home/aaa" },
|
||||
],
|
||||
isWindowsDrive: false,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
getSftpBreadcrumbSegments("//srv/share/logs"),
|
||||
{
|
||||
segments: [
|
||||
{ label: "srv", path: "//srv" },
|
||||
{ label: "share", path: "//srv/share" },
|
||||
{ label: "logs", path: "//srv/share/logs" },
|
||||
],
|
||||
isWindowsDrive: false,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
getSftpBreadcrumbSegments("//srv/share\\logs"),
|
||||
{
|
||||
segments: [
|
||||
{ label: "srv", path: "//srv" },
|
||||
{ label: "share\\logs", path: "//srv/share\\logs" },
|
||||
],
|
||||
isWindowsDrive: false,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
getSftpBreadcrumbSegments("//wsl.localhost/Ubuntu-22.04/home/aaa", {
|
||||
acceptForwardSlashUnc: true,
|
||||
}),
|
||||
{
|
||||
segments: [
|
||||
{ label: "\\\\wsl.localhost\\Ubuntu-22.04", path: "\\\\wsl.localhost\\Ubuntu-22.04" },
|
||||
{ label: "home", path: "\\\\wsl.localhost\\Ubuntu-22.04\\home" },
|
||||
{ label: "aaa", path: "\\\\wsl.localhost\\Ubuntu-22.04\\home\\aaa" },
|
||||
],
|
||||
isWindowsDrive: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("concrete transfer target paths exclude temporary placeholders", () => {
|
||||
assert.equal(isConcreteTransferTargetPath({ targetPath: "/Users/alice/Downloads/report.pdf" }), true);
|
||||
assert.equal(isConcreteTransferTargetPath({ targetPath: "C:\\Users\\alice\\Downloads\\report.pdf" }), true);
|
||||
assert.equal(isConcreteTransferTargetPath({ targetPath: "(temp)" }), false);
|
||||
assert.equal(isConcreteTransferTargetPath({ targetPath: " " }), false);
|
||||
});
|
||||
|
||||
test("SFTP filter clears when entering a different directory", () => {
|
||||
assert.equal(shouldClearSftpFilterForPathChange("/srv", "/srv/app"), true);
|
||||
assert.equal(getSftpFilterAfterPathChange("/srv", "/srv/app", "log"), "");
|
||||
});
|
||||
|
||||
test("SFTP filter clears when navigating to the parent directory", () => {
|
||||
assert.equal(shouldClearSftpFilterForPathChange("/srv/app", getParentPath("/srv/app")), true);
|
||||
assert.equal(getSftpFilterAfterPathChange("/srv/app", getParentPath("/srv/app"), "dist"), "");
|
||||
});
|
||||
|
||||
test("SFTP filter stays when refreshing the same directory", () => {
|
||||
assert.equal(shouldClearSftpFilterForPathChange("/srv/app", "/srv/app"), false);
|
||||
assert.equal(shouldClearSftpFilterForPathChange("/srv/app", "/srv/app/"), false);
|
||||
assert.equal(shouldClearSftpFilterForPathChange("C:\\Users\\alice", "c:/Users/alice"), false);
|
||||
assert.equal(
|
||||
shouldClearSftpFilterForPathChange("\\\\Server\\share\\folder", "\\\\server\\share\\folder\\"),
|
||||
false,
|
||||
);
|
||||
assert.equal(getSftpFilterAfterPathChange("/srv/app", "/srv/app", "log"), "log");
|
||||
assert.equal(getSftpFilterAfterPathChange("/srv/app", "/srv/app/", "log"), "log");
|
||||
assert.equal(getSftpFilterAfterPathChange("C:\\Users\\alice", "c:/Users/alice", "docs"), "docs");
|
||||
});
|
||||
|
||||
test("SFTP filter restores when changed-directory navigation fails", () => {
|
||||
assert.equal(getSftpFilterAfterPathChangeError(true, "log", "typed-while-loading"), "log");
|
||||
});
|
||||
|
||||
test("SFTP filter preserves in-flight edits when same-directory refresh fails", () => {
|
||||
assert.equal(getSftpFilterAfterPathChangeError(false, "log", "typed-while-loading"), "typed-while-loading");
|
||||
});
|
||||
|
||||
test("getSftpPathRoot resolves the filesystem root for breadcrumb navigation", () => {
|
||||
assert.equal(getSftpPathRoot("/var/www"), "/");
|
||||
assert.equal(getSftpPathRoot("/"), "/");
|
||||
assert.equal(getSftpPathRoot("//srv/share/logs"), "/");
|
||||
assert.equal(getSftpPathRoot("C:\\Users\\alice"), "C:\\");
|
||||
assert.equal(getSftpPathRoot("D:/"), "D:\\");
|
||||
assert.equal(getSftpPathRoot("\\\\server\\share\\folder"), "\\\\server\\share");
|
||||
// Forward-slash //host/share stays POSIX unless UNC is explicitly accepted.
|
||||
assert.equal(getSftpPathRoot("//srv/share/logs"), "/");
|
||||
assert.equal(
|
||||
getSftpPathRoot("//srv/share/logs", { acceptForwardSlashUnc: true }),
|
||||
"\\\\srv\\share",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user