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
255 lines
8.0 KiB
TypeScript
255 lines
8.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
confirmSftpClipboardUpload,
|
|
createDropEntriesFromClipboardFiles,
|
|
getSftpClipboardSystemTextPaths,
|
|
getSupportedClipboardUploadFiles,
|
|
isSftpNativeClipboardPasteEnabled,
|
|
resolveSftpClipboardUploadTarget,
|
|
shouldLetNativePasteEventHandleSftpPaste,
|
|
shouldStartClipboardUploadConfirm,
|
|
type ClipboardLocalFile,
|
|
type SftpClipboardUploadRequest,
|
|
} from "./sftp/clipboardUpload.ts";
|
|
import type { SftpFileEntry } from "../types";
|
|
|
|
const file = (name: string, overrides: Partial<SftpFileEntry> = {}): SftpFileEntry => ({
|
|
name,
|
|
type: "file",
|
|
size: 1,
|
|
modified: new Date(0),
|
|
permissions: "-rw-r--r--",
|
|
owner: "",
|
|
group: "",
|
|
...overrides,
|
|
});
|
|
|
|
test("clipboard upload targets the selected folder in the file list", () => {
|
|
const target = resolveSftpClipboardUploadTarget({
|
|
currentPath: "/home/app",
|
|
selectedFileNames: ["logs"],
|
|
files: [file("logs", { type: "directory" })],
|
|
treeSelection: [],
|
|
});
|
|
|
|
assert.equal(target, "/home/app/logs");
|
|
});
|
|
|
|
test("clipboard upload targets the current directory without a concrete folder selection", () => {
|
|
const target = resolveSftpClipboardUploadTarget({
|
|
currentPath: "/home/app",
|
|
selectedFileNames: [],
|
|
files: [file("logs", { type: "directory" })],
|
|
treeSelection: [],
|
|
});
|
|
|
|
assert.equal(target, "/home/app");
|
|
});
|
|
|
|
test("clipboard upload ignores selected regular files when resolving the target", () => {
|
|
const target = resolveSftpClipboardUploadTarget({
|
|
currentPath: "/home/app",
|
|
selectedFileNames: ["readme.md"],
|
|
files: [file("readme.md")],
|
|
treeSelection: [],
|
|
});
|
|
|
|
assert.equal(target, "/home/app");
|
|
});
|
|
|
|
test("clipboard upload targets the selected folder in the tree", () => {
|
|
const target = resolveSftpClipboardUploadTarget({
|
|
currentPath: "/home/app",
|
|
selectedFileNames: [],
|
|
files: [],
|
|
treeSelection: [{ name: "logs", path: "/var/logs", isDirectory: true }],
|
|
});
|
|
|
|
assert.equal(target, "/var/logs");
|
|
});
|
|
|
|
test("SFTP clipboard system text uses selected list paths", () => {
|
|
assert.deepEqual(
|
|
getSftpClipboardSystemTextPaths({
|
|
currentPath: "/home/app",
|
|
selectedFileNames: ["one.txt", "nested two.txt"],
|
|
treeSelection: [],
|
|
}),
|
|
["/home/app/one.txt", "/home/app/nested two.txt"],
|
|
);
|
|
});
|
|
|
|
test("SFTP clipboard system text uses selected tree paths", () => {
|
|
assert.deepEqual(
|
|
getSftpClipboardSystemTextPaths({
|
|
currentPath: "/home/app",
|
|
selectedFileNames: ["ignored.txt"],
|
|
treeSelection: [
|
|
{ name: "logs", path: "/var/logs", isDirectory: true },
|
|
{ name: "report.txt", path: "/var/report.txt", isDirectory: false },
|
|
],
|
|
}),
|
|
["/var/logs", "/var/report.txt"],
|
|
);
|
|
});
|
|
|
|
test("clipboard files become path-backed upload entries", () => {
|
|
const files: ClipboardLocalFile[] = [
|
|
{ path: "/Users/me/Desktop/report.txt", name: "report.txt", isDirectory: false, size: 42 },
|
|
];
|
|
|
|
assert.deepEqual(createDropEntriesFromClipboardFiles(files), [
|
|
{
|
|
file: null,
|
|
localPath: "/Users/me/Desktop/report.txt",
|
|
relativePath: "report.txt",
|
|
isDirectory: false,
|
|
size: 42,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("clipboard upload keeps directories for recursive folder paste", () => {
|
|
const files: ClipboardLocalFile[] = [
|
|
{ path: "/Users/me/Desktop/report.txt", name: "report.txt", isDirectory: false, size: 42 },
|
|
{ path: "/Users/me/Desktop/folder", name: "folder", isDirectory: true, size: 0 },
|
|
];
|
|
|
|
assert.deepEqual(getSupportedClipboardUploadFiles(files), files);
|
|
});
|
|
|
|
test("SFTP paste keydown lets the native paste event handle OS clipboard files", () => {
|
|
assert.equal(shouldLetNativePasteEventHandleSftpPaste("sftpPaste", "Ctrl + V"), true);
|
|
assert.equal(shouldLetNativePasteEventHandleSftpPaste("sftpPaste", "⌘ + V"), true);
|
|
assert.equal(shouldLetNativePasteEventHandleSftpPaste("sftpPaste", "Ctrl + Shift + V"), false);
|
|
assert.equal(shouldLetNativePasteEventHandleSftpPaste("sftpPaste", "Cmd + Shift + V"), false);
|
|
assert.equal(shouldLetNativePasteEventHandleSftpPaste("sftpPaste", "F9"), false);
|
|
assert.equal(shouldLetNativePasteEventHandleSftpPaste("sftpCopy", "Ctrl + V"), false);
|
|
});
|
|
|
|
test("clipboard upload confirmation clears the dialog before awaiting transfer", async () => {
|
|
const events: string[] = [];
|
|
let resolveUpload: (() => void) | undefined;
|
|
const uploadDone = new Promise<void>((resolve) => {
|
|
resolveUpload = resolve;
|
|
});
|
|
|
|
const request: SftpClipboardUploadRequest = {
|
|
scopeId: "pane-1",
|
|
side: "left",
|
|
targetPath: "/remote/inbox",
|
|
files: [{ path: "/tmp/a.txt", name: "a.txt", isDirectory: false, size: 1 }],
|
|
onConfirm: async () => {
|
|
events.push("upload-start");
|
|
await uploadDone;
|
|
events.push("upload-end");
|
|
},
|
|
};
|
|
|
|
const confirmPromise = confirmSftpClipboardUpload({
|
|
request,
|
|
clear: () => {
|
|
events.push("clear");
|
|
},
|
|
onUploaded: (targetPath) => {
|
|
events.push(`uploaded:${targetPath}`);
|
|
},
|
|
});
|
|
|
|
// Dialog must already be cleared while the transfer is still in flight.
|
|
// async function runs to its first await before returning the promise.
|
|
assert.deepEqual(events, ["clear", "upload-start"]);
|
|
|
|
resolveUpload?.();
|
|
await confirmPromise;
|
|
assert.deepEqual(events, ["clear", "upload-start", "upload-end", "uploaded:/remote/inbox"]);
|
|
});
|
|
|
|
test("clipboard upload confirmation skips onUploaded when transfer fails", async () => {
|
|
const events: string[] = [];
|
|
const request: SftpClipboardUploadRequest = {
|
|
scopeId: "pane-1",
|
|
side: "left",
|
|
targetPath: "/remote/inbox",
|
|
files: [{ path: "/tmp/a.txt", name: "a.txt", isDirectory: false, size: 1 }],
|
|
onConfirm: async () => {
|
|
events.push("upload-start");
|
|
throw new Error("boom");
|
|
},
|
|
};
|
|
|
|
await assert.rejects(
|
|
() => confirmSftpClipboardUpload({
|
|
request,
|
|
clear: () => {
|
|
events.push("clear");
|
|
},
|
|
onUploaded: () => {
|
|
events.push("uploaded");
|
|
},
|
|
}),
|
|
/boom/,
|
|
);
|
|
|
|
// Dialog is still cleared so the UI is not stuck; refresh callback is skipped.
|
|
assert.deepEqual(events, ["clear", "upload-start"]);
|
|
});
|
|
|
|
test("clipboard upload confirm guard is per request, not a global lock", () => {
|
|
const first: SftpClipboardUploadRequest = {
|
|
scopeId: "pane-1",
|
|
side: "left",
|
|
targetPath: "/a",
|
|
files: [{ path: "/tmp/a.txt", name: "a.txt", isDirectory: false, size: 1 }],
|
|
onConfirm: async () => {},
|
|
};
|
|
const second: SftpClipboardUploadRequest = {
|
|
scopeId: "pane-1",
|
|
side: "left",
|
|
targetPath: "/b",
|
|
files: [{ path: "/tmp/b.txt", name: "b.txt", isDirectory: false, size: 1 }],
|
|
onConfirm: async () => {},
|
|
};
|
|
|
|
assert.equal(shouldStartClipboardUploadConfirm(first, null), true);
|
|
assert.equal(shouldStartClipboardUploadConfirm(first, first), false);
|
|
// A later paste while the first transfer is still running must remain confirmable.
|
|
assert.equal(shouldStartClipboardUploadConfirm(second, first), true);
|
|
assert.equal(shouldStartClipboardUploadConfirm(null, first), false);
|
|
});
|
|
|
|
test("native clipboard paste follows SFTP paste shortcut availability", () => {
|
|
assert.equal(
|
|
isSftpNativeClipboardPasteEnabled("disabled", [
|
|
{ id: "sftp-paste", action: "sftpPaste", label: "Paste", mac: "⌘ + V", pc: "Ctrl + V", category: "sftp" },
|
|
]),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
isSftpNativeClipboardPasteEnabled("pc", [
|
|
{ id: "sftp-paste", action: "sftpPaste", label: "Paste", mac: "⌘ + V", pc: "Disabled", category: "sftp" },
|
|
]),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
isSftpNativeClipboardPasteEnabled("pc", [
|
|
{ id: "sftp-paste", action: "sftpPaste", label: "Paste", mac: "⌘ + V", pc: "F9", category: "sftp" },
|
|
]),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
isSftpNativeClipboardPasteEnabled("pc", [
|
|
{ id: "sftp-paste", action: "sftpPaste", label: "Paste", mac: "⌘ + V", pc: "Ctrl + Shift + V", category: "sftp" },
|
|
]),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
isSftpNativeClipboardPasteEnabled("pc", [
|
|
{ id: "sftp-paste", action: "sftpPaste", label: "Paste", mac: "⌘ + V", pc: "Ctrl + V", category: "sftp" },
|
|
]),
|
|
true,
|
|
);
|
|
});
|