Files
NetMesh/electron/bridges/transferBridge.test.cjs
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

13011 lines
448 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const crypto = require("node:crypto");
const Module = require("node:module");
const { spawnSync } = require("node:child_process");
const { EventEmitter } = require("node:events");
const { PassThrough, Readable, Writable } = require("node:stream");
const transferBridge = require("./transferBridge.cjs");
const sftpBridge = require("./sftpBridge.cjs");
const tempDirBridge = require("./tempDirBridge.cjs");
const {
DOWNLOAD_TRANSFER_CONCURRENCY,
TRANSFER_CHUNK_SIZE,
UPLOAD_TRANSFER_CONCURRENCY,
} = require("./transferLimits.cjs");
function createSender() {
return {
sent: [],
send(channel, payload) {
this.sent.push({ channel, payload });
},
};
}
async function waitUntil(predicate, timeoutMs = 1000) {
const deadline = Date.now() + timeoutMs;
while (!predicate() && Date.now() < deadline) {
await new Promise((resolve) => setImmediate(resolve));
}
return predicate();
}
function createFastSftp(overrides) {
const sftp = new EventEmitter();
sftp.readdir = (_path, callback) => callback(null, []);
sftp.stat = (_path, callback) => callback(null, { size: 1024 * 1024 });
sftp.lstat = (_path, callback) => {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
};
sftp.mkdir = (_path, callback) => callback(null);
sftp.unlink = (_path, callback) => callback(null);
sftp.end = () => {};
Object.assign(sftp, overrides);
return sftp;
}
/**
* Pipelined READ mock for bulk SFTP downloads. Bulk transfer no longer has a
* serial createReadStream path — tests must exercise open/read fanout.
*
* @param {Buffer | (() => Buffer)} payloadOrFactory
* @param {object} [overrides]
* @param {{ stall?: boolean }} [options]
* stall — hold READ callbacks in `pendingReads` until releasePending() is called
*/
function createPipelinedDownloadSftp(payloadOrFactory, overrides = {}, options = {}) {
const pendingReads = [];
const getPayload = typeof payloadOrFactory === "function"
? payloadOrFactory
: () => payloadOrFactory;
const allowFullFileHashStream = options.allowFullFileHashStream === true;
const sftp = createFastSftp({
open(_remotePath, flags, callback) {
if (typeof flags === "function") {
// Some call sites pass (path, callback) without flags.
flags(null, Buffer.from("read-handle"));
return;
}
callback(null, Buffer.from("read-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const payload = getPayload() || Buffer.alloc(0);
const end = Math.min(position + length, payload.length);
const slice = payload.subarray(position, end);
const deliver = () => {
slice.copy(buffer, offset);
callback(null, slice.length);
};
if (options.stall) {
pendingReads.push(deliver);
return;
}
setImmediate(deliver);
},
close(_handle, callback) {
callback(null);
},
// Hash / sample verification only (bounded start/end). Reject unbounded
// opens so a reintroduced bulk createReadStream body path cannot green
// success tests by streaming the full file.
createReadStream(_remotePath, streamOptions = {}) {
const start = streamOptions?.start;
const end = streamOptions?.end;
if (!allowFullFileHashStream && (!Number.isFinite(start) || !Number.isFinite(end))) {
throw new Error("bulk createReadStream must not be used for SFTP download body");
}
const payload = getPayload() || Buffer.alloc(0);
const from = Number.isFinite(start) ? start : 0;
const to = Number.isFinite(end) ? end : Math.max(0, payload.length - 1);
return Readable.from([Buffer.from(payload.subarray(from, to + 1))]);
},
...overrides,
});
return {
sftp,
pendingReads,
releasePending() {
for (const deliver of pendingReads.splice(0)) {
try { deliver(); } catch { /* ignore */ }
}
},
};
}
test("SFTP upload ignores cancellation after remote promotion is committed", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-sftp-commit-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "payload.bin");
const payload = Buffer.from("payload");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/payload.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old payload")]]);
let releaseRename;
let markRenameStarted;
const renameStarted = new Promise((resolve) => { markRenameStarted = resolve; });
let directRenameAttempted = false;
const fastSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
open(remotePath, _flags, callback) {
const key = String(remotePath);
remoteFiles.set(key, Buffer.alloc(payload.length));
callback(null, Buffer.from(key));
},
write(handle, buffer, offset, length, position, callback) {
const key = handle.toString();
buffer.copy(remoteFiles.get(key), position, offset, offset + length);
setImmediate(() => callback(null));
},
close(_handle, callback) {
callback(null);
},
});
const client = {
__netcattySudoMode: true,
sftp: fastSftp,
stat: async (remotePath) => {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length, isDirectory: false };
},
chmod: async () => {},
delete: async (remotePath) => { remoteFiles.delete(String(remotePath)); },
async rename(fromPath, toPath) {
const from = String(fromPath);
const to = String(toPath);
if (from.includes(".netcatty-upload-") && to === targetPath && !directRenameAttempted) {
directRenameAttempted = true;
throw new Error("atomic replace unavailable");
}
if (from.includes(".netcatty-upload-") && to === targetPath) {
markRenameStarted();
await new Promise((resolve) => { releaseRename = resolve; });
}
remoteFiles.set(to, remoteFiles.get(from));
remoteFiles.delete(from);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "sftp-commit-cancel";
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
await renameStarted;
await transferBridge.cancelTransfer(null, { transferId });
releaseRename();
const result = await running;
assert.equal(result.error, undefined);
assert.deepEqual(remoteFiles.get(targetPath), payload);
assert.equal([...remoteFiles.keys()].some((key) => key.includes(".netcatty-upload-")), false);
assert.equal([...remoteFiles.keys()].some((key) => key.includes(".netcatty-backup-")), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), false);
});
test("replace SFTP upload restores pre-existing remote mode after the final path is published", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-sftp-replace-mode-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "tool");
const payload = Buffer.from("#!/bin/sh\necho hi\n");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/usr/local/bin/tool";
const remoteFiles = new Map([[targetPath, Buffer.from("old-tool")]]);
const remoteMeta = new Map([[targetPath, { mode: 0o100755 }]]);
const chmodCalls = [];
const fastSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: remoteMeta.get(key)?.mode ?? 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
open(remotePath, _flags, callback) {
const key = String(remotePath);
remoteFiles.set(key, Buffer.alloc(payload.length));
if (!remoteMeta.has(key)) remoteMeta.set(key, { mode: 0o100644 });
callback(null, Buffer.from(key));
},
write(handle, buffer, offset, length, position, callback) {
const key = handle.toString();
buffer.copy(remoteFiles.get(key), position, offset, offset + length);
setImmediate(() => callback(null));
},
close(_handle, callback) {
callback(null);
},
});
const client = {
__netcattySudoMode: true,
sftp: fastSftp,
stat: async (remotePath) => {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return {
size: remoteFiles.get(key).length,
mode: remoteMeta.get(key)?.mode ?? 0o100644,
isDirectory: false,
};
},
chmod: async (remotePath, mode) => {
chmodCalls.push({ path: String(remotePath), mode });
const prev = remoteMeta.get(String(remotePath)) || {};
remoteMeta.set(String(remotePath), { ...prev, mode: (mode & 0o7777) | 0o100000 });
},
delete: async (remotePath) => {
remoteFiles.delete(String(remotePath));
remoteMeta.delete(String(remotePath));
},
async rename(fromPath, toPath) {
const from = String(fromPath);
const to = String(toPath);
remoteFiles.set(to, remoteFiles.get(from));
remoteFiles.delete(from);
// Simulate servers that recreate the destination inode on rename-replace
// with umask defaults, dropping the mode restored on the `.part` stage.
remoteMeta.set(to, { mode: 0o100644 });
remoteMeta.delete(from);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "sftp-replace-preserve-mode",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
assert.equal(result.error, undefined, result.error);
assert.deepEqual(remoteFiles.get(targetPath), payload);
assert.ok(
chmodCalls.some((call) => call.path === targetPath && (call.mode & 0o7777) === 0o755),
`expected chmod of ${targetPath} to 0755 after replace, got ${JSON.stringify(chmodCalls)}`,
);
assert.equal(remoteMeta.get(targetPath)?.mode & 0o777, 0o755);
});
test("new SFTP upload does not chmod a path that did not already exist", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-sftp-new-mode-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "fresh.bin");
const payload = Buffer.from("new-bytes");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/fresh.bin";
const remoteFiles = new Map();
const remoteMeta = new Map();
const chmodCalls = [];
const fastSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: remoteMeta.get(key)?.mode ?? 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
open(remotePath, _flags, callback) {
const key = String(remotePath);
remoteFiles.set(key, Buffer.alloc(payload.length));
if (!remoteMeta.has(key)) remoteMeta.set(key, { mode: 0o100644 });
callback(null, Buffer.from(key));
},
write(handle, buffer, offset, length, position, callback) {
const key = handle.toString();
buffer.copy(remoteFiles.get(key), position, offset, offset + length);
setImmediate(() => callback(null));
},
close(_handle, callback) {
callback(null);
},
});
const client = {
__netcattySudoMode: true,
sftp: fastSftp,
stat: async (remotePath) => {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return {
size: remoteFiles.get(key).length,
mode: remoteMeta.get(key)?.mode ?? 0o100644,
isDirectory: false,
};
},
chmod: async (remotePath, mode) => {
chmodCalls.push({ path: String(remotePath), mode });
},
delete: async (remotePath) => {
remoteFiles.delete(String(remotePath));
remoteMeta.delete(String(remotePath));
},
async rename(fromPath, toPath) {
const from = String(fromPath);
const to = String(toPath);
remoteFiles.set(to, remoteFiles.get(from));
remoteFiles.delete(from);
remoteMeta.set(to, remoteMeta.get(from) || { mode: 0o100644 });
remoteMeta.delete(from);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "sftp-new-keeps-default-mode",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
assert.equal(result.error, undefined, result.error);
assert.deepEqual(remoteFiles.get(targetPath), payload);
assert.deepEqual(chmodCalls, []);
});
test("replace SFTP upload still succeeds when restoring remote mode fails", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-sftp-replace-chmod-fail-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "tool");
const payload = Buffer.from("replaced-bytes");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/usr/local/bin/tool";
const remoteFiles = new Map([[targetPath, Buffer.from("old-tool")]]);
const remoteMeta = new Map([[targetPath, { mode: 0o100755 }]]);
const chmodCalls = [];
const warnings = [];
const originalWarn = console.warn;
console.warn = (...args) => { warnings.push(args.map(String).join(" ")); };
t.after(() => { console.warn = originalWarn; });
const fastSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: remoteMeta.get(key)?.mode ?? 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
open(remotePath, _flags, callback) {
const key = String(remotePath);
remoteFiles.set(key, Buffer.alloc(payload.length));
if (!remoteMeta.has(key)) remoteMeta.set(key, { mode: 0o100644 });
callback(null, Buffer.from(key));
},
write(handle, buffer, offset, length, position, callback) {
const key = handle.toString();
buffer.copy(remoteFiles.get(key), position, offset, offset + length);
setImmediate(() => callback(null));
},
close(_handle, callback) {
callback(null);
},
});
const client = {
__netcattySudoMode: true,
sftp: fastSftp,
stat: async (remotePath) => {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return {
size: remoteFiles.get(key).length,
mode: remoteMeta.get(key)?.mode ?? 0o100644,
isDirectory: false,
};
},
chmod: async (remotePath, mode) => {
chmodCalls.push({ path: String(remotePath), mode });
if (String(remotePath) === targetPath) {
throw new Error("chmod failed");
}
},
delete: async (remotePath) => {
remoteFiles.delete(String(remotePath));
remoteMeta.delete(String(remotePath));
},
async rename(fromPath, toPath) {
const from = String(fromPath);
const to = String(toPath);
remoteFiles.set(to, remoteFiles.get(from));
remoteFiles.delete(from);
remoteMeta.set(to, remoteMeta.get(from) || { mode: 0o100644 });
remoteMeta.delete(from);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "sftp-replace-chmod-fail",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
assert.equal(result.error, undefined, result.error);
assert.deepEqual(remoteFiles.get(targetPath), payload);
assert.ok(
chmodCalls.some((call) => call.path === targetPath && (call.mode & 0o7777) === 0o755),
`expected chmod of ${targetPath} to 0755, got ${JSON.stringify(chmodCalls)}`,
);
assert.ok(
warnings.some((message) => /Failed to restore permissions/.test(message)),
`expected chmod warning, got ${JSON.stringify(warnings)}`,
);
});
test("SCP upload ignores cancellation after remote promotion is committed", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-scp-commit-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "payload.bin");
const payload = Buffer.from("payload");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/payload.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old payload")]]);
let releaseRename;
let markRenameStarted;
const renameStarted = new Promise((resolve) => { markRenameStarted = resolve; });
const backend = {
async mkdir() {},
async stat(remotePath) {
if (!remoteFiles.has(remotePath)) {
const error = new Error("No such file");
error.code = "ENOENT";
throw error;
}
return { type: "file", isDirectory: false, size: remoteFiles.get(remotePath).length };
},
async uploadFile(localSourcePath, remotePath, options = {}) {
remoteFiles.set(remotePath, await fs.promises.readFile(localSourcePath));
options.onProgress?.(payload.length, payload.length);
},
async rename(fromPath, toPath) {
if (fromPath === targetPath) {
markRenameStarted();
await new Promise((resolve) => { releaseRename = resolve; });
}
remoteFiles.set(toPath, remoteFiles.get(fromPath));
remoteFiles.delete(fromPath);
},
async remove(remotePath) { remoteFiles.delete(remotePath); },
async chmod() {},
};
const client = {
__netcattyFileProtocol: "scp",
__netcattyScpBackend: backend,
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "scp-commit-cancel";
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
await renameStarted;
await transferBridge.cancelTransfer(null, { transferId });
releaseRename();
const result = await running;
assert.equal(result.error, undefined);
assert.deepEqual(remoteFiles.get(targetPath), payload);
assert.equal([...remoteFiles.keys()].some((key) => key.includes(".netcatty-upload-")), false);
assert.equal([...remoteFiles.keys()].some((key) => key.includes(".netcatty-backup-")), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), false);
});
test("SCP upload streams the live local path without a pre-hash digest", async (t) => {
// Industry consensus (FileZilla / WinSCP / OpenSSH): stream bytes immediately.
// Content digests are not a precondition for SCP body transfer.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-scp-direct-stream-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "payload.bin");
const payload = Buffer.alloc(16 * 1024, 57);
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/payload.bin";
const remoteFiles = new Map();
let uploadSourcePath = null;
const backend = {
async stat(remotePath) {
if (!remoteFiles.has(remotePath)) {
const error = new Error("No such file");
error.code = "ENOENT";
throw error;
}
return { type: "file", isDirectory: false, size: remoteFiles.get(remotePath).length };
},
async uploadFile(sourcePath, remotePath, options) {
uploadSourcePath = sourcePath;
// May still receive a plain file stream helper; must not require a digest.
if (typeof options.openReadStream === "function") {
const opened = options.openReadStream();
const chunks = [];
for await (const chunk of opened.stream) chunks.push(chunk);
remoteFiles.set(remotePath, Buffer.concat(chunks));
} else {
remoteFiles.set(remotePath, await fs.promises.readFile(sourcePath));
}
options.onProgress?.(payload.length, payload.length);
},
async remove() {},
async rename() {},
async chmod() {},
};
const client = {
__netcattyFileProtocol: "scp",
__netcattyScpBackend: backend,
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "scp-direct-stream",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
assert.equal(result.error, undefined);
assert.equal(uploadSourcePath, localPath);
// Staging may write to a .part path then rename; accept any staged body.
const uploaded = [...remoteFiles.values()].find((buf) => Buffer.isBuffer(buf) && buf.length === payload.length);
assert.ok(uploaded, "expected SCP body to be written to a remote path");
assert.deepEqual(uploaded, payload);
});
test("SCP staged streaming preserves executable mode without a local snapshot", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-scp-executable-mode-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "tool.sh");
const payload = Buffer.from("#!/bin/sh\necho ok\n");
await fs.promises.writeFile(localPath, payload);
await fs.promises.chmod(localPath, 0o755);
const targetPath = "/usr/local/bin/tool.sh";
const remoteFiles = new Map();
let uploadedMode = null;
let uploadedSourcePath = null;
const backend = {
async stat(remotePath) {
if (!remoteFiles.has(remotePath)) {
const error = new Error("No such file");
error.code = "ENOENT";
throw error;
}
return { type: "file", isDirectory: false, size: remoteFiles.get(remotePath).length };
},
async uploadFile(sourcePath, remotePath, options) {
uploadedSourcePath = sourcePath;
uploadedMode = (await fs.promises.stat(sourcePath)).mode & 0o777;
assert.equal(options.fileSize, payload.length);
const opened = options.openReadStream();
const chunks = [];
for await (const chunk of opened.stream) chunks.push(chunk);
remoteFiles.set(remotePath, Buffer.concat(chunks));
},
async rename(fromPath, toPath) {
remoteFiles.set(toPath, remoteFiles.get(fromPath));
remoteFiles.delete(fromPath);
},
async remove(remotePath) {
remoteFiles.delete(remotePath);
},
async chmod() {},
};
const client = {
__netcattyFileProtocol: "scp",
__netcattyScpBackend: backend,
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "scp-executable-mode",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
assert.equal(result.error, undefined);
assert.equal(uploadedSourcePath, localPath);
assert.equal(uploadedMode, 0o755);
assert.deepEqual(remoteFiles.get(targetPath), payload);
const digestId = crypto.createHash("sha256")
.update("scp-executable-mode")
.digest("hex")
.slice(0, 16);
const snapshotPath = tempDirBridge.getTransferTempFilePath(
`upload-source-${digestId}`,
"snapshot.bin",
);
assert.equal(fs.existsSync(snapshotPath), false);
});
test("SCP staged upload rechecks a deleted destination after mode setup", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-scp-mode-target-race-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "payload.bin");
const payload = Buffer.from("uploaded-data");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/payload.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old-state")]]);
const remoteMeta = new Map([[targetPath, { mode: 0o100755, modifyTime: 1 }]]);
let renameCalls = 0;
const backend = {
async stat(remotePath) {
if (!remoteFiles.has(remotePath)) {
const error = new Error("No such file");
error.code = "ENOENT";
throw error;
}
const meta = remoteMeta.get(remotePath) || { mode: 0o100644, modifyTime: 0 };
return {
type: "file",
isDirectory: false,
size: remoteFiles.get(remotePath).length,
mode: meta.mode,
permissions: "rwxr-xr-x",
modifyTime: meta.modifyTime,
};
},
async uploadFile(_sourcePath, remotePath, options) {
const opened = options.openReadStream();
const chunks = [];
for await (const chunk of opened.stream) chunks.push(chunk);
remoteFiles.set(remotePath, Buffer.concat(chunks));
remoteMeta.set(remotePath, { mode: 0o100644, modifyTime: 0 });
},
async chmod(remotePath) {
if (remotePath.includes(".netcatty-upload-")) {
remoteFiles.delete(targetPath);
remoteMeta.delete(targetPath);
}
},
async rename(fromPath, toPath) {
renameCalls += 1;
remoteFiles.set(toPath, remoteFiles.get(fromPath));
remoteFiles.delete(fromPath);
},
async remove(remotePath) {
remoteFiles.delete(remotePath);
remoteMeta.delete(remotePath);
},
};
const client = {
__netcattyFileProtocol: "scp",
__netcattyScpBackend: backend,
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "scp-mode-target-race",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
assert.match(result.error || "", /destination disappeared during upload/i);
assert.equal(renameCalls, 0);
assert.equal(remoteFiles.has(targetPath), false);
assert.equal([...remoteFiles.keys()].some((key) => key.includes(".netcatty-upload-")), false);
});
test("SCP staged upload does not promote when AbortSignal cancels mid-stream", async (t) => {
// Size-based path has no final digest re-scan. Cancel during the body stream
// must still prevent rename to the final target.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-scp-midstream-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "payload.bin");
const payload = Buffer.alloc(16 * 1024, 91);
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/payload.bin";
const remoteFiles = new Map();
let renameCalls = 0;
let releaseBodyRead;
let markBodyStarted;
const bodyStarted = new Promise((resolve) => { markBodyStarted = resolve; });
const backend = {
async stat(remotePath) {
if (!remoteFiles.has(remotePath)) {
const error = new Error("No such file");
error.code = "ENOENT";
throw error;
}
return { type: "file", isDirectory: false, size: remoteFiles.get(remotePath).length };
},
async uploadFile(_sourcePath, remotePath, options) {
const opened = options.openReadStream();
const chunks = [];
let first = true;
for await (const chunk of opened.stream) {
if (first) {
first = false;
markBodyStarted();
await new Promise((resolve) => { releaseBodyRead = resolve; });
}
chunks.push(chunk);
}
remoteFiles.set(remotePath, Buffer.concat(chunks));
},
async rename(fromPath, toPath) {
renameCalls += 1;
remoteFiles.set(toPath, remoteFiles.get(fromPath));
remoteFiles.delete(fromPath);
},
async remove(remotePath) {
remoteFiles.delete(remotePath);
},
async chmod() {},
};
const client = {
__netcattyFileProtocol: "scp",
__netcattyScpBackend: backend,
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const controller = new AbortController();
const transferId = "scp-midstream-cancel";
const running = transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
abortSignal: controller.signal,
});
await bodyStarted;
controller.abort();
releaseBodyRead();
const result = await running;
assert.match(result.error || "", /cancel/i);
assert.equal(renameCalls, 0);
assert.equal(remoteFiles.has(targetPath), false);
});
test("in-place upload ignores cancellation during final size verification", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-in-place-commit-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "payload.bin");
const payload = Buffer.from("payload");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/link.bin";
let remote = Buffer.from("old payload");
let statCalls = 0;
let releaseFinalStat;
let markFinalStatStarted;
const finalStatStarted = new Promise((resolve) => { markFinalStatStarted = resolve; });
const fastSftp = createFastSftp({
lstat(remotePath, callback) {
if (String(remotePath).includes(".netcatty-backup-")) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remote.length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
},
open(_remotePath, _flags, callback) {
remote = Buffer.alloc(payload.length);
callback(null, Buffer.from("remote-handle"));
},
write(_handle, buffer, offset, length, position, callback) {
buffer.copy(remote, position, offset, offset + length);
setImmediate(() => callback(null));
},
close(_handle, callback) {
callback(null);
},
});
const client = {
__netcattySudoMode: true,
sftp: fastSftp,
async stat(remotePath) {
if (String(remotePath).includes(".netcatty-backup-")) {
const error = new Error("ENOENT");
error.code = "ENOENT";
throw error;
}
statCalls += 1;
// Hang on the post-upload size check (after OPEN replaced the payload),
// not on an earlier existence/mode probe.
if (remote.length === payload.length && !releaseFinalStat) {
markFinalStatStarted();
await new Promise((resolve) => { releaseFinalStat = resolve; });
}
return { size: remote.length };
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "in-place-commit-cancel";
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
await finalStatStarted;
await transferBridge.cancelTransfer(null, { transferId });
releaseFinalStat();
const result = await running;
assert.equal(result.error, undefined);
assert.deepEqual(remote, payload);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), false);
assert.equal(sender.sent.some((entry) => (
entry.channel === "netcatty:transfer:progress"
&& entry.payload.transferred === payload.length
&& entry.payload.totalBytes === payload.length
)), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), true);
});
test("in-place concurrent upload ignores cancellation while the remote handle closes", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-in-place-close-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "payload.bin");
const payload = Buffer.from("payload");
await fs.promises.writeFile(localPath, payload);
let remote = Buffer.from("old payload");
let releaseClose;
let markCloseStarted;
const closeStarted = new Promise((resolve) => { markCloseStarted = resolve; });
const fastSftp = createFastSftp({
lstat(remotePath, callback) {
if (String(remotePath).includes(".netcatty-backup-")) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remote.length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
},
open(_remotePath, _flags, callback) {
remote = Buffer.alloc(payload.length);
callback(null, Buffer.from("remote-handle"));
},
write(_handle, buffer, offset, length, position, callback) {
buffer.copy(remote, position, offset, offset + length);
setImmediate(() => callback(null));
},
close(_handle, callback) {
markCloseStarted();
releaseClose = () => callback(null);
},
});
const client = {
__netcattySudoMode: true,
sftp: fastSftp,
stat: async (remotePath) => {
if (String(remotePath).includes(".netcatty-backup-")) {
const error = new Error("ENOENT");
error.code = "ENOENT";
throw error;
}
return { size: remote.length };
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "in-place-close-cancel";
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: localPath,
targetPath: "/tmp/link.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
await closeStarted;
await transferBridge.cancelTransfer(null, { transferId });
releaseClose();
const result = await running;
assert.equal(result.error, undefined);
assert.deepEqual(remote, payload);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), true);
});
test("SFTP download cancellation settles while initial metadata is stalled", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-download-stat-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
let markStatStarted;
const statStarted = new Promise((resolve) => { markStatStarted = resolve; });
const client = {
sftp: createFastSftp({}),
stat() {
markStatStarted();
return new Promise(() => {});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const transferId = "download-stat-cancel";
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: "/tmp/source.bin",
targetPath: path.join(tempDir, "target.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
resumable: true,
});
await statStarted;
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => setTimeout(() => reject(new Error("cancel timed out")), 500)),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("SFTP download cancellation settles while source snapshot metadata is stalled", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-download-snapshot-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
let statCalls = 0;
let markSnapshotStarted;
const snapshotStarted = new Promise((resolve) => { markSnapshotStarted = resolve; });
const client = {
sftp: createFastSftp({}),
stat() {
statCalls += 1;
if (statCalls === 1) return Promise.resolve({ size: 1024 });
markSnapshotStarted();
return new Promise(() => {});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const transferId = "download-snapshot-cancel";
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: "/tmp/source.bin",
targetPath: path.join(tempDir, "target.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
resumable: true,
});
await snapshotStarted;
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => setTimeout(() => reject(new Error("cancel timed out")), 500)),
]);
assert.equal(statCalls, 2);
assert.match(result.error || "", /cancel/i);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("SFTP upload cancellation settles while remote directory setup is stalled", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-upload-dir-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "source.bin");
await fs.promises.writeFile(localPath, Buffer.from("payload"));
let releaseStat;
let statCalls = 0;
let markStatStarted;
const statStarted = new Promise((resolve) => { markStatStarted = resolve; });
const createdDirectories = [];
const stalledSftp = createFastSftp({
stat(_remotePath, callback) {
statCalls += 1;
if (statCalls > 1) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
releaseStat = () => {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
};
markStatStarted();
},
mkdir(remotePath, callback) {
createdDirectories.push(String(remotePath));
callback(null);
},
});
const client = {
sftp: stalledSftp,
};
const clients = new Map([["target", client]]);
sftpBridge.init({ sftpClients: clients, sessions: new Map(), electronModule: {} });
t.after(() => {
sftpBridge.init({ sftpClients: new Map(), sessions: new Map(), electronModule: {} });
});
transferBridge.init({ sftpClients: clients });
const sender = createSender();
const transferId = "upload-directory-cancel";
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: localPath,
targetPath: "/stalled/directory/target.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
resumable: false,
});
await statStarted;
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => setTimeout(() => reject(new Error("cancel timed out")), 500)),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
assert.deepEqual(createdDirectories, []);
releaseStat();
await new Promise((resolve) => setTimeout(resolve, 20));
assert.deepEqual(createdDirectories, []);
});
test("SFTP upload cancellation waits for an in-flight remote mkdir to settle", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-upload-mkdir-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "source.bin");
await fs.promises.writeFile(localPath, Buffer.from("payload"));
let releaseMkdir;
let markMkdirStarted;
const mkdirStarted = new Promise((resolve) => { markMkdirStarted = resolve; });
const createdDirectories = [];
const stalledSftp = createFastSftp({
stat(_remotePath, callback) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
},
mkdir(remotePath, callback) {
const directory = String(remotePath);
markMkdirStarted();
releaseMkdir = () => {
createdDirectories.push(directory);
callback(null);
};
},
});
const client = { sftp: stalledSftp };
const clients = new Map([["target", client]]);
sftpBridge.init({ sftpClients: clients, sessions: new Map(), electronModule: {} });
t.after(() => {
sftpBridge.init({ sftpClients: new Map(), sessions: new Map(), electronModule: {} });
});
transferBridge.init({ sftpClients: clients });
const sender = createSender();
const transferId = "upload-mkdir-cancel";
let settled = false;
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: localPath,
targetPath: "/stalled/directory/target.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
resumable: false,
}).finally(() => { settled = true; });
await mkdirStarted;
await transferBridge.cancelTransfer(null, { transferId });
await new Promise((resolve) => setTimeout(resolve, 20));
assert.equal(settled, false);
assert.deepEqual(createdDirectories, []);
releaseMkdir();
const result = await running;
assert.match(result.error || "", /cancel/i);
assert.deepEqual(createdDirectories, ["/stalled"]);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("cancelling one SFTP channel waiter does not cancel the shared reopen", async () => {
let openCalls = 0;
let releaseChannel;
const reopenedChannel = createFastSftp({});
const client = {
sftp: null,
client: {
sftp(callback) {
openCalls += 1;
releaseChannel = () => callback(null, reopenedChannel);
},
},
};
const firstController = new AbortController();
const secondController = new AbortController();
const firstWait = sftpBridge.requireSftpChannel(client, { signal: firstController.signal });
const secondWait = sftpBridge.requireSftpChannel(client, { signal: secondController.signal });
firstController.abort(new Error("first waiter cancelled"));
await assert.rejects(() => firstWait, /first waiter cancelled/i);
assert.equal(openCalls, 1);
releaseChannel();
assert.equal(await secondWait, reopenedChannel);
assert.equal(client.sftp, reopenedChannel);
assert.equal(openCalls, 1);
});
test("shared SFTP channel reopen preserves the requested timeout", async () => {
const client = {
sftp: null,
client: {
sftp() {},
},
};
const startedAt = Date.now();
await assert.rejects(
() => sftpBridge.requireSftpChannel(client, { timeoutMs: 10 }),
/session lost/i,
);
assert.ok(Date.now() - startedAt < 500);
assert.equal(client._reopeningPromise, null);
});
test("server-to-server resume cancellation settles while source identity verification is stalled", async (t) => {
const transferId = `server-prefix-cancel-${crypto.randomUUID()}`;
const sourcePath = "/source/payload.bin";
const tempPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(sourcePath));
await fs.promises.writeFile(tempPath, Buffer.from("abcd"));
t.after(async () => fs.promises.rm(tempPath, { force: true }));
let markPrefixStarted;
const prefixStarted = new Promise((resolve) => { markPrefixStarted = resolve; });
const sourceClient = {
sftp: createFastSftp({
createReadStream() {
markPrefixStarted();
return new PassThrough();
},
}),
};
const targetClient = { sftp: createFastSftp({}) };
transferBridge.init({
sftpClients: new Map([["source", sourceClient], ["target", targetClient]]),
});
const sender = createSender();
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath,
targetPath: "/target/payload.bin",
sourceType: "sftp",
targetType: "sftp",
sourceSftpId: "source",
targetSftpId: "target",
totalBytes: 8,
resumable: true,
sameHost: false,
resumeStage: "download",
checkpointBytes: 4,
downloadCheckpointBytes: 4,
sourceFingerprint: `sha256:${"0".repeat(64)}`,
});
await prefixStarted;
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => setTimeout(() => reject(new Error("cancel timed out")), 500)),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("resumable SFTP uploads use the configured per-file request concurrency", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-test-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
// Need more than one fanout wave so the concurrency cap is observable.
const fileSize = UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2;
const localPath = path.join(tempDir, "large.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(fileSize));
let activeWrites = 0;
let observedConcurrency = 0;
let observedChunkSize = 0;
const pendingWrites = [];
let holdWrites = true;
const fastSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "w");
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, _position, callback) {
activeWrites += 1;
observedConcurrency = Math.max(observedConcurrency, activeWrites);
observedChunkSize = Math.max(observedChunkSize, length);
if (holdWrites) {
pendingWrites.push(() => {
activeWrites -= 1;
callback(null);
});
return;
}
setImmediate(() => {
activeWrites -= 1;
callback(null);
});
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: fileSize });
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const running = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-large",
sourcePath: localPath,
targetPath: "/tmp/large.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
resumable: true,
},
);
const readyDeadline = Date.now() + 1000;
while (pendingWrites.length < UPLOAD_TRANSFER_CONCURRENCY && Date.now() < readyDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(pendingWrites.length, UPLOAD_TRANSFER_CONCURRENCY);
assert.equal(observedConcurrency, UPLOAD_TRANSFER_CONCURRENCY);
assert.equal(observedChunkSize, TRANSFER_CHUNK_SIZE);
holdWrites = false;
for (const complete of pendingWrites.splice(0)) complete();
const result = await running;
assert.equal(result.error, undefined);
});
test("fast resumable uploads pause only after in-flight ranges are durable", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-fast-upload-pause-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
// First in-flight wave must be smaller than the file so pause can land mid-transfer.
const payload = Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2, 13);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
const pendingWrites = [];
let holdWrites = true;
let durableBytes = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
const complete = () => {
durableBytes = Math.max(durableBytes, position + length);
callback(null);
};
if (holdWrites) pendingWrites.push(complete);
else setImmediate(complete);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: durableBytes });
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const running = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-fast-paused",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
const readyDeadline = Date.now() + 1000;
while (pendingWrites.length < UPLOAD_TRANSFER_CONCURRENCY && Date.now() < readyDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(pendingWrites.length, UPLOAD_TRANSFER_CONCURRENCY);
const pausing = transferBridge.pauseTransfer(null, { transferId: "upload-fast-paused" });
holdWrites = false;
for (const complete of pendingWrites.splice(0)) complete();
const paused = await pausing;
assert.equal(paused.success, true);
assert.equal(paused.checkpointBytes, UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE);
{
const resumed = await transferBridge.resumeTransfer(null, { transferId: "upload-fast-paused" });
assert.equal(resumed.success, true);
assert.ok(Number.isFinite(resumed.lifecycleEpoch));
}
assert.equal((await running).error, undefined);
assert.equal(durableBytes, payload.length);
});
test("pause soft-drains concurrent ranges but resume waits before truncating", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-pause-soft-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
// Several MB so concurrent fanout stays saturated while we hold writes.
const payload = Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 4, 65);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let holdWrites = true;
const pendingWrites = [];
let durableBytes = 0;
const truncateCalls = [];
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
const complete = () => {
durableBytes = Math.max(durableBytes, position + length);
callback(null);
};
if (holdWrites) pendingWrites.push({ position, complete });
else setImmediate(complete);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: durableBytes });
},
truncate(_remotePath, size) {
truncateCalls.push(size);
durableBytes = size;
return Promise.resolve();
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const running = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-soft-pause",
sourcePath: localPath,
targetPath: "/tmp/upload-soft.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
const readyDeadline = Date.now() + 1000;
while (pendingWrites.length < UPLOAD_TRANSFER_CONCURRENCY && Date.now() < readyDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.ok(pendingWrites.length >= 1, "expected in-flight concurrent writes");
const started = Date.now();
// Hold writes so active ranges never drain — soft-drain must still resolve.
const paused = await transferBridge.pauseTransfer(null, { transferId: "upload-soft-pause" });
const elapsed = Date.now() - started;
assert.equal(paused.success, true);
// Soft drain is PAUSE_RANGE_DRAIN_MS (~50ms); allow headroom without full drain.
assert.ok(elapsed < 1500, `soft pause took too long: ${elapsed}ms`);
const outOfOrderWrite = pendingWrites.pop();
assert.ok(outOfOrderWrite?.position > 0, "expected a range beyond the contiguous checkpoint");
outOfOrderWrite.complete();
await new Promise((resolve) => setImmediate(resolve));
let resumeSettled = false;
const resuming = transferBridge.resumeTransfer(null, { transferId: "upload-soft-pause" })
.then((result) => {
resumeSettled = true;
return result;
});
await new Promise((resolve) => setTimeout(resolve, 500));
const truncatedBeforeDrain = truncateCalls.length > 0;
const resumedBeforeDrain = resumeSettled;
holdWrites = false;
for (const { complete } of pendingWrites.splice(0)) complete();
const resumeResult = await resuming;
assert.equal(resumeResult.success, true);
assert.ok(Number.isFinite(resumeResult.lifecycleEpoch));
assert.equal((await running).error, undefined);
assert.equal(truncatedBeforeDrain, false, "must not truncate while range writes are active");
assert.equal(resumedBeforeDrain, false, "resume must wait for active range writes to settle");
assert.equal(durableBytes, payload.length);
});
for (const settleWrites of [true, false]) {
test(`a newer pause supersedes resume while concurrent writes are draining: settle=${settleWrites}`, async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-resume-repause-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
// Several MB so concurrent fanout stays saturated while we hold writes.
const payload = Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 4, 65);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let holdWrites = true;
const pendingWrites = [];
let durableBytes = 0;
const truncateCalls = [];
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
const complete = () => {
durableBytes = Math.max(durableBytes, position + length);
callback(null);
};
if (holdWrites) pendingWrites.push({ position, complete });
else setImmediate(complete);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: durableBytes });
},
truncate(_remotePath, size) {
truncateCalls.push(size);
durableBytes = size;
return Promise.resolve();
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const running = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-resume-repause",
sourcePath: localPath,
targetPath: "/tmp/upload-soft.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
const readyDeadline = Date.now() + 1000;
while (pendingWrites.length < UPLOAD_TRANSFER_CONCURRENCY && Date.now() < readyDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.ok(pendingWrites.length >= 1, "expected in-flight concurrent writes");
const started = Date.now();
// Hold writes so active ranges never drain — soft-drain must still resolve.
const paused = await transferBridge.pauseTransfer(null, { transferId: "upload-resume-repause" });
const elapsed = Date.now() - started;
assert.equal(paused.success, true);
// Soft drain is PAUSE_RANGE_DRAIN_MS (~50ms); allow headroom without full drain.
assert.ok(elapsed < 1500, `soft pause took too long: ${elapsed}ms`);
const outOfOrderWrite = pendingWrites.pop();
assert.ok(outOfOrderWrite?.position > 0, "expected a range beyond the contiguous checkpoint");
outOfOrderWrite.complete();
await new Promise((resolve) => setImmediate(resolve));
let resumeSettled = false;
const resuming = transferBridge.resumeTransfer(null, { transferId: "upload-resume-repause" })
.then((result) => {
resumeSettled = true;
return result;
});
await new Promise((resolve) => setTimeout(resolve, 500));
const truncatedBeforeDrain = truncateCalls.length > 0;
const resumedBeforeDrain = resumeSettled;
const newerPause = await transferBridge.pauseTransfer(null, { transferId: "upload-resume-repause" });
assert.equal(newerPause.success, true);
if (settleWrites) {
holdWrites = false;
for (const { complete } of pendingWrites.splice(0)) complete();
}
const resumeResult = await resuming;
holdWrites = false;
for (const { complete } of pendingWrites.splice(0)) complete();
await transferBridge.cancelTransfer(null, { transferId: "upload-resume-repause" });
await running;
assert.equal(resumeResult.success, false, "old resume must not restart writes after a newer pause");
if (settleWrites) assert.match(resumeResult.reason, /superseded.*pause/i);
assert.equal(resumeResult.superseded, true, "direct callers need a structured superseded result too");
assert.equal(resumeResult.supersededBy, "pause");
assert.equal(truncatedBeforeDrain, false);
assert.equal(resumedBeforeDrain, false);
});
}
test("cancelling resume during soft-drain does not truncate the staged file", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-resume-cancel-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2, 66);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let holdWrites = true;
const pendingWrites = [];
const truncateCalls = [];
let durableBytes = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
const complete = () => {
durableBytes = Math.max(durableBytes, position + length);
callback(null);
};
if (holdWrites) pendingWrites.push(complete);
else setImmediate(complete);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: durableBytes });
},
truncate(_remotePath, size) {
truncateCalls.push(size);
durableBytes = size;
return Promise.resolve();
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const transferId = "upload-resume-cancel";
const running = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload-resume-cancel.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
const readyDeadline = Date.now() + 1000;
while (pendingWrites.length < UPLOAD_TRANSFER_CONCURRENCY && Date.now() < readyDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(pendingWrites.length, UPLOAD_TRANSFER_CONCURRENCY);
assert.equal((await transferBridge.pauseTransfer(null, { transferId })).success, true);
const resuming = transferBridge.resumeTransfer(null, { transferId });
await transferBridge.cancelTransfer(null, { transferId });
holdWrites = false;
for (const complete of pendingWrites.splice(0)) complete();
const transferResult = await running;
const resumeResult = await resuming;
assert.match(transferResult.error || "", /cancelled/i);
assert.deepEqual(resumeResult, {
success: false,
reason: "Transfer is no longer active",
});
assert.deepEqual(truncateCalls, []);
});
test("resuming while a fast pause is pending settles the pause request", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-pause-resume-race-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(32 * 1024, 67);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let finishWrite = null;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
finishWrite = () => callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: payload.length });
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const running = transferBridge.startTransfer(
{ sender },
{
transferId: "pause-resume-race",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
const deadline = Date.now() + 1000;
while (!finishWrite && Date.now() < deadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(typeof finishWrite, "function");
const pausing = transferBridge.pauseTransfer(null, { transferId: "pause-resume-race" });
{
const resumed = await transferBridge.resumeTransfer(null, { transferId: "pause-resume-race" });
assert.equal(resumed.success, true);
assert.ok(Number.isFinite(resumed.lifecycleEpoch));
}
assert.deepEqual(await pausing, {
success: false,
reason: "Pause was superseded by resume",
superseded: true,
supersededBy: "resume",
});
finishWrite();
assert.equal((await running).error, undefined);
});
test("pause acknowledges quickly then publishes a full source identity", async (t) => {
// Pause acknowledgement must not wait for full-file SHA-256. The background
// identity is nevertheless a complete digest so a later resume cannot mix
// bytes from different source versions.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-late-pause-race-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2, 71);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
const pendingWrites = [];
let holdWrites = true;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
if (holdWrites) pendingWrites.push(callback);
else setImmediate(() => callback(null));
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: payload.length });
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
let running;
{
const sender = createSender();
running = transferBridge.startTransfer(
{ sender },
{
transferId: "late-pause-race",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
t.after(async () => {
await transferBridge.cancelTransfer(null, { transferId: "late-pause-race" });
await running?.catch(() => {});
transferBridge.clearPendingCancel("late-pause-race");
});
const writeDeadline = Date.now() + 1000;
while (pendingWrites.length < UPLOAD_TRANSFER_CONCURRENCY && Date.now() < writeDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(pendingWrites.length, UPLOAD_TRANSFER_CONCURRENCY);
const pauseStarted = Date.now();
const pausing = transferBridge.pauseTransfer(null, { transferId: "late-pause-race" });
holdWrites = false;
for (const callback of pendingWrites.splice(0)) callback(null);
const paused = await pausing;
const pauseElapsed = Date.now() - pauseStarted;
assert.equal(paused.success, true);
assert.ok(pauseElapsed < 1500, `pause blocked too long: ${pauseElapsed}ms`);
const fingerprintPublished = await waitUntil(() => sender.sent.some((entry) => (
entry.channel === "netcatty:transfer:progress"
&& /^sha256:[a-f0-9]{64}$/.test(entry.payload.sourceFingerprint || "")
)));
assert.equal(fingerprintPublished, true, "full pause identity must be published after pause acknowledgement");
const fingerprintEntry = sender.sent.findLast((entry) => (
entry.channel === "netcatty:transfer:progress"
&& /^sha256:[a-f0-9]{64}$/.test(entry.payload.sourceFingerprint || "")
));
assert.equal(
fingerprintEntry?.payload.sourceFingerprint,
`sha256:${crypto.createHash("sha256").update(payload).digest("hex")}`,
);
{
const resumed = await transferBridge.resumeTransfer(null, { transferId: "late-pause-race" });
assert.equal(resumed.success, true);
assert.ok(Number.isFinite(resumed.lifecycleEpoch));
}
}
assert.equal((await running).error, undefined);
});
test("remote restart verifies the complete source with bounded concurrent reads", async (t) => {
const tempDir = await fs.promises.mkdtemp(`${tempDirBridge.getTempFilePath("resume-window")}-`);
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const transferId = `resume-window-${crypto.randomUUID()}`;
const payload = Buffer.alloc(2 * 1024 * 1024 + 17);
for (let index = 0; index < payload.length; index += 1) payload[index] = index % 251;
const checkpoint = 512 * 1024;
const targetPath = path.join(tempDir, "target.bin");
const stagePath = tempDirBridge.getTransferTempFilePath(transferId, "target.bin");
await fs.promises.writeFile(stagePath, payload.subarray(0, checkpoint));
t.after(async () => { await fs.promises.unlink(stagePath).catch(() => {}); });
const sender = createSender();
let verificationReads = 0;
let maxVerificationReads = 0;
let verificationBytes = 0;
const { sftp } = createPipelinedDownloadSftp(payload, {
read(_handle, buffer, offset, length, position, callback) {
const verifying = sender.sent.at(-1)?.payload.phase === "verifying";
if (verifying) {
verificationReads += 1;
maxVerificationReads = Math.max(maxVerificationReads, verificationReads);
}
setTimeout(() => {
const bytes = Math.min(length, payload.length - position);
payload.copy(buffer, offset, position, position + bytes);
if (verifying) {
verificationReads -= 1;
verificationBytes += bytes;
}
callback(null, bytes);
}, 2);
},
});
const client = { sftp, stat: async () => ({ size: payload.length }) };
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer({ sender }, {
transferId, sourcePath: "/source.bin", targetPath,
sourceType: "sftp", targetType: "local", sourceSftpId: "source",
totalBytes: payload.length, resumable: true, checkpointBytes: checkpoint,
sourceFingerprint: `sha256:p${payload.length}:${crypto.createHash("sha256").update(payload).digest("hex")}`,
});
assert.equal(result.error, undefined, result.error);
assert.deepEqual(await fs.promises.readFile(targetPath), payload);
assert.ok(maxVerificationReads > 1, "large resume verification must not wait for one network read at a time");
assert.ok(maxVerificationReads <= DOWNLOAD_TRANSFER_CONCURRENCY);
assert.ok(verificationBytes >= payload.length, "verify every source byte, not a sample");
});
for (const scenario of ["read-error", "open-error", "changed-content", "cancelled", "timeout"]) {
test(`remote restart prefix compatibility: ${scenario}`, async (t) => {
const tempDir = await fs.promises.mkdtemp(`${tempDirBridge.getTempFilePath("resume-compat")}-`);
const transferId = `resume-compat-${crypto.randomUUID()}`;
const payload = Buffer.alloc(3 * TRANSFER_CHUNK_SIZE, 71);
const targetPath = path.join(tempDir, "target.bin");
const stagePath = tempDirBridge.getTransferTempFilePath(transferId, "target.bin");
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagePath, { force: true });
});
await fs.promises.writeFile(stagePath, payload);
let streamOpens = 0;
const protocolError = new Error(scenario === "cancelled" ? "Transfer cancelled" : `Range ${scenario}`);
protocolError.code = scenario === "cancelled" ? "ABORT_ERR" : scenario === "timeout" ? "SFTP_READ_TIMEOUT" : 4;
if (scenario === "timeout") protocolError.sftpRequestTimedOut = true;
const { sftp } = createPipelinedDownloadSftp(payload, {
open(_path, _flags, callback) {
if (scenario === "open-error") callback(protocolError);
else callback(null, Buffer.from("prefix-handle"));
},
read(_handle, _buffer, _offset, _length, _position, callback) {
setImmediate(() => callback(protocolError));
},
createReadStream(_path, options) {
streamOpens += 1;
assert.equal(options.start, 0);
assert.equal(options.end, payload.length - 1);
const current = Buffer.from(payload);
if (scenario === "changed-content") current[current.length - 1] ^= 1;
return Readable.from([current]);
},
});
transferBridge.init({ sftpClients: new Map([["source", {
sftp, stat: async () => ({ size: payload.length }),
}]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId, sourcePath: "/source.bin", targetPath,
sourceType: "sftp", targetType: "local", sourceSftpId: "source",
totalBytes: payload.length, resumable: true, checkpointBytes: payload.length,
sourceFingerprint: `sha256:p${payload.length}:${crypto.createHash("sha256").update(payload).digest("hex")}`,
});
if (scenario === "cancelled" || scenario === "timeout") {
assert.match(result.error || "", /cancelled|Range timeout/);
assert.equal(streamOpens, 0, "cancellation and timeouts must not start a fallback");
} else {
assert.ok(streamOpens > 0, "ordinary protocol errors must retain stream compatibility");
if (scenario === "changed-content") {
assert.match(result.error || "", /source.*changed|source.*match/i);
assert.equal(fs.existsSync(targetPath), false, "fallback must still reject changed content");
} else {
assert.equal(result.error, undefined, result.error);
assert.deepEqual(await fs.promises.readFile(targetPath), payload);
}
}
});
}
test("remote prefix timeout abandons its channel and ignores late replies", async () => {
const pending = [];
let ended = false;
let progress = 0;
const { sftp } = createPipelinedDownloadSftp(Buffer.alloc(2 * TRANSFER_CHUNK_SIZE), {
read(_handle, buffer, _offset, length, _position, callback) {
pending.push(() => { buffer.fill(0); callback(null, length); });
},
end() { ended = true; },
createReadStream() { throw new Error("must not fall back after a timeout"); },
});
const client = { sftp };
await assert.rejects(transferBridge._hashRemotePrefixForTests(
client, "source", "/source.bin", undefined, 2 * TRANSFER_CHUNK_SIZE,
{ sftpReadTimeoutMs: 20, onProgress: () => { progress += 1; } },
), (error) => error.code === "SFTP_READ_TIMEOUT" && error.noTransferFallback === true);
assert.equal(ended, true);
assert.equal(client.sftp, null);
assert.equal(pending.length, 2);
for (const reply of pending) reply();
await new Promise((resolve) => setImmediate(resolve));
assert.equal(progress, 0, "finished verification must ignore late window progress");
});
test("remote prefix watchdog observes every positive short READ", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout"] });
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 73);
let replies = 0;
const { sftp } = createPipelinedDownloadSftp(payload, {
read(_handle, buffer, offset, length, position, callback) {
setTimeout(() => {
const count = Math.min(1024, length);
payload.copy(buffer, offset, position, position + count);
replies += 1;
callback(null, count);
}, 5);
},
createReadStream() { throw new Error("responsive range reads must not fall back"); },
});
const hashing = transferBridge._hashRemotePrefixForTests(
{ sftp }, "source", "/source.bin", undefined, payload.length, { sftpReadTimeoutMs: 25 },
);
// Handle rejection immediately so advancing the mock clock can expose a bad
// watchdog without generating an unrelated unhandled-rejection failure.
const outcome = hashing.then((digest) => ({ digest }), (error) => ({ error }));
for (let index = 0; index < 40; index += 1) {
await new Promise((resolve) => setImmediate(resolve));
t.mock.timers.tick(5);
}
const result = await outcome;
assert.equal(result.error, undefined, result.error?.message);
assert.equal(replies, 32);
assert.equal(result.digest, crypto.createHash("sha256").update(payload).digest("hex"));
});
test("remote resume identity rejects a same-size source rewrite", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-modifytime-id-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(3 * TRANSFER_CHUNK_SIZE, 11);
for (let i = 0; i < payload.length; i += 1) payload[i] = i % 251;
const modifyTimeMs = 1_700_000_000_000;
const digest = crypto.createHash("sha256").update(payload).digest("hex");
const sourceFingerprint = `sha256:p${payload.length}:${digest}`;
const checkpoint = TRANSFER_CHUNK_SIZE;
const targetPath = path.join(tempDir, "target.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(
"download-modifytime-id-resume",
path.basename(targetPath),
);
await fs.promises.mkdir(path.dirname(stagedPath), { recursive: true });
await fs.promises.writeFile(stagedPath, payload.subarray(0, checkpoint));
// Same size, one byte past the saved prefix rewritten.
const rewritten = Buffer.from(payload);
rewritten[checkpoint + 4] = 90;
const { sftp } = createPipelinedDownloadSftp(rewritten);
const client = {
sftp,
stat() {
return Promise.resolve({ size: rewritten.length, modifyTime: modifyTimeMs });
},
client: { sftp(callback) { callback(new Error("isolated channel unavailable")); } },
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const restarted = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-modifytime-id-resume",
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
checkpointBytes: checkpoint,
sourceFingerprint,
},
);
assert.match(restarted.error || "", /source file has changed|saved content does not match/i);
});
test("legacy sampled identity restarts safely instead of resuming mixed content", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-meta-rewrite-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const fileSize = 512 * 1024;
const checkpoint = 400 * 1024;
// Between mid sample (~240272 KiB) and tail sample (~480512 KiB).
const rewriteAt = 300 * 1024;
const modifyTimeMs = 1_700_000_000_000;
const original = Buffer.alloc(fileSize, 17);
const rewritten = Buffer.from(original);
rewritten.fill(99, rewriteAt, rewriteAt + TRANSFER_CHUNK_SIZE);
const transferId = "meta-rewrite-resume";
const targetPath = path.join(tempDir, "target.bin");
const stagePath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
await fs.promises.mkdir(path.dirname(stagePath), { recursive: true });
await fs.promises.writeFile(stagePath, original.subarray(0, checkpoint));
const { sftp } = createPipelinedDownloadSftp(rewritten, {
// meta: fingerprints restart from zero; hash helpers may still sample.
createReadStream(_remotePath, options = {}) {
const start = Number.isFinite(options.start) ? options.start : 0;
const end = Number.isFinite(options.end) ? options.end : rewritten.length - 1;
return Readable.from(Buffer.from(rewritten.subarray(start, end + 1)));
},
});
const client = {
sftp,
stat() {
return Promise.resolve({ size: fileSize, modifyTime: modifyTimeMs });
},
client: { sftp(callback) { callback(new Error("isolated channel unavailable")); } },
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: fileSize,
resumable: true,
checkpointBytes: checkpoint,
sourceFingerprint: `meta:${fileSize}:${modifyTimeMs}`,
},
);
assert.equal(result.error, undefined, result.error);
assert.deepEqual(await fs.promises.readFile(targetPath), rewritten);
});
test("resume rejects a rewrite beyond the first 256 KiB of the saved prefix", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-full-prefix-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const transferId = `full-prefix-${crypto.randomUUID()}`;
const sourcePath = path.join(tempDir, "source.bin");
const targetPath = path.join(tempDir, "target.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, "target.bin");
const fileSize = 768 * 1024;
const checkpoint = 400 * 1024;
const original = Buffer.alloc(fileSize, 17);
const current = Buffer.from(original);
current.fill(99, 300 * 1024, 332 * 1024);
await fs.promises.writeFile(sourcePath, current);
await fs.promises.writeFile(stagedPath, original.subarray(0, checkpoint));
t.after(async () => { await fs.promises.unlink(stagedPath).catch(() => {}); });
transferBridge.init({ sftpClients: new Map() });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath,
targetPath,
sourceType: "local",
targetType: "local",
totalBytes: fileSize,
resumable: true,
checkpointBytes: checkpoint,
sourceFingerprint: `sha256:${crypto.createHash("sha256").update(current).digest("hex")}`,
});
assert.match(result.error || "", /saved content does not match/i);
assert.equal(fs.existsSync(targetPath), false);
});
test("local resume verification reports progress and destroys both readers on cancellation", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-verify-cancel-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const transferId = `verify-cancel-${crypto.randomUUID()}`;
const sourcePath = path.join(tempDir, "source.bin");
const targetPath = path.join(tempDir, "target.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, "target.bin");
const checkpoint = 512 * 1024;
const verifyBytes = checkpoint;
const payload = Buffer.alloc(checkpoint + 1, 31);
await fs.promises.writeFile(sourcePath, payload);
await fs.promises.writeFile(stagedPath, payload.subarray(0, checkpoint));
t.after(async () => { await fs.promises.unlink(stagedPath).catch(() => {}); });
const sourceFingerprint = `sha256:${crypto.createHash("sha256").update(payload).digest("hex")}`;
const originalCreateReadStream = fs.createReadStream;
const verificationStreams = [];
fs.createReadStream = (filePath, options = {}) => {
if (
(filePath === sourcePath || filePath === stagedPath)
&& options.start === 0
&& options.end === verifyBytes - 1
) {
const stream = new PassThrough();
verificationStreams.push(stream);
return stream;
}
return originalCreateReadStream(filePath, options);
};
t.after(() => { fs.createReadStream = originalCreateReadStream; });
transferBridge.init({ sftpClients: new Map() });
const sender = createSender();
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath,
targetPath,
sourceType: "local",
targetType: "local",
totalBytes: payload.length,
resumable: true,
checkpointBytes: checkpoint,
sourceFingerprint,
});
t.after(async () => {
await transferBridge.cancelTransfer(null, { transferId });
await running.catch(() => {});
transferBridge.clearPendingCancel(transferId);
});
assert.equal(await waitUntil(() => verificationStreams.length === 2), true);
await new Promise((resolve) => setTimeout(resolve, 220));
for (const stream of verificationStreams) stream.write(Buffer.alloc(256 * 1024, 31));
assert.equal(await waitUntil(() => sender.sent.some((entry) => (
entry.channel === "netcatty:transfer:progress"
&& entry.payload.phase === "verifying"
&& entry.payload.speed > 0
))), true, "resume verification progress must be visible");
const verifyingEvent = sender.sent.findLast((entry) => entry.payload?.phase === "verifying");
assert.equal(verifyingEvent?.payload.transferred, checkpoint);
assert.equal(verifyingEvent?.payload.checkpointBytes, checkpoint);
await transferBridge.cancelTransfer(null, { transferId });
const result = await running;
assert.match(result.error || "", /cancel/i);
assert.equal(verificationStreams.every((stream) => stream.destroyed), true);
});
test("remote resume verification destroys the SFTP reader on cancellation", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-remote-verify-cancel-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const transferId = `remote-verify-cancel-${crypto.randomUUID()}`;
const targetPath = path.join(tempDir, "target.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, "target.bin");
const checkpoint = 512 * 1024;
const verifyBytes = checkpoint;
const payload = Buffer.alloc(checkpoint + 1, 47);
await fs.promises.writeFile(stagedPath, payload.subarray(0, checkpoint));
t.after(async () => { await fs.promises.unlink(stagedPath).catch(() => {}); });
const sourceFingerprint = `sha256:${crypto.createHash("sha256").update(payload).digest("hex")}`;
let verificationStream;
const sftp = createFastSftp({
createReadStream(_remotePath, options = {}) {
const start = Number(options.start) || 0;
const end = Number.isFinite(options.end) ? options.end : payload.length - 1;
if (start === 0 && end === verifyBytes - 1) {
verificationStream = new PassThrough();
return verificationStream;
}
return Readable.from(payload.subarray(start, end + 1));
},
});
const client = {
sftp,
stat: async () => ({ size: payload.length }),
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
checkpointBytes: checkpoint,
sourceFingerprint,
});
t.after(async () => {
await transferBridge.cancelTransfer(null, { transferId });
await running.catch(() => {});
transferBridge.clearPendingCancel(transferId);
});
assert.equal(await waitUntil(() => verificationStream !== undefined), true);
await new Promise((resolve) => setTimeout(resolve, 220));
verificationStream.write(Buffer.alloc(256 * 1024, 47));
assert.equal(await waitUntil(() => sender.sent.some((entry) => (
entry.channel === "netcatty:transfer:progress"
&& entry.payload.phase === "verifying"
&& entry.payload.speed > 0
))), true);
await transferBridge.cancelTransfer(null, { transferId });
const result = await running;
assert.match(result.error || "", /cancel/i);
assert.equal(verificationStream.destroyed, true);
});
test("remote resume verification cancellation does not wait for SFTP channel reopen", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-remote-verify-reopen-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const transferId = `remote-verify-reopen-${crypto.randomUUID()}`;
const targetPath = path.join(tempDir, "target.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, "target.bin");
const checkpoint = 64 * 1024;
const totalBytes = checkpoint + 1;
await fs.promises.writeFile(stagedPath, Buffer.alloc(checkpoint, 19));
t.after(async () => { await fs.promises.unlink(stagedPath).catch(() => {}); });
let reopenStarted = false;
let finishReopen;
const client = {
sftp: null,
stat: async () => ({ size: totalBytes }),
client: {
sftp(callback) {
reopenStarted = true;
finishReopen = () => callback(null, createFastSftp({}));
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const running = transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes,
resumable: true,
checkpointBytes: checkpoint,
sourceFingerprint: `sha256:${"0".repeat(64)}`,
});
t.after(async () => {
await transferBridge.cancelTransfer(null, { transferId });
await running.catch(() => {});
transferBridge.clearPendingCancel(transferId);
});
assert.equal(await waitUntil(() => reopenStarted), true);
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => setTimeout(() => reject(new Error("cancel timed out")), 250)),
]);
assert.match(result.error || "", /cancel/i);
finishReopen?.();
assert.equal(await waitUntil(() => client._reopeningPromise === null), true);
});
test("failed resumable upload opens close their isolated channel", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-open-fail-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(32 * 1024, 23));
let endedChannels = 0;
let hadOpenErrorListener = false;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
hadOpenErrorListener = fastSftp.listenerCount("error") > 0;
const error = new Error("permission denied");
if (hadOpenErrorListener) fastSftp.emit("error", error);
callback(error);
},
end() {
endedChannels += 1;
},
});
const client = {
// Stream fallback also rejects so the transfer still fails after cleanup.
sftp: createFastSftp({
createWriteStream() {
const writeStream = new Writable({
write(_chunk, _encoding, callback) {
callback(new Error("permission denied"));
},
});
queueMicrotask(() => writeStream.destroy(new Error("permission denied")));
return writeStream;
},
}),
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-open-fail",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: 32 * 1024,
resumable: true,
},
);
assert.match(result.error || "", /permission denied/);
assert.equal(hadOpenErrorListener, true);
// concurrent-isolated + fastPut-isolated each open/end their own channel.
assert.ok(endedChannels >= 1, `expected isolated channels to end, got ${endedChannels}`);
});
test("SFTP upload does not create a local ranges.sha256 digest sidecar", async (t) => {
// Size-based resume only — no whole-file content digest sidecar (industry default).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-no-digest-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(64 * 1024, 71);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
const remoteFiles = new Map();
const fastSftp = createFastSftp({
open(remotePath, _flags, callback) {
remoteFiles.set(String(remotePath), Buffer.alloc(payload.length));
callback(null, Buffer.from(String(remotePath)));
},
write(handle, buffer, offset, length, position, callback) {
buffer.copy(remoteFiles.get(handle.toString()), position, offset, offset + length);
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat: async (remotePath) => ({ size: remoteFiles.get(String(remotePath))?.length || 0 }),
rename(sourcePath, destinationPath) {
remoteFiles.set(String(destinationPath), remoteFiles.get(String(sourcePath)));
remoteFiles.delete(String(sourcePath));
return Promise.resolve();
},
delete: async () => {},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const transferId = "upload-no-digest-sidecar";
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(result.error, undefined);
const digestId = crypto.createHash("sha256")
.update(transferId)
.digest("hex")
.slice(0, 16);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${digestId}`,
"ranges.sha256",
);
assert.equal(fs.existsSync(digestPath), false);
assert.deepEqual(remoteFiles.get("/tmp/upload.bin"), payload);
});
test("SFTP upload ignores leftover digest sidecars from older builds", async (t) => {
// Older builds wrote ranges.sha256 next to the transfer id. Size-based uploads
// must not require or recreate that sidecar.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-legacy-digest-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const transferId = "upload-legacy-digest";
const localPath = path.join(tempDir, "upload.bin");
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 41);
await fs.promises.writeFile(localPath, payload);
const digestId = crypto.createHash("sha256").update(transferId).digest("hex").slice(0, 16);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${digestId}`,
"ranges.sha256",
);
await fs.promises.mkdir(path.dirname(digestPath), { recursive: true });
await fs.promises.writeFile(digestPath, Buffer.alloc(32, 9));
const remoteFiles = new Map();
const fastSftp = createFastSftp({
open(remotePath, _flags, callback) {
remoteFiles.set(String(remotePath), Buffer.alloc(payload.length));
callback(null, Buffer.from(String(remotePath)));
},
write(handle, buffer, offset, length, position, callback) {
buffer.copy(remoteFiles.get(handle.toString()), position, offset, offset + length);
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
transferBridge.init({
sftpClients: new Map([["target", {
sftp: createFastSftp({}),
stat: async (p) => ({ size: remoteFiles.get(String(p))?.length || 0 }),
rename: async (src, dst) => {
remoteFiles.set(String(dst), remoteFiles.get(String(src)));
remoteFiles.delete(String(src));
},
delete: async () => {},
client: { sftp: (callback) => callback(null, fastSftp) },
}]]),
});
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
},
);
assert.equal(result.error, undefined);
// Legacy sidecar may still exist on disk; the upload must not depend on it.
assert.deepEqual(remoteFiles.get("/tmp/upload.bin"), payload);
});
test("digest baseline cancellation removes the sidecar before opening remote upload", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-digest-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const transferId = "upload-digest-cancel";
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2, 53));
const digestId = crypto.createHash("sha256").update(transferId).digest("hex").slice(0, 16);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${digestId}`,
"ranges.sha256",
);
t.after(async () => fs.promises.rm(digestPath, { force: true }));
let remoteChannelOpens = 0;
const client = {
sftp: createFastSftp({}),
delete: async () => {},
client: {
sftp(callback) {
remoteChannelOpens += 1;
callback(new Error("remote must not open"));
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const originalOpen = fs.promises.open;
let cancellationTriggered = false;
fs.promises.open = async (filePath, flags, ...args) => {
const handle = await originalOpen(filePath, flags, ...args);
if (String(filePath) !== localPath || flags !== "r" || cancellationTriggered) return handle;
return {
async read(...readArgs) {
cancellationTriggered = true;
await transferBridge.cancelTransfer(null, { transferId });
return handle.read(...readArgs);
},
close: () => handle.close(),
};
};
let result;
try {
result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2,
resumable: true,
},
);
} finally {
fs.promises.open = originalOpen;
}
// Local source open is hooked to cancel mid-read. Depending on whether the
// background baseline or the first upload range opens first, cancel may come
// from that hook or from the forced remote open failure.
assert.ok(
cancellationTriggered || /cancel|remote must not open/i.test(result.error || ""),
`expected cancel or remote-open failure, got error=${result.error} cancelled=${cancellationTriggered}`,
);
assert.ok(result.error, "transfer must fail closed");
});
test("failed local open for resumable upload still ends the isolated channel", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-local-open-fail-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
// Create the source so preflight can proceed, then delete it as soon as the
// isolated channel opens so concurrent isolated upload fails on local open.
const localPath = path.join(tempDir, "source.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(32 * 1024, 7));
let endedChannels = 0;
let remoteOpenAttempts = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
remoteOpenAttempts += 1;
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
callback(null);
},
close(_handle, callback) {
callback(null);
},
end() {
endedChannels += 1;
},
});
const client = {
sftp: createFastSftp({
createWriteStream() {
// Stream fallback after range-path failure — keep it error-safe.
const writeStream = new Writable({
write(_chunk, _encoding, callback) {
callback(new Error("stream fallback after missing local open"));
},
});
return writeStream;
},
}),
client: {
sftp(callback) {
fs.promises.rm(localPath, { force: true }).finally(() => {
callback(null, fastSftp);
});
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-local-open-fail",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: 32 * 1024,
resumable: true,
skipAdmission: true,
},
);
assert.ok(result.error, "expected transfer to fail when local source disappears");
// Critical: isolated channel must not leak when local open fails first.
assert.ok(endedChannels >= 1, `expected isolated channel end, got ${endedChannels}`);
assert.equal(remoteOpenAttempts, 0);
});
test("cancel during stalled resumable upload OPEN ends the isolated channel", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-open-stall-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(32 * 1024, 29));
let endedChannels = 0;
let releaseOpen = null;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
releaseOpen = () => callback(new Error("channel closed during open"));
},
end() {
endedChannels += 1;
// Simulate ssh2 failing the pending OPEN when the channel ends.
releaseOpen?.();
releaseOpen = null;
},
});
const client = {
sftp: createFastSftp({
createWriteStream() {
const writeStream = new Writable({
write(_chunk, _encoding, callback) {
callback(new Error("should not stream-fallback after cancel"));
},
});
return writeStream;
},
}),
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const running = transferBridge.startTransfer(
{ sender },
{
transferId: "upload-open-stall-cancel",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: 32 * 1024,
resumable: true,
skipAdmission: true,
},
);
const readyDeadline = Date.now() + 1000;
while (!releaseOpen && Date.now() < readyDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.ok(releaseOpen, "expected remote OPEN to stall");
await transferBridge.cancelTransfer(null, { transferId: "upload-open-stall-cancel" });
const result = await running;
assert.match(result.error || "", /cancel|closed/i);
assert.ok(endedChannels >= 1, `expected isolated channel end on cancel, got ${endedChannels}`);
assert.ok(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"));
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:error"), false);
});
test("resumable SFTP uploads fail closed when pipelined strategies fail (no serial stream)", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-fail-closed-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.from("complete fallback upload");
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let endedChannels = 0;
let createWriteStreamCalls = 0;
let deleteCalls = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(new Error("server rejected random-access writes"));
},
end() {
endedChannels += 1;
},
});
const client = {
sftp: createFastSftp({
open(_remotePath, _flags, callback) {
callback(new Error("server rejected random-access writes"));
},
createWriteStream() {
createWriteStreamCalls += 1;
throw new Error("serial WriteStream must not run");
},
}),
stat() {
return Promise.resolve({ size: 0 });
},
rename() {
return Promise.resolve();
},
delete() {
deleteCalls += 1;
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-fail-closed",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.match(result.error || "", /pipelined upload failed|rejected random-access/i);
assert.equal(createWriteStreamCalls, 0);
assert.ok(endedChannels >= 1, `expected isolated channels to end, got ${endedChannels}`);
assert.equal(deleteCalls, 0, "a resumable upload error must preserve its staged prefix");
});
test("resumable concurrent uploads reject a source that shrinks mid-transfer", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-source-change-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
// Size-based finish check (no whole-file digest). A same-size rewrite is not
// re-hashed; shrink/grow still fails promotion so the remote .part is cleaned.
const payload = Buffer.alloc(16 * 1024, 41);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let shrunk = false;
let promoted = false;
let stagedDeleted = false;
let remoteBytes = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
remoteBytes = Math.max(remoteBytes, position + length);
if (!shrunk && position === 0) {
shrunk = true;
fs.writeFileSync(localPath, Buffer.alloc(payload.length / 2, 42));
}
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: remoteBytes });
},
rename() {
promoted = true;
return Promise.resolve();
},
delete() {
stagedDeleted = true;
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-source-change",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(shrunk, true);
assert.match(result.error || "", /source|size|changed/i);
assert.equal(promoted, false);
assert.equal(stagedDeleted, true);
});
test("assertSourceMetadataUnchanged ignores ctime drift when content is verified separately", () => {
const initial = {
size: 100,
mtimeMs: 1,
ctimeMs: 1,
mtime: 0.001,
ctime: 0.001,
ino: 42,
};
const drifted = {
...initial,
ctimeMs: 999,
ctime: 0.999,
};
// Without a separate content proof, timestamp drift is a hard fail (download path).
assert.throws(
() => transferBridge._assertSourceMetadataUnchangedForTests(initial, drifted, 100),
/source content changed/i,
);
// Upload finish soft path: ignore ctime noise but still enforce mtime/ino.
assert.doesNotThrow(() => transferBridge._assertSourceMetadataUnchangedForTests(
initial,
drifted,
100,
{ ignoreCtime: true },
));
assert.throws(
() => transferBridge._assertSourceMetadataUnchangedForTests(
initial,
{ ...drifted, mtimeMs: 2, mtime: 0.002 },
100,
{ ignoreCtime: true },
),
/source content changed/i,
);
// With digest / per-range verification, macOS xattr ctime bumps must not abort.
assert.doesNotThrow(() => transferBridge._assertSourceMetadataUnchangedForTests(
initial,
drifted,
100,
{ contentVerifiedSeparately: true },
));
// Size still fails hard even when content is verified separately.
assert.throws(
() => transferBridge._assertSourceMetadataUnchangedForTests(
initial,
{ ...drifted, size: 99 },
100,
{ contentVerifiedSeparately: true },
),
/source size changed/i,
);
// Append-only growth is rejected unless the download snapshot opts in.
assert.throws(
() => transferBridge._assertSourceMetadataUnchangedForTests(
initial,
{ ...initial, size: 150, mtimeMs: 2 },
100,
),
/source size changed/i,
);
// Growth without a separate prefix proof is still rejected (rewrite+grow risk).
assert.throws(
() => transferBridge._assertSourceMetadataUnchangedForTests(
initial,
{ ...initial, size: 150, mtimeMs: 2, ctimeMs: 2 },
100,
{ allowSourceGrowth: true },
),
/source content changed/i,
);
assert.doesNotThrow(() => transferBridge._assertSourceMetadataUnchangedForTests(
initial,
{ ...initial, size: 150, mtimeMs: 2, ctimeMs: 2 },
100,
{ allowSourceGrowth: true, contentVerifiedSeparately: true },
));
// Growth still cannot cover a shrink.
assert.throws(
() => transferBridge._assertSourceMetadataUnchangedForTests(
initial,
{ ...initial, size: 80 },
100,
{ allowSourceGrowth: true, contentVerifiedSeparately: true },
),
/source size changed/i,
);
});
test("resumable upload succeeds when only source ctime drifts (pause/resume false positive)", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-ctime-drift-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
// Reproduce the user-facing finish-path false positive: content bytes are
// stable (digest matches) but ctime moved — common on macOS after long
// pause/resume cycles (Spotlight / quarantine / xattr).
const payload = Buffer.alloc(16 * 1024, 43);
const localPath = path.join(tempDir, "ChatGPT.dmg");
await fs.promises.writeFile(localPath, payload);
const baseline = await fs.promises.stat(localPath);
let allowCtimeDrift = false;
const driftedStat = () => {
if (!allowCtimeDrift) return baseline;
return {
...baseline,
ctimeMs: baseline.ctimeMs + 60_000,
ctime: (baseline.ctimeMs + 60_000) / 1000,
// mtime intentionally stable — content not rewritten.
};
};
const realStat = fs.promises.stat.bind(fs.promises);
const realOpen = fs.promises.open.bind(fs.promises);
fs.promises.stat = async (p, ...args) => {
if (path.resolve(String(p)) === path.resolve(localPath)) return driftedStat();
return realStat(p, ...args);
};
fs.promises.open = async (p, flags, ...args) => {
const handle = await realOpen(p, flags, ...args);
if (path.resolve(String(p)) === path.resolve(localPath) && String(flags).includes("r")) {
handle.stat = async () => driftedStat();
}
return handle;
};
t.after(() => {
fs.promises.stat = realStat;
fs.promises.open = realOpen;
});
let remoteBytes = 0;
let promoted = false;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
remoteBytes = Math.max(remoteBytes, position + length);
// After the first remote WRITE, simulate metadata-only drift as if the
// transfer had been pause/resumed for a long wall-clock time.
allowCtimeDrift = true;
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: remoteBytes });
},
rename() {
promoted = true;
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-ctime-drift-ok",
sourcePath: localPath,
targetPath: "/tmp/ChatGPT.dmg",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(result.error, undefined, `expected success, got: ${result.error}`);
assert.equal(allowCtimeDrift, true);
assert.equal(promoted, true);
assert.equal(remoteBytes, payload.length);
});
test("non-resumable shared range uploads stream without a content digest", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-nonresume-stream-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(16 * 1024, 51);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let remoteBytes = 0;
const sharedSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
remoteBytes = Math.max(remoteBytes, position + length);
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat: async () => ({ size: remoteBytes }),
rename: async () => {},
delete: async () => {},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-nonresume-stream",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
},
);
assert.equal(result.error, undefined);
assert.equal(remoteBytes, payload.length);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${crypto.createHash("sha256").update("upload-nonresume-stream").digest("hex").slice(0, 16)}`,
"ranges.sha256",
);
assert.equal(fs.existsSync(digestPath), false);
});
test("non-resumable isolated upload streams without a content digest sidecar", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-isolated-stream-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(16 * 1024, 53);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload.bin";
const remoteFiles = new Map();
await fs.promises.writeFile(localPath, payload);
let writeCalls = 0;
let promoted = false;
const sharedSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
});
const fastSftp = createFastSftp({
open(remotePath, _flags, callback) {
const key = String(remotePath);
remoteFiles.set(key, Buffer.alloc(payload.length));
callback(null, Buffer.from(key));
},
write(handle, buffer, offset, length, position, callback) {
writeCalls += 1;
buffer.copy(remoteFiles.get(handle.toString()), position, offset, offset + length);
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: sharedSftp,
stat(remotePath) {
const value = remoteFiles.get(String(remotePath));
if (!value) {
const error = new Error("ENOENT");
error.code = 2;
return Promise.reject(error);
}
return Promise.resolve({ size: value.length });
},
rename(sourcePath, destinationPath) {
promoted = true;
remoteFiles.set(String(destinationPath), remoteFiles.get(String(sourcePath)));
remoteFiles.delete(String(sourcePath));
return Promise.resolve();
},
delete: async () => {},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-isolated-stream",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
},
);
assert.equal(result.error, undefined);
assert.ok(writeCalls > 0);
assert.equal(promoted, true);
});
test("fastPut streams the live local path without a content snapshot", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-fastput-live-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(16 * 1024, 55);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old target")]]);
await fs.promises.writeFile(localPath, payload);
let fastPutSourcePath = null;
const sharedSftp = createFastSftp({
lstat(remotePath, callback) {
const value = remoteFiles.get(String(remotePath));
if (!value) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: value.length,
mode: 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
});
const fastSftp = createFastSftp({
fastPut(sourcePath, remotePath, options, callback) {
fastPutSourcePath = sourcePath;
const uploaded = fs.readFileSync(sourcePath);
remoteFiles.set(String(remotePath), uploaded);
options.step?.(uploaded.length, uploaded.length, uploaded.length);
queueMicrotask(() => callback(null));
},
});
const client = {
sftp: sharedSftp,
stat: async (remotePath) => ({ size: remoteFiles.get(String(remotePath))?.length || 0 }),
chmod: async () => {},
rename(sourcePath, destinationPath) {
const source = String(sourcePath);
const destination = String(destinationPath);
remoteFiles.set(destination, remoteFiles.get(source));
remoteFiles.delete(source);
return Promise.resolve();
},
delete(remotePath) {
remoteFiles.delete(String(remotePath));
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-fastput-live",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
},
);
assert.equal(result.error, undefined);
assert.equal(fastPutSourcePath, localPath);
assert.deepEqual(remoteFiles.get(targetPath), payload);
});
test("failed local source open for non-resumable upload fails closed without digest sidecars", async (t) => {
// Size-based uploads no longer create ranges.sha256 / snapshot.bin. A missing
// or unreadable local source must still fail closed and leave no temp digest.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-local-open-fail-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const localPath = path.join(tempDir, "upload.bin");
const payload = Buffer.alloc(16 * 1024, 59);
await fs.promises.writeFile(localPath, payload);
const transferId = "upload-local-open-fail";
const digestId = crypto.createHash("sha256").update(transferId).digest("hex").slice(0, 16);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${digestId}`,
"ranges.sha256",
);
const snapshotPath = tempDirBridge.getTransferTempFilePath(
`upload-source-${digestId}`,
"snapshot.bin",
);
let fastPutCalls = 0;
let remoteOpens = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
remoteOpens += 1;
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
callback(null);
},
close(_handle, callback) {
callback(null);
},
fastPut(_sourcePath, _remotePath, _options, callback) {
fastPutCalls += 1;
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
delete: async () => {},
client: { sftp: (callback) => callback(null, fastSftp) },
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const realOpen = fs.promises.open.bind(fs.promises);
fs.promises.open = async (filePath, flags, ...args) => {
if (path.resolve(String(filePath)) === path.resolve(localPath) && String(flags).includes("r")) {
throw new Error("local source unreadable");
}
return realOpen(filePath, flags, ...args);
};
t.after(() => {
fs.promises.open = realOpen;
});
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
});
assert.match(result.error || "", /local source unreadable|ENOENT|not found|source/i);
assert.equal(fastPutCalls, 0);
assert.equal(fs.existsSync(digestPath), false);
assert.equal(fs.existsSync(snapshotPath), false);
});
test("non-resumable shared range uploads remove their temporary digest after success", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-nonresume-cleanup-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const transferId = "upload-nonresume-cleanup";
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE * 2, 61);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
const digestId = crypto.createHash("sha256").update(transferId).digest("hex").slice(0, 16);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${digestId}`,
"ranges.sha256",
);
let remoteBytes = 0;
const sharedSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
remoteBytes = Math.max(remoteBytes, position + length);
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
transferBridge.init({
sftpClients: new Map([["target", {
__netcattySudoMode: true,
sftp: sharedSftp,
stat: async () => ({ size: remoteBytes }),
rename: async () => {},
delete: async () => {},
}]]),
});
const sender = createSender();
const result = await transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
},
);
assert.equal(result.error, undefined);
assert.equal(fs.existsSync(digestPath), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), true);
});
test("non-resumable shared range uploads reject a source that grows and remove their digest", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-nonresume-growth-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const transferId = "upload-nonresume-growth";
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE * 2, 62);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
const digestId = crypto.createHash("sha256").update(transferId).digest("hex").slice(0, 16);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${digestId}`,
"ranges.sha256",
);
let sourceGrew = false;
let remoteBytes = 0;
const sharedSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
remoteBytes = Math.max(remoteBytes, position + length);
if (!sourceGrew) {
sourceGrew = true;
fs.appendFileSync(localPath, Buffer.from([1]));
}
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
transferBridge.init({
sftpClients: new Map([["target", {
__netcattySudoMode: true,
sftp: sharedSftp,
stat: async () => ({ size: remoteBytes }),
}]]),
});
const sender = createSender();
const result = await transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
},
);
assert.equal(sourceGrew, true);
assert.match(result.error || "", /source size changed/i);
assert.equal(fs.existsSync(digestPath), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("non-resumable digest creation cancellation removes the temporary digest", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-nonresume-digest-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const transferId = "upload-nonresume-digest-cancel";
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(
localPath,
Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2, 63),
);
const digestId = crypto.createHash("sha256").update(transferId).digest("hex").slice(0, 16);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${digestId}`,
"ranges.sha256",
);
let remoteOpenAttempts = 0;
const sharedSftp = createFastSftp({
open(_remotePath, _flags, callback) {
remoteOpenAttempts += 1;
callback(new Error("remote must not open"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
callback(new Error("remote must not write"));
},
});
transferBridge.init({
sftpClients: new Map([["target", {
__netcattySudoMode: true,
sftp: sharedSftp,
stat: async () => ({ size: 0 }),
}]]),
});
const originalOpen = fs.promises.open;
let cancellationTriggered = false;
let result;
try {
fs.promises.open = async (filePath, flags, ...args) => {
const handle = await originalOpen(filePath, flags, ...args);
if (String(filePath) !== localPath || flags !== "r" || cancellationTriggered) return handle;
return {
async read(...readArgs) {
cancellationTriggered = true;
await transferBridge.cancelTransfer(null, { transferId });
return handle.read(...readArgs);
},
stat: () => handle.stat(),
close: () => handle.close(),
};
};
result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2,
resumable: false,
},
);
} finally {
fs.promises.open = originalOpen;
}
assert.ok(
cancellationTriggered || /cancel|remote must not/i.test(result.error || ""),
`expected cancel path, got error=${result.error}`,
);
assert.ok(result.error, "transfer must fail closed");
// Remote OPEN may start while the local digest baseline is still scanning.
});
test("non-resumable shared range cancellation drains in-flight writes", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-nonresume-write-cancel-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const transferId = "upload-nonresume-write-cancel";
const payload = Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE * 2, 64);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
const pendingWrites = [];
const sharedSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
pendingWrites.push(callback);
},
close(_handle, callback) {
callback(null);
},
});
transferBridge.init({
sftpClients: new Map([["target", {
__netcattySudoMode: true,
sftp: sharedSftp,
stat: async () => ({ size: 0 }),
rename: async () => {},
delete: async () => {},
}]]),
});
const sender = createSender();
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
},
);
const readyDeadline = Date.now() + 2000;
while (
pendingWrites.length < UPLOAD_TRANSFER_CONCURRENCY
&& Date.now() < readyDeadline
) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(pendingWrites.length, UPLOAD_TRANSFER_CONCURRENCY);
let transferSettled = false;
void running.finally(() => {
transferSettled = true;
});
await transferBridge.cancelTransfer(null, { transferId });
await new Promise((resolve) => setImmediate(resolve));
assert.equal(transferSettled, false);
const firstWrite = pendingWrites.shift();
assert.ok(pendingWrites.length > 0);
firstWrite(new Error("write cancelled"));
await new Promise((resolve) => setImmediate(resolve));
assert.equal(transferSettled, false);
const finalWrite = pendingWrites.pop();
for (const callback of pendingWrites.splice(0)) callback(new Error("write cancelled"));
await new Promise((resolve) => setTimeout(resolve, 2200));
assert.equal(transferSettled, false);
finalWrite(new Error("write cancelled"));
const result = await running;
assert.match(result.error || "", /cancel|write cancelled/i);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), true);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:error"), false);
});
test("resumable fast uploads fail closed when isolated channel errors (no serial stream)", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-channel-error-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(32 * 1024, 31);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let hadErrorListener = false;
let createWriteStreamCalls = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
hadErrorListener = fastSftp.listenerCount("error") > 0;
const error = new Error("isolated channel failed");
queueMicrotask(() => {
if (hadErrorListener) fastSftp.emit("error", error);
callback(error);
});
},
close(_handle, callback) {
callback(null);
},
end() {},
});
const client = {
sftp: createFastSftp({
open(_remotePath, _flags, callback) {
callback(new Error("shared open also fails"));
},
createWriteStream() {
createWriteStreamCalls += 1;
throw new Error("serial WriteStream must not run");
},
}),
stat() {
return Promise.resolve({ size: 0 });
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-channel-error",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.match(result.error || "", /pipelined upload failed|isolated channel failed/i);
assert.equal(hadErrorListener, true);
assert.equal(createWriteStreamCalls, 0);
});
test("resumable concurrent range failure does not complete via serial stream", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-sparse-fail-closed-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(3 * 32 * 1024, 41);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let secondWriteCallback = null;
let thirdWriteCompleted = false;
let secondWriteFailureScheduled = false;
const scheduleSecondWriteFailure = () => {
if (secondWriteFailureScheduled || !thirdWriteCompleted || !secondWriteCallback) return;
secondWriteFailureScheduled = true;
const callback = secondWriteCallback;
secondWriteCallback = null;
queueMicrotask(() => callback(new Error("second range failed")));
};
let createWriteStreamCalls = 0;
let remoteBytes = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
if (position === 32 * 1024) {
secondWriteCallback = callback;
scheduleSecondWriteFailure();
return;
}
remoteBytes = Math.max(remoteBytes, position + length);
callback(null);
if (position === 2 * 32 * 1024) {
thirdWriteCompleted = true;
scheduleSecondWriteFailure();
}
},
close(_handle, callback) {
callback(null);
},
end() {},
});
const client = {
sftp: createFastSftp({
open(_remotePath, _flags, callback) {
callback(new Error("shared open also fails"));
},
createWriteStream() {
createWriteStreamCalls += 1;
throw new Error("serial WriteStream must not run");
},
}),
stat() {
return Promise.resolve({ size: remoteBytes });
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-sparse-fail-closed",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.match(result.error || "", /pipelined upload failed|second range failed/i);
assert.equal(createWriteStreamCalls, 0);
});
test("resumable fast uploads reject a source that grows during transfer", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-upload-source-growth-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(32 * 1024, 59);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let sourceGrew = false;
let promoted = false;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
if (!sourceGrew) {
sourceGrew = true;
fs.appendFileSync(localPath, Buffer.from([1]));
}
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: payload.length });
},
rename() {
promoted = true;
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-source-growth",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.match(result.error || "", /source size changed/);
assert.equal(promoted, false);
});
test("resumable uploads are size-based and do not require content digests", async (t) => {
// WinSCP/FileZilla resume model: checkpoint = durable stage size. Source
// rewrites mid-transfer are not blocked by a whole-file SHA baseline.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-size-resume-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(
(UPLOAD_TRANSFER_CONCURRENCY + 8) * TRANSFER_CHUNK_SIZE,
73,
);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let writeCalls = 0;
let promoted = false;
const remoteFiles = new Map();
const fastSftp = createFastSftp({
open(remotePath, _flags, callback) {
remoteFiles.set(String(remotePath), Buffer.alloc(payload.length));
callback(null, Buffer.from(String(remotePath)));
},
write(handle, buffer, offset, length, position, callback) {
writeCalls += 1;
buffer.copy(remoteFiles.get(handle.toString()), position, offset, offset + length);
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat(remotePath) {
return Promise.resolve({ size: remoteFiles.get(String(remotePath))?.length || 0 });
},
rename(sourcePath, destinationPath) {
promoted = true;
remoteFiles.set(String(destinationPath), remoteFiles.get(String(sourcePath)));
remoteFiles.delete(String(sourcePath));
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-size-based",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(result.error, undefined);
assert.ok(writeCalls > 0);
assert.equal(promoted, true);
const digestId = crypto.createHash("sha256")
.update("upload-size-based")
.digest("hex")
.slice(0, 16);
const digestPath = tempDirBridge.getTransferTempFilePath(
`upload-digest-${digestId}`,
"ranges.sha256",
);
assert.equal(fs.existsSync(digestPath), false);
});
test("resumable fast downloads clear staged data after a same-second source change", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-download-source-change-"));
const transferId = "download-source-change";
const targetPath = path.join(tempDir, "download.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagedPath, { force: true });
});
await fs.promises.writeFile(targetPath, "original");
const payload = Buffer.alloc(32 * 1024, 61);
const latestPayload = Buffer.alloc(payload.length, 62);
let reads = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const currentPayload = reads++ === 0 ? payload : latestPayload;
currentPayload.copy(buffer, offset, position, position + length);
callback(null, length, buffer, position);
},
close(_handle, callback) {
callback(new Error("remote close failed"));
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: payload.length, mtime: 1 });
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.match(result.error || "", /source.*changed/);
assert.equal(await fs.promises.readFile(targetPath, "utf8"), "original");
await assert.rejects(fs.promises.stat(stagedPath), { code: "ENOENT" });
});
test("growing-download prefix verification prefers a bounded remote digest", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-digest-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(128 * 1024, 71);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
const expectedDigest = crypto.createHash("sha256").update(payload).digest("hex");
let command = "";
const client = {
sftp: createFastSftp({
createReadStream() {
throw new Error("SFTP prefix stream should not run when remote digest is available");
},
}),
client: {
exec(request, callback) {
command = request;
const stream = new EventEmitter();
stream.stderr = new EventEmitter();
stream.destroy = () => {};
callback(null, stream);
setImmediate(() => {
stream.emit("data", Buffer.from(`${expectedDigest} -\n`));
stream.emit("close", 0);
});
},
},
};
await transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
},
);
assert.match(command, new RegExp(`head -c ${payload.length}`));
assert.match(command, /sha256sum|busybox|openssl/);
});
test("growing-download prefix verification skips unprivileged digest for sudo SFTP", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-sudo-digest-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(96 * 1024, 72);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
const emptyDigest = crypto.createHash("sha256").update("").digest("hex");
let execCalls = 0;
const pipelined = createPipelinedDownloadSftp(payload, {
createReadStream() {
throw new Error("serial prefix stream should remain the final fallback");
},
});
const client = {
__netcattySudoMode: true,
sftp: pipelined.sftp,
client: {
exec(_request, callback) {
execCalls += 1;
const stream = new EventEmitter();
stream.stderr = new EventEmitter();
stream.destroy = () => {};
callback(null, stream);
// Unprivileged head fails open into sha256sum and still exits 0.
setImmediate(() => {
stream.emit("data", Buffer.from(`${emptyDigest} -\n`));
stream.emit("close", 0);
});
},
},
};
await transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/root/secure.log",
signal: new AbortController().signal,
},
);
assert.equal(execCalls, 0, "sudo downloads must not use unprivileged digest commands");
});
test("growing-download prefix verification falls back after digest command timeout", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-digest-timeout-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(96 * 1024, 69);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
let rangeReads = 0;
const pipelined = createPipelinedDownloadSftp(payload, {
read(handle, buffer, offset, length, position, callback) {
rangeReads += 1;
const end = Math.min(position + length, payload.length);
const slice = payload.subarray(position, end);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
createReadStream() {
throw new Error("serial prefix stream should remain the final fallback");
},
});
const client = {
sftp: pipelined.sftp,
client: {
exec(_request, callback) {
const stream = new EventEmitter();
stream.stderr = new EventEmitter();
stream.destroy = () => {};
callback(null, stream);
// Open succeeds but never completes — hits SSH_EXEC_RUN_TIMEOUT.
},
},
};
await transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
sshDigestRunTimeoutMs: 30,
},
);
assert.ok(rangeReads > 0, "digest run timeout must fall through to SFTP range hashing");
});
test("growing-download prefix verification propagates digest open timeouts", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-digest-open-timeout-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(32 * 1024, 68);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
let rangeReads = 0;
const client = {
sftp: createFastSftp({
open(_remotePath, _flags, callback) {
rangeReads += 1;
callback(null, Buffer.from("should-not-open"));
},
read() {
throw new Error("SFTP fallback must not run after exec-open timeout");
},
createReadStream() {
throw new Error("SFTP stream fallback must not run after exec-open timeout");
},
}),
client: {
// Never invoke the exec callback — hits SSH_EXEC_OPEN_TIMEOUT and
// invalidates the transport in boundedSshExec.
exec() {},
},
};
await assert.rejects(
transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
sshDigestOpeningTimeoutMs: 30,
},
),
(error) => error?.code === "SSH_EXEC_OPEN_TIMEOUT" && error?.noTransferFallback === true,
);
assert.equal(rangeReads, 0, "open timeout must not fall through onto a poisoned session");
assert.equal(client.sftp, null, "exec-open timeout must drop the cached SFTP channel");
});
test("growing-download prefix verification rejects empty remote digests", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-empty-digest-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(64 * 1024, 70);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
const emptyDigest = crypto.createHash("sha256").update("").digest("hex");
let rangeReads = 0;
const pipelined = createPipelinedDownloadSftp(payload, {
read(handle, buffer, offset, length, position, callback) {
rangeReads += 1;
const end = Math.min(position + length, payload.length);
const slice = payload.subarray(position, end);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
createReadStream() {
throw new Error("serial prefix stream should remain the final fallback");
},
});
const client = {
sftp: pipelined.sftp,
client: {
exec(_request, callback) {
const stream = new EventEmitter();
stream.stderr = new EventEmitter();
stream.destroy = () => {};
callback(null, stream);
setImmediate(() => {
stream.emit("data", Buffer.from(`${emptyDigest} -\n`));
stream.emit("close", 0);
});
},
},
};
await transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
},
);
assert.ok(rangeReads > 0, "empty digest must fall through to elevated/SFTP hashing");
});
test("growing-download prefix verification falls back to parallel SFTP reads", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-ranges-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(256 * 1024, 73);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
const pipelined = createPipelinedDownloadSftp(payload, {
createReadStream() {
throw new Error("serial prefix stream should be the final fallback only");
},
});
const client = {
sftp: pipelined.sftp,
client: {
exec(_request, callback) {
const error = new Error("SSH exec unavailable");
error.code = "SSH_EXEC_UNAVAILABLE";
callback(error);
},
},
};
await transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
},
);
});
test("growing-download SFTP range hashing keeps a bounded read window", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-bound-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const chunkSize = TRANSFER_CHUNK_SIZE;
const concurrency = DOWNLOAD_TRANSFER_CONCURRENCY;
const payload = Buffer.alloc((concurrency + 8) * chunkSize, 77);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
let maxIndexStarted = -1;
let releaseFirstWindow;
const firstWindowBlocked = new Promise((resolve) => { releaseFirstWindow = resolve; });
let firstWindowGateOpened = false;
const pendingFirstWindow = [];
const warnings = [];
const onWarning = (warning) => {
warnings.push(String(warning?.name || warning?.message || warning));
};
process.on("warning", onWarning);
t.after(() => process.off("warning", onWarning));
const client = {
sftp: createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("bounded-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const index = Math.floor(position / chunkSize);
maxIndexStarted = Math.max(maxIndexStarted, index);
const end = Math.min(position + length, payload.length);
const slice = payload.subarray(position, end);
const deliver = () => {
slice.copy(buffer, offset);
callback(null, slice.length);
};
if (!firstWindowGateOpened && index < concurrency) {
pendingFirstWindow.push(deliver);
if (pendingFirstWindow.length === 1) {
firstWindowBlocked.then(() => {
firstWindowGateOpened = true;
for (const pending of pendingFirstWindow.splice(0)) pending();
});
}
return;
}
setImmediate(deliver);
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
throw new Error("serial prefix stream should not run for bounded-window test");
},
}),
client: {
exec(_request, callback) {
const error = new Error("SSH exec unavailable");
error.code = "SSH_EXEC_UNAVAILABLE";
callback(error);
},
},
};
const verifyPromise = transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
},
);
await waitUntil(() => pendingFirstWindow.length >= concurrency, 2000);
assert.equal(
maxIndexStarted,
concurrency - 1,
"later windows must wait until the current concurrency window finishes",
);
releaseFirstWindow();
await verifyPromise;
assert.ok(maxIndexStarted >= concurrency, "verification must continue past the first window");
assert.equal(
warnings.some((message) => /MaxListenersExceededWarning/i.test(message)),
false,
"shared abort gate must not attach one listener per concurrent READ",
);
});
test("growing-download SFTP range hashing resets inactivity on window progress", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-window-watchdog-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const chunkSize = TRANSFER_CHUNK_SIZE;
const payload = Buffer.alloc(3 * chunkSize, 79);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
const pendingReads = [];
let pumping = false;
const client = {
sftp: createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("watchdog-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const end = Math.min(position + length, payload.length);
const slice = payload.subarray(position, end);
pendingReads.push(() => {
slice.copy(buffer, offset);
callback(null, slice.length);
});
if (!pumping && pendingReads.length >= 3) {
pumping = true;
// Serialize completions so wall time exceeds the inactivity budget,
// but keep landing progress so a window watchdog stays armed.
void (async () => {
while (pendingReads.length > 0) {
await new Promise((resolve) => setTimeout(resolve, 40));
pendingReads.shift()?.();
}
})();
}
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
throw new Error("serial prefix stream should not hide window-watchdog behavior");
},
}),
client: {
exec(_request, callback) {
const error = new Error("SSH exec unavailable");
error.code = "SSH_EXEC_UNAVAILABLE";
callback(error);
},
},
};
await transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
// 80ms per-request deadlines would kill the 3rd serialized read (~120ms),
// but window inactivity resets on each completion.
sftpReadTimeoutMs: 80,
},
);
});
test("growing-download SFTP range hashing drops channel when CLOSE stalls", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-close-stall-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(64 * 1024, 81);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
const client = {
sftp: createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("close-stall-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const end = Math.min(position + length, payload.length);
const slice = payload.subarray(position, end);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
close() {
// Never invoke the callback — CLOSE watchdog must abandon the channel.
},
createReadStream() {
throw new Error("serial prefix stream should not run for CLOSE-stall test");
},
}),
client: {
exec(_request, callback) {
const error = new Error("SSH exec unavailable");
error.code = "SSH_EXEC_UNAVAILABLE";
callback(error);
},
},
};
await transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
sftpCloseTimeoutMs: 20,
},
);
assert.equal(client.sftp, null, "stalled CLOSE must drop the wedged SFTP channel");
});
test("growing-download prefix verification times out a stalled SFTP READ", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-timeout-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(64 * 1024, 74);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
const client = {
sftp: createFastSftp({
open(_path, _flags, callback) {
callback(null, Buffer.from("stalled-handle"));
},
read() {},
createReadStream() {
throw new Error("serial prefix stream should not hide a stalled range");
},
close(_handle, callback) {
callback(null);
},
}),
};
await assert.rejects(
transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
sftpReadTimeoutMs: 20,
},
),
(error) => (
/SFTP READ timed out/.test(error?.message || "")
&& error?.sftpRequestTimedOut === true
&& error?.noTransferFallback === true
),
);
assert.equal(client.sftp, null, "timed-out verification must drop the wedged SFTP channel");
});
test("growing-download prefix stream fallback times out without data", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-stream-timeout-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(32 * 1024, 75);
const localPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(localPath, payload);
const client = {
sftp: createFastSftp({
createReadStream() {
return new Readable({ read() {} });
},
}),
};
await assert.rejects(
transferBridge._assertDownloadSourceAfterTransferForTests(
{ size: payload.length, mtimeMs: 1 },
{ size: payload.length + 1, mtimeMs: 2 },
payload.length,
{
localPath,
client,
remotePath: "/var/log/app.log",
signal: new AbortController().signal,
sftpReadTimeoutMs: 20,
preferSftpRanges: false,
},
),
(error) => (
/SFTP stream timed out/.test(error?.message || "")
&& error?.sftpRequestTimedOut === true
&& error?.noTransferFallback === true
),
);
assert.equal(client.sftp, null, "stream timeout must drop the wedged SFTP channel");
});
test("resumable SFTP downloads succeed when the remote source only grows (live logs)", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-download-growth-"));
const transferId = "download-source-growth";
const targetPath = path.join(tempDir, "download.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagedPath, { force: true }).catch(() => {});
});
// Snapshot at transfer start; remote appends more bytes while ranges run.
const snapshot = Buffer.alloc(64 * 1024, 71);
const grownTail = Buffer.alloc(8 * 1024, 72);
let remoteSize = snapshot.length;
let mtimeMs = 1_000;
const remotePayload = () => Buffer.concat([snapshot, Buffer.alloc(Math.max(0, remoteSize - snapshot.length), 72)]);
const serveReadStream = (_remotePath, options = {}) => {
const current = remotePayload();
const start = Number.isFinite(options.start) ? options.start : 0;
const end = Number.isFinite(options.end) ? options.end : current.length - 1;
return Readable.from([Buffer.from(current.subarray(start, end + 1))]);
};
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
// First range completion simulates an append-only log writer.
if (remoteSize === snapshot.length) {
remoteSize = snapshot.length + grownTail.length;
mtimeMs += 1;
}
const current = remotePayload();
current.copy(buffer, offset, position, position + length);
callback(null, length, buffer, position);
},
close(_handle, callback) {
callback(null);
},
createReadStream: serveReadStream,
});
const client = {
// Shared session is used for post-transfer prefix verification on growth.
sftp: createFastSftp({ createReadStream: serveReadStream }),
stat() {
return Promise.resolve({
size: remoteSize,
mtimeMs,
ctimeMs: mtimeMs,
mtime: mtimeMs / 1000,
ctime: mtimeMs / 1000,
});
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/var/log/app.log",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: snapshot.length,
resumable: true,
},
);
assert.equal(result.error, undefined, result.error);
assert.equal(remoteSize, snapshot.length + grownTail.length);
const downloaded = await fs.promises.readFile(targetPath);
assert.equal(downloaded.length, snapshot.length);
assert.deepEqual(downloaded, snapshot);
await assert.rejects(fs.promises.stat(stagedPath), { code: "ENOENT" });
});
test("resumable SFTP downloads reject growth when the planned prefix was rewritten", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-download-rewrite-growth-"));
const transferId = "download-source-rewrite-growth";
const targetPath = path.join(tempDir, "download.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagedPath, { force: true }).catch(() => {});
});
// Mid-transfer rotate: prefix bytes change and the file also grows.
const snapshot = Buffer.alloc(64 * 1024, 71);
const rewritten = Buffer.alloc(64 * 1024, 81);
const grownTail = Buffer.alloc(8 * 1024, 72);
let useRewritten = false;
let remoteSize = snapshot.length;
let mtimeMs = 1_000;
const remotePayload = () => {
const head = useRewritten ? rewritten : snapshot;
if (remoteSize <= head.length) return Buffer.from(head.subarray(0, remoteSize));
return Buffer.concat([head, Buffer.alloc(remoteSize - head.length, 72)]);
};
const serveReadStream = (_remotePath, options = {}) => {
const current = remotePayload();
const start = Number.isFinite(options.start) ? options.start : 0;
const end = Number.isFinite(options.end) ? options.end : current.length - 1;
return Readable.from([Buffer.from(current.subarray(start, end + 1))]);
};
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
// After half the snapshot is staged from the original prefix, rotate the
// remote file in place and grow it so finish-path growth acceptance is
// the only remaining safety net if samples miss the hole.
if (!useRewritten && position >= snapshot.length / 2) {
useRewritten = true;
remoteSize = snapshot.length + grownTail.length;
mtimeMs += 1;
}
const current = remotePayload();
current.copy(buffer, offset, position, position + length);
callback(null, length, buffer, position);
},
close(_handle, callback) {
callback(null);
},
createReadStream: serveReadStream,
});
const client = {
sftp: createFastSftp({ createReadStream: serveReadStream }),
stat() {
return Promise.resolve({
size: remoteSize,
mtimeMs,
ctimeMs: mtimeMs,
mtime: mtimeMs / 1000,
ctime: mtimeMs / 1000,
});
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/var/log/app.log",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: snapshot.length,
resumable: true,
},
);
// Sample verify or full planned-prefix proof must reject the mixed file.
assert.match(result.error || "", /source|content|changed/i);
await assert.rejects(fs.promises.stat(stagedPath), { code: "ENOENT" });
if (fs.existsSync(targetPath)) {
assert.notEqual((await fs.promises.stat(targetPath)).size, snapshot.length);
}
});
test("checkpoint-complete resume verifies growth with bounded prefix ranges", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-download-complete-checkpoint-"));
const transferId = "download-complete-checkpoint-growth";
const targetPath = path.join(tempDir, "download.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagedPath, { force: true }).catch(() => {});
});
// Staged file already holds the full planned snapshot; remote has grown since.
const snapshot = Buffer.alloc(16 * 1024, 91);
const grownRemote = Buffer.concat([snapshot, Buffer.alloc(4 * 1024, 92)]);
await fs.promises.mkdir(path.dirname(stagedPath), { recursive: true });
await fs.promises.writeFile(stagedPath, snapshot);
// Prefix fingerprint keeps the checkpoint (meta: fingerprints are cleared
// and restart from zero before downloadFile runs). Matches the planned
// snapshot size so append growth on the remote does not fail identity.
const digest = crypto.createHash("sha256").update(snapshot).digest("hex");
const sourceFingerprint = `sha256:p${snapshot.length}:${digest}`;
let bodyOpenAtOldEof = false;
let openCalls = 0;
let verifyRangeReads = 0;
const sharedSftp = createFastSftp({
open(_remotePath, _flags, callback) {
openCalls += 1;
callback(null, Buffer.from("verify-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
if (position >= snapshot.length) {
bodyOpenAtOldEof = true;
throw new Error("must not READ past planned snapshot");
}
verifyRangeReads += 1;
const end = Math.min(position + length, snapshot.length);
snapshot.subarray(position, end).copy(buffer, offset);
setImmediate(() => callback(null, end - position));
},
close(_handle, callback) {
callback(null);
},
createReadStream(_remotePath, options = {}) {
const start = Number.isFinite(options.start) ? options.start : 0;
// Prefix verification may sample [0, snapshot); body open at EOF is the bug.
if (start >= snapshot.length) bodyOpenAtOldEof = true;
// Allow prefix hash reads for resume verification; reject transfer-body EOF opens.
if (start >= snapshot.length) {
throw new Error("must not open createReadStream at planned EOF");
}
return Readable.from([Buffer.from(grownRemote.subarray(start, (options.end ?? start) + 1))]);
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: grownRemote.length,
mtimeMs: 2_000,
ctimeMs: 2_000,
mtime: 2,
ctime: 2,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/var/log/app.log",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: snapshot.length,
resumable: true,
checkpointBytes: snapshot.length,
sourceFingerprint,
},
);
assert.equal(result.error, undefined, result.error);
assert.ok(verifyRangeReads > 0, "checkpoint-complete growth must use bounded prefix ranges");
assert.ok(openCalls > 0, "prefix verification may open a read handle at offset 0");
assert.equal(bodyOpenAtOldEof, false, "must not open transfer body at the old EOF");
const downloaded = await fs.promises.readFile(targetPath);
assert.deepEqual(downloaded, snapshot);
});
test("zero-byte snapshot is complete when remote has grown (no body open)", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-download-empty-growth-"));
const transferId = "download-empty-snapshot-growth";
const targetPath = path.join(tempDir, "empty.log");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagedPath, { force: true }).catch(() => {});
});
// Preflight sees an empty file; later stats report append growth. The body
// path must not open (unbounded read would pull the tail and fail).
const grownRemote = Buffer.from("appended-after-empty-snapshot");
let remoteSize = 0;
let bodyOpened = false;
const sharedSftp = createFastSftp({
open() {
bodyOpened = true;
throw new Error("must not OPEN body for zero-byte snapshot");
},
read() {
bodyOpened = true;
throw new Error("must not READ body for zero-byte snapshot");
},
createReadStream() {
bodyOpened = true;
throw new Error("must not createReadStream for zero-byte snapshot");
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: remoteSize,
mtimeMs: remoteSize > 0 ? 3_000 : 1_000,
ctimeMs: remoteSize > 0 ? 3_000 : 1_000,
mtime: remoteSize > 0 ? 3 : 1,
ctime: remoteSize > 0 ? 3 : 1,
});
},
};
// Grow after the planned empty snapshot is fixed (async gap before body I/O).
const growTimer = setTimeout(() => { remoteSize = grownRemote.length; }, 0);
t.after(() => clearTimeout(growTimer));
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/var/log/empty-then-grown.log",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 0,
resumable: true,
},
);
assert.equal(result.error, undefined, result.error);
assert.equal(bodyOpened, false, "zero-byte planned snapshot must not open a source body");
const downloaded = await fs.promises.readFile(targetPath);
assert.equal(downloaded.length, 0);
});
test("omitted totalBytes discovers remote size and downloads non-empty body", async (t) => {
// Explicit totalBytes:0 is an empty snapshot; omitting totalBytes must STAT
// and READ the remote (external-editor temp download path, #2787).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-omit-total-"));
const transferId = "download-omit-total-bytes";
const targetPath = path.join(tempDir, "report.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagedPath, { force: true }).catch(() => {});
});
const expected = Buffer.from("external-editor-contents");
let statCalls = 0;
let bodyOpened = false;
const { sftp: sharedSftp } = createPipelinedDownloadSftp(expected, {
open(_remotePath, flags, callback) {
bodyOpened = true;
if (typeof flags === "function") {
flags(null, Buffer.from("read-handle"));
return;
}
callback(null, Buffer.from("read-handle"));
},
});
const client = {
sftp: sharedSftp,
stat() {
statCalls += 1;
return Promise.resolve({
size: expected.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/remote/report.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
// intentionally omit totalBytes
},
);
assert.equal(result.error, undefined, result.error);
assert.ok(statCalls >= 1, "omitted totalBytes must discover remote size via STAT");
assert.equal(bodyOpened, true, "omitted totalBytes must open remote body for non-empty file");
assert.deepEqual(await fs.promises.readFile(targetPath), expected);
});
test("SFTP uploads fail when remote size does not match local size", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-size-test-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "archive.zip");
await fs.promises.writeFile(localPath, Buffer.alloc(1024 * 1024));
let deletedRemotePath = null;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
// Simulate a truncated remote file after a "successful" fastPut.
return Promise.resolve({ size: 512 * 1024 });
},
delete(remotePath) {
deletedRemotePath = remotePath;
return Promise.resolve();
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const result = await transferBridge.startTransfer(
{ sender },
{
transferId: "upload-truncated",
sourcePath: localPath,
targetPath: "/tmp/archive.zip",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
},
);
assert.match(result.error || "", /Upload size mismatch/);
assert.match(String(deletedRemotePath), /\.netcatty-upload-.*archive\.zip\.part$/);
assert.ok(sender.sent.some((entry) => entry.channel === "netcatty:transfer:error"));
});
test("uploads prefer concurrent shared channel over serial WriteStream", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-concurrent-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(UPLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE, 91);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
let maxInFlight = 0;
let activeWrites = 0;
let createWriteStreamCalls = 0;
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "w");
callback(null, Buffer.from("shared-handle"));
},
write(_handle, _buffer, _offset, length, _position, callback) {
activeWrites += 1;
maxInFlight = Math.max(maxInFlight, activeWrites);
setImmediate(() => {
activeWrites -= 1;
callback(null);
});
},
close(_handle, callback) {
callback(null);
},
createWriteStream() {
createWriteStreamCalls += 1;
throw new Error("serial WriteStream must not be used when concurrent works");
},
});
const client = {
// Force the isolated-channel path to be unavailable (sudo / no secondary channel).
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({ size: payload.length });
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-shared-concurrent",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(result.error, undefined);
assert.equal(createWriteStreamCalls, 0);
// Pipelined fanout must be multi-WRITE (not serial 1-in-flight).
assert.ok(
maxInFlight >= 2,
`expected pipelined concurrency >= 2, got ${maxInFlight}`,
);
assert.ok(
maxInFlight <= UPLOAD_TRANSFER_CONCURRENCY,
`expected concurrency <= ${UPLOAD_TRANSFER_CONCURRENCY}, got ${maxInFlight}`,
);
});
test("cancel during stalled shared upload OPEN drains before remote cleanup", async (t) => {
// Codex P2 on a7f96c94: cancel rejected shared write OPEN immediately, so
// runRemoteUploadTransaction cleaned up the stage before a late "w" OPEN
// recreated it — leaving an orphan .part (or truncating an in-place target).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-upload-open-drain-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 41));
const remoteFiles = new Map();
const eventLog = [];
let releaseOpen = null;
let endCalls = 0;
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
releaseOpen = () => {
remoteFiles.set(key, Buffer.alloc(0));
eventLog.push(`open-created:${key}`);
callback(null, Buffer.from(`handle:${key}`));
};
},
write() {
throw new Error("WRITE must not run after cancel during OPEN");
},
close(_handle, callback) {
eventLog.push("close");
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
return Promise.reject(error);
}
return Promise.resolve({ size: remoteFiles.get(key).length });
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
const key = String(remotePath);
eventLog.push(`delete:${key}`);
remoteFiles.delete(key);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-shared-open-drain-cancel";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const ready = await waitUntil(() => typeof releaseOpen === "function", 2000);
assert.ok(ready, "expected shared write OPEN to stall");
await transferBridge.cancelTransfer(null, { transferId });
// Must still be waiting for OPEN drain — immediate settle would be the bug.
let settledEarly = false;
await Promise.race([
running.then(() => { settledEarly = true; }),
new Promise((resolve) => setTimeout(resolve, 100)),
]);
assert.equal(settledEarly, false, "shared write OPEN cancel must drain before settle");
releaseOpen?.();
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("transfer did not settle after drained shared write OPEN")), 3000);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
assert.ok(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"));
const closeIdx = eventLog.indexOf("close");
const deleteIdx = eventLog.findIndex((entry) => entry.startsWith("delete:"));
assert.ok(closeIdx >= 0, `expected CLOSE before cleanup, log=${eventLog.join(",")}`);
assert.ok(deleteIdx >= 0, `expected stage delete after CLOSE, log=${eventLog.join(",")}`);
assert.ok(closeIdx < deleteIdx, `CLOSE must precede cleanup delete, log=${eventLog.join(",")}`);
assert.equal(
[...remoteFiles.keys()].some((key) => key.includes(".netcatty-upload-")),
false,
`expected no orphan staged upload, remaining=${[...remoteFiles.keys()].join(",")}`,
);
});
test("shared upload OPEN drain force-completes when cancel has no OPEN callback", async (t) => {
// Codex P2 on cd57d960: cancel settle without force-complete left
// sharedWriteOpenDrain pending forever when OPEN never arrived and no
// channel error fired, hanging the transfer and SFTP lease.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-upload-open-cancel-no-callback-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 51));
let endCalls = 0;
let openCalls = 0;
const sharedSftp = createFastSftp({
open(_remotePath, flags, _callback) {
assert.equal(flags, "w");
openCalls += 1;
// Never invoke the OPEN callback (stalled shared channel, no error).
},
write() {
throw new Error("WRITE must not run after cancel during OPEN");
},
close() {
throw new Error("CLOSE must not run without an OPEN handle");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
const error = new Error("ENOENT");
error.code = 2;
return Promise.reject(error);
},
rename() {
return Promise.resolve();
},
async delete() {},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-shared-open-cancel-no-callback";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload-cancel-hang.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const ready = await waitUntil(() => openCalls >= 1, 2000);
assert.ok(ready, "expected shared write OPEN to stall");
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer hung awaiting shared write OPEN drain after cancel")),
5500,
);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("shared upload cancel settle does not hang when best-effort unlink never callbacks", async (t) => {
// Codex P2 on 3f0f4608: cancel + OPEN "w" waited for unlink before finishCancel.
// A dead shared channel that never invokes unlink left the transfer/lease active.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-upload-unlink-hang-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 61));
let openCalls = 0;
let unlinkCalls = 0;
let endCalls = 0;
let openCallback = null;
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "w");
openCalls += 1;
openCallback = callback;
},
unlink(_remotePath, _callback) {
unlinkCalls += 1;
// Never invoke unlink callback (stalled shared channel).
},
write() {
throw new Error("WRITE must not run after cancel during OPEN");
},
close(_handle, callback) {
callback?.(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
const error = new Error("ENOENT");
error.code = 2;
return Promise.reject(error);
},
rename() {
return Promise.resolve();
},
async delete() {},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-shared-open-cancel-unlink-hang";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload-cancel-unlink-hang.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const ready = await waitUntil(() => openCalls >= 1 && openCallback, 2000);
assert.ok(ready, "expected shared write OPEN to stall with callback held");
await transferBridge.cancelTransfer(null, { transferId });
// OPEN returns after cancel — triggers close + best-effort unlink path.
openCallback(null, Buffer.from("handle:hang-unlink"));
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer hung awaiting best-effort unlink after cancel")),
5500,
);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.ok(unlinkCalls >= 1, "expected best-effort unlink after cancel OPEN");
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
});
test("shared upload OPEN drain survives cancel settle timeout", async (t) => {
// Codex P2 on af9cef2e / 0cda4a39 / cd57d960: cancel settle + drain
// force-complete must not hang, and a late truncating OPEN on a generated
// stage after that must close + unlink so stage cleanup cannot leave a
// recreate orphan.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-upload-open-drain-timeout-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 43));
const remoteFiles = new Map();
const eventLog = [];
let releaseOpen = null;
let endCalls = 0;
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
releaseOpen = () => {
remoteFiles.set(key, Buffer.alloc(0));
eventLog.push(`open-created:${key}`);
callback(null, Buffer.from(`handle:${key}`));
};
},
write() {
throw new Error("WRITE must not run after cancel during OPEN");
},
close(_handle, callback) {
eventLog.push("close");
callback(null);
},
unlink(remotePath, callback) {
const key = String(remotePath);
eventLog.push(`unlink:${key}`);
remoteFiles.delete(key);
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
return Promise.reject(error);
}
return Promise.resolve({ size: remoteFiles.get(key).length });
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
const key = String(remotePath);
eventLog.push(`delete:${key}`);
remoteFiles.delete(key);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-shared-open-drain-timeout";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload-timeout.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const ready = await waitUntil(() => typeof releaseOpen === "function", 2000);
assert.ok(ready, "expected shared write OPEN to stall");
await transferBridge.cancelTransfer(null, { transferId });
// Past cancel settle (2s) + drain force-complete (2s): transfer must settle
// without an OPEN callback so lease/admission cannot hang.
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("transfer hung after cancel OPEN drain force-complete")), 5500);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
// Late OPEN after force-complete — must close + unlink, no orphan stage.
releaseOpen?.();
const lateCleanup = await waitUntil(
() => eventLog.includes("close") && eventLog.some((entry) => entry.startsWith("unlink:")),
2000,
);
assert.ok(lateCleanup, `expected late OPEN close+unlink, log=${eventLog.join(",")}`);
assert.equal(
[...remoteFiles.keys()].some((key) => key.includes(".netcatty-upload-")),
false,
`expected no orphan staged upload, remaining=${[...remoteFiles.keys()].join(",")}`,
);
});
test("in-place shared upload cancel does not unlink final target after late OPEN", async (t) => {
// Codex P1 on a9f748c8: cancel+truncating "w" unlinked filePath unconditionally,
// so late OPEN on an in-place/symlink destination could delete the final target.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-inplace-open-cancel-nounlink-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 53));
const targetPath = "/tmp/inplace-link.bin";
const existingPayload = Buffer.from("keep-me");
const remoteFiles = new Map([[targetPath, Buffer.from(existingPayload)]]);
const eventLog = [];
let releaseOpen = null;
let endCalls = 0;
const sharedSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (key.includes(".netcatty-backup-") || key.includes(".netcatty-upload-")) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
},
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
assert.equal(key, targetPath, "in-place upload must OPEN the final target");
releaseOpen = () => {
remoteFiles.set(key, Buffer.alloc(0));
eventLog.push(`open-created:${key}`);
callback(null, Buffer.from(`handle:${key}`));
};
},
write() {
throw new Error("WRITE must not run after cancel during OPEN");
},
close(_handle, callback) {
eventLog.push("close");
callback(null);
},
unlink(remotePath, callback) {
const key = String(remotePath);
eventLog.push(`unlink:${key}`);
remoteFiles.delete(key);
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
const key = String(remotePath);
eventLog.push(`delete:${key}`);
remoteFiles.delete(key);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-shared-inplace-open-cancel-nounlink";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: false,
skipAdmission: true,
},
);
const ready = await waitUntil(() => typeof releaseOpen === "function", 2000);
assert.ok(ready, "expected shared in-place write OPEN to stall");
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("transfer hung after in-place cancel OPEN drain force-complete")), 5500);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
releaseOpen?.();
const lateClose = await waitUntil(() => eventLog.includes("close"), 2000);
assert.ok(lateClose, `expected late OPEN close, log=${eventLog.join(",")}`);
assert.equal(
eventLog.some((entry) => entry.startsWith("unlink:")),
false,
`in-place cancel must not unlink final target, log=${eventLog.join(",")}`,
);
assert.equal(
eventLog.some((entry) => entry === `delete:${targetPath}`),
false,
`in-place cancel must not delete final target, log=${eventLog.join(",")}`,
);
assert.ok(remoteFiles.has(targetPath), "final in-place target must still exist after cancel");
});
test("shared upload OPEN drain survives channel error before OPEN callback", async (t) => {
// Codex P2 on 532cf6cc: channel error completed sharedWriteOpenDrain before the
// OPEN callback, so cleanup could race a late truncating OPEN.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-upload-open-channel-error-drain-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 47));
const remoteFiles = new Map();
const eventLog = [];
let releaseOpen = null;
let endCalls = 0;
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
releaseOpen = () => {
remoteFiles.set(key, Buffer.alloc(0));
eventLog.push(`open-created:${key}`);
callback(null, Buffer.from(`handle:${key}`));
};
},
write() {
throw new Error("WRITE must not run after channel error during OPEN");
},
close(_handle, callback) {
eventLog.push("close");
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
return Promise.reject(error);
}
return Promise.resolve({ size: remoteFiles.get(key).length });
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
const key = String(remotePath);
eventLog.push(`delete:${key}`);
remoteFiles.delete(key);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-shared-open-channel-error-drain";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload-channel-error.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const ready = await waitUntil(() => typeof releaseOpen === "function", 2000);
assert.ok(ready, "expected shared write OPEN to stall");
sharedSftp.emit("error", new Error("shared SFTP channel died during upload OPEN"));
// Channel error settles the OPEN wait, but cleanup must wait for drain.
await new Promise((resolve) => setTimeout(resolve, 100));
assert.equal(eventLog.some((entry) => entry.startsWith("delete:")), false,
"cleanup must not run before late OPEN drain after channel error");
releaseOpen?.();
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("transfer did not settle after channel error + late OPEN")), 5000);
}),
]);
assert.ok(result.error, "expected transfer to fail after channel error");
assert.match(result.error, /shared SFTP channel died during upload OPEN/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
assert.ok(eventLog.includes("close"), `expected CLOSE after late OPEN, log=${eventLog.join(",")}`);
// Resumable uploads preserve the stage on error (not cancel), so delete may
// not run; the invariant is drain completed (CLOSE) before the failure path
// returned, with no racing truncating OPEN after settle.
assert.ok(
eventLog.indexOf("close") >= 0
&& eventLog.findIndex((entry) => entry.startsWith("open-created:")) >= 0
&& eventLog.indexOf("close")
> eventLog.findIndex((entry) => entry.startsWith("open-created:")),
`late OPEN must be closed before failure returns, log=${eventLog.join(",")}`,
);
});
test("shared upload OPEN drain force-completes when channel error has no OPEN callback", async (t) => {
// Codex P2 on 90494c0c: channel error rejected OPEN but left sharedWriteOpenDrain
// pending forever when the OPEN callback never arrived, hanging the transfer
// and SFTP lease on a dead shared channel.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-upload-open-channel-error-no-callback-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 49));
let endCalls = 0;
let openCalls = 0;
const sharedSftp = createFastSftp({
open(_remotePath, flags, _callback) {
assert.equal(flags, "w");
openCalls += 1;
// Never invoke the OPEN callback (dead channel after error).
},
write() {
throw new Error("WRITE must not run after channel error during OPEN");
},
close() {
throw new Error("CLOSE must not run without an OPEN handle");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
const error = new Error("ENOENT");
error.code = 2;
return Promise.reject(error);
},
rename() {
return Promise.resolve();
},
async delete() {},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-shared-open-channel-error-no-callback";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload-channel-error-hang.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const ready = await waitUntil(() => openCalls >= 1, 2000);
assert.ok(ready, "expected shared write OPEN to stall");
sharedSftp.emit("error", new Error("shared SFTP channel died with no OPEN callback"));
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer hung awaiting shared write OPEN drain after channel error")),
5000,
);
}),
]);
assert.ok(result.error, "expected transfer to fail after channel error");
assert.match(result.error, /shared SFTP channel died with no OPEN callback/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("fastPut fallback cancel settles while prior isolated write OPEN gate is unresolved", async (t) => {
// #2755 / Codex P2 on 667e9115: non-resumable concurrent-isolated OPEN emits a
// channel error without invoking its callback, so pendingWriteOpenPathGate
// stays unresolved by design. uploadFile then awaits that gate before
// fastPut; uploadFileConcurrent already cleared transfer.abort, so Cancel
// must still settle the transfer (not hang on the unconditional await).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-fastput-gate-cancel-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 61);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload-fastput-gate-cancel.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old")]]);
await fs.promises.writeFile(localPath, payload);
let isolatedChannelCount = 0;
let firstOpenStarted = false;
let firstIsolated = null;
let fastPutCalls = 0;
let reopenedEnded = 0;
const sharedSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
});
const client = {
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
remoteFiles.delete(String(remotePath));
},
client: {
sftp(callback) {
isolatedChannelCount += 1;
if (isolatedChannelCount === 1) {
firstIsolated = createFastSftp({
open(_remotePath, flags, _cb) {
assert.equal(flags, "w");
firstOpenStarted = true;
// Never invoke the OPEN callback (dead isolated channel).
},
write() {
throw new Error("WRITE must not run after channel error during OPEN");
},
close() {
throw new Error("CLOSE must not run without an OPEN handle");
},
fastPut() {
throw new Error("first isolated channel must not fastPut");
},
end() {},
});
callback(null, firstIsolated);
return;
}
// Reopened channel for fastPut fallback — would succeed if reached.
callback(null, createFastSftp({
fastPut(_local, _remote, _opts, cb) {
fastPutCalls += 1;
cb(null);
},
end() {
reopenedEnded += 1;
},
}));
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-fastput-gate-cancel";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const openReady = await waitUntil(() => firstOpenStarted, 2000);
assert.ok(openReady, "expected concurrent-isolated write OPEN to stall");
firstIsolated.emit("error", new Error("isolated SFTP channel died during OPEN"));
// Wait until uploadFile has fallen through concurrent and reopened for fastPut
// (second isolated channel), which is when it awaits pendingWriteOpenPathGate.
const reopened = await waitUntil(() => isolatedChannelCount >= 2, 2000);
assert.ok(reopened, "expected fastPut isolated channel reopen while OPEN gate pending");
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer hung awaiting pendingWriteOpenPathGate before fastPut")),
1500,
);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(fastPutCalls, 0, "cancel must settle before fastPut runs on unresolved gate");
assert.equal(reopenedEnded, 1, "cancel during gate wait must end the reopened isolated channel");
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), true);
});
test("fastPut fallback fails closed when prior isolated write OPEN gate never settles", async (t) => {
// #2755 companion: without cancel, a dead isolated OPEN that never callbacks
// must not pin the transfer forever on pendingWriteOpenPathGate, and must not
// fall through to another writer that races the still-pending truncate.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-fastput-gate-timeout-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 62);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload-fastput-gate-timeout.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old")]]);
await fs.promises.writeFile(localPath, payload);
let isolatedChannelCount = 0;
let firstOpenStarted = false;
let firstIsolated = null;
let fastPutCalls = 0;
let reopenedEnded = 0;
let sharedWriteOpens = 0;
const sharedSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
open(_remotePath, flags, callback) {
sharedWriteOpens += 1;
callback(null, Buffer.from(`shared-handle:${flags}`));
},
write(_handle, _buffer, _offset, length, _position, callback) {
callback(null);
},
close(_handle, callback) {
callback(null);
},
createWriteStream() {
throw new Error("stream fallback must not run after noTransferFallback gate timeout");
},
});
const client = {
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
remoteFiles.delete(String(remotePath));
},
client: {
sftp(callback) {
isolatedChannelCount += 1;
if (isolatedChannelCount === 1) {
firstIsolated = createFastSftp({
open(_remotePath, flags) {
assert.equal(flags, "w");
firstOpenStarted = true;
},
write() {
throw new Error("WRITE must not run after channel error during OPEN");
},
end() {},
});
callback(null, firstIsolated);
return;
}
callback(null, createFastSftp({
fastPut(_local, _remote, _opts, cb) {
fastPutCalls += 1;
cb(null);
},
end() {
reopenedEnded += 1;
},
}));
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-fastput-gate-timeout";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const openReady = await waitUntil(() => firstOpenStarted, 2000);
assert.ok(openReady, "expected concurrent-isolated write OPEN to stall");
firstIsolated.emit("error", new Error("isolated SFTP channel died during OPEN"));
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer hung awaiting pendingWriteOpenPathGate before fastPut")),
5000,
);
}),
]);
assert.match(result.error || "", /Timed out waiting for prior write OPEN to settle before fastPut/i);
assert.equal(fastPutCalls, 0, "must not fastPut while prior OPEN gate is unresolved");
assert.equal(sharedWriteOpens, 0, "must not fall through to shared write after gate timeout");
assert.equal(reopenedEnded, 1, "gate timeout must end the reopened isolated channel");
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("later same-path upload fails promptly after prior write OPEN gate is poisoned", async (t) => {
// Codex P2 on dca41093: after the fail-closed fastPut gate timeout, the dead
// OPEN's truncatingSharedWriteOpenGates entry must not leave a later in-place
// upload blocked forever in pathGate.waitForPrior. Use a symlink destination
// so both attempts OPEN the same final path (allowInPlaceFallback). Subsequent
// waiters must fail promptly (noTransferFallback) while the barrier stays
// fail-closed — they must not hang and must not issue another truncating OPEN.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-gate-fail-closed-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 63);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload-inplace-gate-fail-closed.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old")]]);
await fs.promises.writeFile(localPath, payload);
let isolatedChannelCount = 0;
let firstOpenStarted = false;
let firstOpenPath = null;
let firstIsolated = null;
let laterWriteOpens = 0;
const symlinkLstat = (remotePath, callback) => {
const key = String(remotePath);
if (key.includes(".netcatty-backup-") || key.includes(".netcatty-upload-") || key.includes(".netcatty-")) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
};
const sharedSftp = createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, callback) {
laterWriteOpens += 1;
callback(null, Buffer.from(`shared-handle:${flags}:${remotePath}`));
},
write(_handle, _buffer, _offset, length, _position, callback) {
callback(null);
},
close(_handle, callback) {
callback(null);
},
createWriteStream() {
throw new Error("stream fallback must not run after fail-closed path gate");
},
});
const client = {
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
remoteFiles.delete(String(remotePath));
},
client: {
sftp(callback) {
isolatedChannelCount += 1;
if (isolatedChannelCount === 1) {
firstIsolated = createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags) {
assert.equal(flags, "w");
assert.equal(String(remotePath), targetPath, "first attempt must OPEN in-place final path");
firstOpenPath = String(remotePath);
firstOpenStarted = true;
},
write() {
throw new Error("WRITE must not run after channel error during OPEN");
},
end() {},
});
callback(null, firstIsolated);
return;
}
// Later isolated channels: OPEN must not be reached while the path gate
// is fail-closed; count attempts if the barrier is incorrectly released.
callback(null, createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, cb) {
laterWriteOpens += 1;
cb(null, Buffer.from(`isolated-handle:${flags}:${remotePath}`));
},
write(_handle, _buffer, _offset, length, _position, cb) {
cb(null);
},
close(_handle, cb) {
cb(null);
},
fastPut(_local, _remote, _opts, cb) {
cb(null);
},
end() {},
}));
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstSender = createSender();
const firstRunning = transferBridge.startTransfer(
{ sender: firstSender },
{
transferId: "upload-inplace-gate-fail-closed-first",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const openReady = await waitUntil(() => firstOpenStarted, 2000);
assert.ok(openReady, "expected concurrent-isolated in-place write OPEN to stall");
assert.equal(firstOpenPath, targetPath);
firstIsolated.emit("error", new Error("isolated SFTP channel died during OPEN"));
const firstResult = await Promise.race([
firstRunning,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("first transfer hung awaiting pendingWriteOpenPathGate before fastPut")),
5000,
);
}),
]);
assert.match(
firstResult.error || "",
/isolated SFTP channel died during OPEN|In-place write OPEN poison|Timed out waiting for prior write OPEN/i,
);
// Later same-path in-place upload must fail promptly — not hang on the
// unresolved truncatingSharedWriteOpenGates entry (Codex P2).
const laterSender = createSender();
const laterStartedAt = Date.now();
const laterRunning = transferBridge.startTransfer(
{ sender: laterSender },
{
transferId: "upload-inplace-gate-fail-closed-later",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const laterResult = await Promise.race([
laterRunning,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("later same-path upload hung on unresolved write OPEN path gate")),
1500,
);
}),
]);
assert.ok(
Date.now() - laterStartedAt < 1500,
"later upload must settle promptly after fail-closed path gate",
);
assert.ok(laterResult.error, "expected later upload to fail closed on unresolved path gate");
assert.match(
laterResult.error,
/Timed out waiting for prior write OPEN|prior write OPEN never settled|path gate is fail-closed|write OPEN path gate|In-place write OPEN poison/i,
);
assert.equal(
laterWriteOpens,
0,
"fail-closed path gate must not allow a later truncating write OPEN",
);
assert.equal(laterSender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("path gate poison survives a same-path waiter queued during fastPut timeout", async (t) => {
// Codex P1 on 0292802c: if another upload replaces the map entry during the
// 2s fastPut gate wait, fail() must still leave a fail-closed barrier so a
// third upload cannot OPEN while the original truncating OPEN may land.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-gate-poison-successor-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 64);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload-inplace-gate-poison-successor.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old")]]);
await fs.promises.writeFile(localPath, payload);
let isolatedChannelCount = 0;
let firstOpenStarted = false;
let firstIsolated = null;
let thirdWriteOpens = 0;
const symlinkLstat = (remotePath, callback) => {
const key = String(remotePath);
if (key.includes(".netcatty-backup-") || key.includes(".netcatty-upload-") || key.includes(".netcatty-")) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
};
const client = {
sftp: createFastSftp({
lstat: symlinkLstat,
open() {
throw new Error("shared OPEN must not run while path gate is fail-closed");
},
createWriteStream() {
throw new Error("stream fallback must not run after fail-closed path gate");
},
}),
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
remoteFiles.delete(String(remotePath));
},
client: {
sftp(callback) {
isolatedChannelCount += 1;
if (isolatedChannelCount === 1) {
firstIsolated = createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags) {
assert.equal(flags, "w");
assert.equal(String(remotePath), targetPath);
firstOpenStarted = true;
},
end() {},
});
callback(null, firstIsolated);
return;
}
callback(null, createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, cb) {
thirdWriteOpens += 1;
cb(null, Buffer.from(`isolated-handle:${flags}:${remotePath}`));
},
write(_handle, _buffer, _offset, length, _position, cb) {
cb(null);
},
close(_handle, cb) {
cb(null);
},
fastPut(_local, _remote, _opts, cb) {
cb(null);
},
end() {},
}));
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-poison-successor-first",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const openReady = await waitUntil(() => firstOpenStarted, 2000);
assert.ok(openReady, "expected first in-place write OPEN to stall");
firstIsolated.emit("error", new Error("isolated SFTP channel died during OPEN"));
// In-place OPEN poison is terminal immediately (no fastPut wait). Queue a
// second same-path upload while the first is settling so fail() must still
// leave a fail-closed barrier for a third upload.
const secondRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-poison-successor-second",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const firstResult = await Promise.race([
firstRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("first transfer hung on gate timeout")), 5000);
}),
]);
assert.match(
firstResult.error || "",
/isolated SFTP channel died during OPEN|In-place write OPEN poison|Timed out waiting for prior write OPEN/i,
);
const secondResult = await Promise.race([
secondRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("second transfer hung after prior gate poison")), 1500);
}),
]);
assert.ok(secondResult.error, "expected second waiter to fail closed after poison");
assert.match(
secondResult.error,
/prior write OPEN never settled|path gate is fail-closed|In-place write OPEN poison/i,
);
const thirdStartedAt = Date.now();
const thirdRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-poison-successor-third",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const thirdResult = await Promise.race([
thirdRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("third upload hung or raced after successor release")), 1500);
}),
]);
assert.ok(Date.now() - thirdStartedAt < 1500, "third upload must settle promptly");
assert.ok(thirdResult.error, "expected third upload to fail closed");
assert.match(thirdResult.error, /prior write OPEN never settled|path gate is fail-closed/i);
assert.equal(thirdWriteOpens, 0, "poisoned barrier must survive successor waiter release");
});
test("path gate poison clears after late original OPEN settles past successor", async (t) => {
// Codex P2 on 64450bfd: after fail() poisons a queued successor, the OPEN
// owner's late callback must still be able to release the barrier. Successor
// fail propagation must not steal the map slot, or every later upload stays
// fail-closed forever even though the dangerous OPEN has settled.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-gate-poison-clear-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 66);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload-inplace-gate-poison-clear.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old")]]);
await fs.promises.writeFile(localPath, payload);
let isolatedChannelCount = 0;
let firstOpenStarted = false;
let releaseFirstOpen = null;
let firstIsolated = null;
let postClearWriteOpens = 0;
const symlinkLstat = (remotePath, callback) => {
const key = String(remotePath);
if (key.includes(".netcatty-backup-") || key.includes(".netcatty-upload-") || key.includes(".netcatty-")) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
};
const client = {
sftp: createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, callback) {
postClearWriteOpens += 1;
callback(null, Buffer.from(`shared-handle:${flags}:${remotePath}`));
},
write(_handle, _buffer, _offset, length, _position, callback) {
callback(null);
},
close(_handle, callback) {
callback(null);
},
createWriteStream() {
throw new Error("stream fallback must not run in this test");
},
}),
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
remoteFiles.delete(String(remotePath));
},
client: {
sftp(callback) {
isolatedChannelCount += 1;
if (isolatedChannelCount === 1) {
firstIsolated = createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, openCb) {
assert.equal(flags, "w");
assert.equal(String(remotePath), targetPath);
firstOpenStarted = true;
releaseFirstOpen = () => {
openCb(null, Buffer.from(`late-handle:${remotePath}`));
};
},
close(_handle, cb) {
cb(null);
},
unlink(_remotePath, cb) {
cb(null);
},
end() {},
});
callback(null, firstIsolated);
return;
}
callback(null, createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, cb) {
postClearWriteOpens += 1;
remoteFiles.set(String(remotePath), Buffer.from(payload));
cb(null, Buffer.from(`isolated-handle:${flags}:${remotePath}`));
},
write(_handle, _buffer, _offset, length, _position, cb) {
cb(null);
},
close(_handle, cb) {
cb(null);
},
fastPut(local, remote, _opts, cb) {
postClearWriteOpens += 1;
remoteFiles.set(String(remote), Buffer.from(payload));
cb(null);
},
end() {},
}));
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-poison-clear-first",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const openReady = await waitUntil(() => firstOpenStarted, 2000);
assert.ok(openReady, "expected first in-place write OPEN to stall");
firstIsolated.emit("error", new Error("isolated SFTP channel died during OPEN"));
const secondRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-poison-clear-second",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const firstResult = await Promise.race([
firstRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("first transfer hung on gate timeout")), 5000);
}),
]);
assert.match(
firstResult.error || "",
/isolated SFTP channel died during OPEN|In-place write OPEN poison|Timed out waiting for prior write OPEN/i,
);
await Promise.race([
secondRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("second transfer hung after prior gate poison")), 1500);
}),
]);
// Late OPEN from the original attempt settles and must clear the owner's
// poisoned barrier even though a successor was poisoned during the wait.
assert.equal(typeof releaseFirstOpen, "function", "expected late OPEN callback to be capturable");
releaseFirstOpen();
await new Promise((resolve) => setTimeout(resolve, 100));
const thirdRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-poison-clear-third",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const thirdResult = await Promise.race([
thirdRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("third upload hung after late OPEN should have cleared poison")), 5000);
}),
]);
assert.notEqual(
thirdResult.error && /prior write OPEN never settled|path gate is fail-closed/i.test(thirdResult.error),
true,
`third upload must not stay permanently fail-closed after late OPEN release, error=${thirdResult.error}`,
);
assert.ok(
postClearWriteOpens > 0
|| (thirdResult.transferId && thirdResult.error == null && thirdResult.cancelled !== true)
|| thirdResult.cancelled === true
|| thirdResult.error,
`expected third upload to settle after barrier clear, result=${JSON.stringify(thirdResult)}`,
);
});
test("later same-path upload fails promptly after cancel during unresolved OPEN gate wait", async (t) => {
// Codex P2 on 0292802c: cancel during the fastPut pending-gate wait must
// poison the shared path gate, same as the idle timeout path.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-gate-cancel-poison-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 65);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload-inplace-gate-cancel-poison.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old")]]);
await fs.promises.writeFile(localPath, payload);
let isolatedChannelCount = 0;
let firstOpenStarted = false;
let firstIsolated = null;
let laterWriteOpens = 0;
const symlinkLstat = (remotePath, callback) => {
const key = String(remotePath);
if (key.includes(".netcatty-backup-") || key.includes(".netcatty-upload-") || key.includes(".netcatty-")) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
};
const client = {
sftp: createFastSftp({
lstat: symlinkLstat,
open() {
laterWriteOpens += 1;
throw new Error("shared OPEN must not run while path gate is fail-closed");
},
createWriteStream() {
throw new Error("stream fallback must not run after fail-closed path gate");
},
}),
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
remoteFiles.delete(String(remotePath));
},
client: {
sftp(callback) {
isolatedChannelCount += 1;
if (isolatedChannelCount === 1) {
firstIsolated = createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags) {
assert.equal(flags, "w");
assert.equal(String(remotePath), targetPath);
firstOpenStarted = true;
},
end() {},
});
callback(null, firstIsolated);
return;
}
callback(null, createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, cb) {
laterWriteOpens += 1;
cb(null, Buffer.from(`isolated-handle:${flags}:${remotePath}`));
},
write(_handle, _buffer, _offset, length, _position, cb) {
cb(null);
},
close(_handle, cb) {
cb(null);
},
fastPut(_local, _remote, _opts, cb) {
cb(null);
},
end() {},
}));
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstTransferId = "upload-inplace-gate-cancel-poison-first";
const firstRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: firstTransferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const openReady = await waitUntil(() => firstOpenStarted, 2000);
assert.ok(openReady, "expected first in-place write OPEN to stall");
// Cancel while the in-place OPEN is still unsettled. After the settle timeout
// the path gate is poisoned so later same-path uploads fail promptly.
await transferBridge.cancelTransfer(null, { transferId: firstTransferId });
const firstResult = await Promise.race([
firstRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("first transfer hung on cancel during OPEN")), 5000);
}),
]);
assert.match(firstResult.error || "", /cancel/i);
const laterStartedAt = Date.now();
const laterRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-cancel-poison-later",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const laterResult = await Promise.race([
laterRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("later upload hung after cancel left path gate unresolved")), 1500);
}),
]);
assert.ok(Date.now() - laterStartedAt < 1500, "later upload must settle promptly after cancel poison");
assert.ok(laterResult.error, "expected later upload to fail closed");
assert.match(laterResult.error, /prior write OPEN never settled|path gate is fail-closed/i);
assert.equal(laterWriteOpens, 0, "cancel poison must not allow a later truncating write OPEN");
});
test("path gate poison clears after owning isolated transport ends", async (t) => {
// Codex P2 on 713719c2: a dead OPEN that never callbacks leaves a host-keyed
// poison. Once the owning isolated channel has ended and a short grace
// passes, clear the barrier so reconnect / later uploads are not
// permanently fail-closed until process restart.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-gate-transport-clear-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 67);
const localPath = path.join(tempDir, "upload.bin");
const targetPath = "/tmp/upload-inplace-gate-transport-clear.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("old")]]);
await fs.promises.writeFile(localPath, payload);
let isolatedChannelCount = 0;
let firstOpenStarted = false;
let firstIsolated = null;
let postClearWriteOpens = 0;
const symlinkLstat = (remotePath, callback) => {
const key = String(remotePath);
if (key.includes(".netcatty-backup-") || key.includes(".netcatty-upload-") || key.includes(".netcatty-")) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
};
const client = {
sftp: createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, callback) {
postClearWriteOpens += 1;
remoteFiles.set(String(remotePath), Buffer.from(payload));
callback(null, Buffer.from(`shared-handle:${flags}:${remotePath}`));
},
write(_handle, _buffer, _offset, length, _position, callback) {
callback(null);
},
close(_handle, callback) {
callback(null);
},
createWriteStream() {
throw new Error("stream fallback must not run in this test");
},
}),
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
remoteFiles.delete(String(remotePath));
},
client: {
sftp(callback) {
isolatedChannelCount += 1;
if (isolatedChannelCount === 1) {
firstIsolated = createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags) {
assert.equal(flags, "w");
assert.equal(String(remotePath), targetPath);
firstOpenStarted = true;
},
end() {
this.emit("end");
this.emit("close");
},
});
callback(null, firstIsolated);
return;
}
callback(null, createFastSftp({
lstat: symlinkLstat,
open(remotePath, flags, cb) {
postClearWriteOpens += 1;
remoteFiles.set(String(remotePath), Buffer.from(payload));
cb(null, Buffer.from(`isolated-handle:${flags}:${remotePath}`));
},
write(_handle, _buffer, _offset, length, _position, cb) {
cb(null);
},
close(_handle, cb) {
cb(null);
},
fastPut(local, remote, _opts, cb) {
postClearWriteOpens += 1;
remoteFiles.set(String(remote), Buffer.from(payload));
cb(null);
},
end() {},
}));
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-transport-clear-first",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const openReady = await waitUntil(() => firstOpenStarted, 2000);
assert.ok(openReady, "expected first in-place write OPEN to stall");
firstIsolated.emit("error", new Error("isolated SFTP channel died during OPEN"));
const firstResult = await Promise.race([
firstRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("first transfer hung on gate timeout")), 5000);
}),
]);
assert.match(
firstResult.error || "",
/isolated SFTP channel died during OPEN|In-place write OPEN poison|Timed out waiting for prior write OPEN/i,
);
// Immediately after poison the barrier must still fail closed.
const midRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-transport-clear-mid",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const midResult = await Promise.race([
midRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("mid upload hung on poisoned gate")), 1500);
}),
]);
assert.match(midResult.error || "", /prior write OPEN never settled|path gate is fail-closed/i);
// After transport-gone grace, the host-keyed poison must clear.
await new Promise((resolve) => setTimeout(resolve, 2100));
const laterRunning = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-inplace-gate-transport-clear-later",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
},
);
const laterResult = await Promise.race([
laterRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("later upload hung after transport-gone poison clear")), 5000);
}),
]);
assert.notEqual(
laterResult.error && /prior write OPEN never settled|path gate is fail-closed/i.test(laterResult.error),
true,
`later upload must not stay permanently fail-closed after transport end, error=${laterResult.error}`,
);
assert.ok(
postClearWriteOpens > 0
|| (laterResult.transferId && laterResult.error == null && laterResult.cancelled !== true)
|| laterResult.cancelled === true
|| laterResult.error,
`expected later upload to settle after transport-gone clear, result=${JSON.stringify(laterResult)}`,
);
});
test("shared upload OPEN drain unlinks late staged OPEN after channel-error force-complete", async (t) => {
// Codex P2 on bd42c51c: late truncating OPEN after channel-error drain
// force-complete is not a cancel (`transfer.cancelled` false). Non-resumable
// staged uploads already cleaned the stage after the forced drain; the late
// "w" OPEN must still unlink the recreated `.netcatty-upload-*` orphan.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-upload-open-channel-error-late-unlink-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 55));
const remoteFiles = new Map();
const eventLog = [];
let releaseOpen = null;
let endCalls = 0;
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
releaseOpen = () => {
remoteFiles.set(key, Buffer.alloc(0));
eventLog.push(`open-created:${key}`);
callback(null, Buffer.from(`handle:${key}`));
};
},
write() {
throw new Error("WRITE must not run after channel error during OPEN");
},
close(_handle, callback) {
eventLog.push("close");
callback(null);
},
unlink(remotePath, callback) {
const key = String(remotePath);
eventLog.push(`unlink:${key}`);
remoteFiles.delete(key);
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
const key = String(remotePath);
eventLog.push(`delete:${key}`);
remoteFiles.delete(key);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "upload-shared-open-channel-error-late-unlink";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: localPath,
targetPath: "/tmp/upload-channel-error-late-unlink.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
// Non-resumable → cleanupStage runs after failure (no preserve-on-error).
resumable: false,
skipAdmission: true,
},
);
const ready = await waitUntil(() => typeof releaseOpen === "function", 2000);
assert.ok(ready, "expected shared write OPEN to stall");
sharedSftp.emit("error", new Error("shared SFTP channel died before OPEN callback"));
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer hung awaiting shared write OPEN drain after channel error")),
5000,
);
}),
]);
assert.ok(result.error, "expected transfer to fail after channel error");
assert.match(result.error, /shared SFTP channel died before OPEN callback/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
// Late OPEN after drain force-complete — must close + unlink generated stage.
releaseOpen?.();
const lateCleanup = await waitUntil(
() => eventLog.includes("close") && eventLog.some((entry) => entry.startsWith("unlink:")),
2000,
);
assert.ok(lateCleanup, `expected late OPEN close+unlink after channel error, log=${eventLog.join(",")}`);
assert.equal(
[...remoteFiles.keys()].some((key) => key.includes(".netcatty-upload-")),
false,
`expected no orphan staged upload, remaining=${[...remoteFiles.keys()].join(",")}`,
);
});
test("late shared OPEN unlink still cleans non-resumable stage under same-id retry", async (t) => {
// Codex P2 on 7c446c7: retry ownership must not suppress unlink for every
// generatedStagePath. Non-resumable uploads use a unique
// `.netcatty-upload-*` path per attempt; a stale late OPEN on attempt 1's
// path must still unlink so the orphan is not left behind when a same-id
// retry already owns activeTransfers.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-open-late-unlink-nonresume-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 59));
const targetPath = "/tmp/upload-late-unlink-nonresume.bin";
const transferId = "upload-shared-open-late-unlink-nonresume";
const remoteFiles = new Map();
const eventLog = [];
/** @type {null | (() => void)} */
let releaseFirstOpen = null;
let openGeneration = 0;
let endCalls = 0;
/** @type {null | (() => void)} */
let releaseRetryWrites = null;
const retryWritesReleased = new Promise((resolve) => {
releaseRetryWrites = resolve;
});
/** @type {string | null} */
let firstStagePath = null;
/** @type {string | null} */
let retryStagePath = null;
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
openGeneration += 1;
const generation = openGeneration;
if (generation === 1) {
firstStagePath = key;
assert.match(key, /\.netcatty-upload-.*\.part$/, "first attempt must OPEN unique non-resumable stage");
releaseFirstOpen = () => {
if (!remoteFiles.has(key)) {
remoteFiles.set(key, Buffer.alloc(0));
}
eventLog.push(`open-created-1:${key}`);
callback(null, Buffer.from(`handle-1:${key}`));
};
return;
}
retryStagePath = key;
assert.match(key, /\.netcatty-upload-.*\.part$/, "retry must OPEN a fresh non-resumable stage");
assert.notEqual(key, firstStagePath, "non-resumable retry must not reuse first stage path");
remoteFiles.set(key, Buffer.from("retry-stage"));
eventLog.push(`open-created-${generation}:${key}`);
callback(null, Buffer.from(`handle-${generation}:${key}`));
},
write(handle, buffer, offset, length, position, callback) {
void retryWritesReleased.then(() => {
const key = String(handle).replace(/^handle-\d+:/, "");
const current = remoteFiles.get(key) || Buffer.alloc(0);
const end = position + length;
const next = Buffer.alloc(Math.max(current.length, end));
current.copy(next);
buffer.copy(next, position, offset, offset + length);
remoteFiles.set(key, next);
eventLog.push(`write:${key}:${length}@${position}`);
setImmediate(() => callback(null));
});
},
close(handle, callback) {
eventLog.push(`close:${String(handle)}`);
callback(null);
},
unlink(remotePath, callback) {
const key = String(remotePath);
eventLog.push(`unlink:${key}`);
remoteFiles.delete(key);
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
const key = String(remotePath);
eventLog.push(`delete:${key}`);
remoteFiles.delete(key);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstSender = createSender();
const firstRunning = transferBridge.startTransfer(
{ sender: firstSender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: false,
skipAdmission: true,
},
);
const firstOpenReady = await waitUntil(() => typeof releaseFirstOpen === "function", 2000);
assert.ok(firstOpenReady, "expected first shared write OPEN to stall");
sharedSftp.emit("error", new Error("shared SFTP channel died before first OPEN callback"));
const firstResult = await Promise.race([
firstRunning,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("first transfer hung awaiting shared write OPEN drain after channel error")),
5000,
);
}),
]);
assert.ok(firstResult.error, "expected first transfer to fail after channel error");
assert.match(firstResult.error, /shared SFTP channel died before first OPEN callback/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
// Same transfer id retry (non-resumable) owns a different stage path before
// the stale OPEN callback from attempt 1 arrives.
transferBridge.clearPendingCancel(transferId);
const retrySender = createSender();
const retryRunning = transferBridge.startTransfer(
{ sender: retrySender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: false,
skipAdmission: true,
},
);
const retryOpenReady = await waitUntil(
() => eventLog.some((entry) => entry.startsWith("open-created-2:")),
3000,
);
assert.ok(retryOpenReady, `expected retry OPEN, log=${eventLog.join(",")}`);
assert.ok(retryStagePath, "retry must have opened a stage");
assert.ok(
remoteFiles.has(retryStagePath),
"retry must own its unique stage before stale late OPEN",
);
const retryStageBeforeLateOpen = Buffer.from(remoteFiles.get(retryStagePath));
// Stale late OPEN from attempt 1: must close + unlink attempt-1 stage only.
releaseFirstOpen?.();
const lateCleanup = await waitUntil(
() => eventLog.some((entry) => entry.startsWith("close:handle-1:"))
&& eventLog.some((entry) => entry === `unlink:${firstStagePath}`),
2000,
);
assert.ok(
lateCleanup,
`expected late close+unlink of first non-resumable stage, log=${eventLog.join(",")}`,
);
assert.equal(
eventLog.some((entry) => entry === `unlink:${retryStagePath}`),
false,
`stale late OPEN must not unlink retry stage, log=${eventLog.join(",")}`,
);
assert.ok(
remoteFiles.has(retryStagePath),
"retry stage must survive stale late OPEN on a different path",
);
assert.ok(
remoteFiles.get(retryStagePath).equals(retryStageBeforeLateOpen),
"retry stage bytes must not be removed by stale late OPEN unlink",
);
assert.equal(
remoteFiles.has(firstStagePath),
false,
"first attempt non-resumable stage must be unlinked (no orphan)",
);
releaseRetryWrites?.();
await transferBridge.cancelTransfer(null, { transferId });
const retryResult = await Promise.race([
retryRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("retry transfer hung after cancel")), 5000);
}),
]);
assert.ok(
retryResult.cancelled === true || /cancel/i.test(String(retryResult.error || "")),
`expected retry cancel settle, result=${JSON.stringify(retryResult)}`,
);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
});
test("late shared OPEN unlink skips same-id retry resume stage", async (t) => {
// Codex P2 on d19ecb88 + P1 on 42a27ef7:
// - After drain force-complete, a late truncating "w" must not unlink a
// same-id retry's deterministic resume stage.
// - Path gate serializes truncating OPENs on that path; if force-complete
// already released the gate and the retry accepted its own OPEN, a late
// stale OPEN that re-truncates must invalidate the retry rather than allow
// a sparse promote.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-open-late-unlink-retry-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 57));
const targetPath = "/tmp/upload-late-unlink-retry.bin";
const transferId = "upload-shared-open-late-unlink-retry";
const deterministicStagePath = `/tmp/.upload-late-unlink-retry.bin.netcatty-${transferId}.part`;
const remoteFiles = new Map();
const eventLog = [];
/** @type {null | (() => void)} */
let releaseFirstOpen = null;
let openGeneration = 0;
let endCalls = 0;
/** @type {null | (() => void)} */
let releaseRetryWrites = null;
const retryWritesReleased = new Promise((resolve) => {
releaseRetryWrites = resolve;
});
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
openGeneration += 1;
const generation = openGeneration;
if (generation === 1) {
assert.equal(key, deterministicStagePath, "first attempt must OPEN deterministic resume stage");
releaseFirstOpen = () => {
// Model a late server-side truncating OPEN: wipe whatever is at the
// path when the stale OPEN finally applies.
remoteFiles.set(key, Buffer.alloc(0));
eventLog.push(`open-created-1:${key}`);
callback(null, Buffer.from(`handle-1:${key}`));
};
return;
}
remoteFiles.set(key, Buffer.from("retry-stage"));
eventLog.push(`open-created-${generation}:${key}`);
callback(null, Buffer.from(`handle-${generation}:${key}`));
},
write(handle, buffer, offset, length, position, callback) {
// Hold retry WRITEs so the stale OPEN can land mid-attempt.
void retryWritesReleased.then(() => {
const key = String(handle).replace(/^handle-\d+:/, "");
const current = remoteFiles.get(key) || Buffer.alloc(0);
const end = position + length;
const next = Buffer.alloc(Math.max(current.length, end));
current.copy(next);
buffer.copy(next, position, offset, offset + length);
remoteFiles.set(key, next);
eventLog.push(`write:${key}:${length}@${position}`);
setImmediate(() => callback(null));
});
},
close(handle, callback) {
eventLog.push(`close:${String(handle)}`);
callback(null);
},
unlink(remotePath, callback) {
const key = String(remotePath);
eventLog.push(`unlink:${key}`);
remoteFiles.delete(key);
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
const key = String(remotePath);
eventLog.push(`delete:${key}`);
remoteFiles.delete(key);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstSender = createSender();
const firstRunning = transferBridge.startTransfer(
{ sender: firstSender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const firstOpenReady = await waitUntil(() => typeof releaseFirstOpen === "function", 2000);
assert.ok(firstOpenReady, "expected first shared write OPEN to stall");
sharedSftp.emit("error", new Error("shared SFTP channel died before first OPEN callback"));
const firstResult = await Promise.race([
firstRunning,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("first transfer hung awaiting shared write OPEN drain after channel error")),
5000,
);
}),
]);
assert.ok(firstResult.error, "expected first transfer to fail after channel error");
assert.match(firstResult.error, /shared SFTP channel died before first OPEN callback/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
// Same-id retry reuses the deterministic stage. Drain force-complete does
// not release the path gate: the retry must wait until the stale OPEN
// callback settles (Codex P1 — no hard-cap race for different transferIds).
transferBridge.clearPendingCancel(transferId);
const retrySender = createSender();
const retryRunning = transferBridge.startTransfer(
{ sender: retrySender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
await new Promise((resolve) => setTimeout(resolve, 500));
assert.equal(
eventLog.some((entry) => entry.startsWith("open-created-2:")),
false,
`retry must not OPEN while prior gate is held, log=${eventLog.join(",")}`,
);
// Settling the first OPEN frees the gate; then the retry may proceed.
releaseFirstOpen?.();
const lateClose = await waitUntil(
() => eventLog.some((entry) => entry.startsWith("close:handle-1:")),
2000,
);
assert.ok(lateClose, `expected close of first OPEN handle, log=${eventLog.join(",")}`);
const retryOpenReady = await waitUntil(
() => eventLog.some((entry) => entry.startsWith("open-created-2:")),
5000,
);
assert.ok(retryOpenReady, `expected retry OPEN after prior gate release, log=${eventLog.join(",")}`);
releaseRetryWrites?.();
const retryResult = await Promise.race([
retryRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("retry transfer hung after prior OPEN settled")), 8000);
}),
]);
// Normal success is { transferId, totalBytes }; cancel/error also settle.
assert.ok(
retryResult
&& (
(retryResult.transferId && retryResult.error == null && retryResult.cancelled !== true)
|| retryResult.cancelled === true
|| retryResult.error
),
`expected retry to settle after gate release, result=${JSON.stringify(retryResult)}`,
);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
});
test("truncating shared OPEN path gate serializes same-path retry until prior OPEN settles", async (t) => {
// Codex P1 on 42a27ef7: while attempt-1's truncating OPEN is still pending
// (no force-complete yet), a same-id retry must not issue OPEN "w" on the
// deterministic stage — wait for the prior gate.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-open-path-gate-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 61));
const targetPath = "/tmp/upload-path-gate.bin";
const transferId = "upload-shared-open-path-gate";
const deterministicStagePath = `/tmp/.upload-path-gate.bin.netcatty-${transferId}.part`;
const remoteFiles = new Map();
const eventLog = [];
/** @type {null | (() => void)} */
let releaseFirstOpen = null;
let openGeneration = 0;
let endCalls = 0;
/** @type {null | (() => void)} */
let releaseRetryWrites = null;
const retryWritesReleased = new Promise((resolve) => {
releaseRetryWrites = resolve;
});
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
openGeneration += 1;
const generation = openGeneration;
if (generation === 1) {
releaseFirstOpen = () => {
remoteFiles.set(key, Buffer.alloc(0));
eventLog.push(`open-created-1:${key}`);
callback(null, Buffer.from(`handle-1:${key}`));
};
return;
}
remoteFiles.set(key, Buffer.from("retry-after-gate"));
eventLog.push(`open-created-${generation}:${key}`);
callback(null, Buffer.from(`handle-${generation}:${key}`));
},
write(handle, buffer, offset, length, position, callback) {
void retryWritesReleased.then(() => {
const key = String(handle).replace(/^handle-\d+:/, "");
const current = remoteFiles.get(key) || Buffer.alloc(0);
const end = position + length;
const next = Buffer.alloc(Math.max(current.length, end));
current.copy(next);
buffer.copy(next, position, offset, offset + length);
remoteFiles.set(key, next);
setImmediate(() => callback(null));
});
},
close(handle, callback) {
eventLog.push(`close:${String(handle)}`);
callback(null);
},
unlink(remotePath, callback) {
eventLog.push(`unlink:${String(remotePath)}`);
remoteFiles.delete(String(remotePath));
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
remoteFiles.delete(String(remotePath));
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstSender = createSender();
// Do not emit channel error: keep OPEN pending without force-complete so the
// path gate stays held until the OPEN callback.
const firstRunning = transferBridge.startTransfer(
{ sender: firstSender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const firstOpenReady = await waitUntil(() => typeof releaseFirstOpen === "function", 2000);
assert.ok(firstOpenReady, "expected first shared write OPEN to stall");
// Start same-id retry while first OPEN is still pending (gate held).
// Overwrite activeTransfers; first attempt is still awaiting OPEN.
transferBridge.clearPendingCancel(transferId);
const retrySender = createSender();
const retryRunning = transferBridge.startTransfer(
{ sender: retrySender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
await new Promise((resolve) => setTimeout(resolve, 300));
assert.equal(
eventLog.some((entry) => entry.startsWith("open-created-2:")),
false,
`retry must wait on path gate while prior truncating OPEN is pending, log=${eventLog.join(",")}`,
);
// Settle first OPEN (cancel it via late path): release callback as success
// but first transfer may still be the waiter — force cancel first attempt.
await transferBridge.cancelTransfer(null, { transferId: `${transferId}-noop` }).catch(() => {});
// First attempt is no longer active (retry replaced it). Release OPEN.
releaseFirstOpen?.();
const lateClose = await waitUntil(
() => eventLog.some((entry) => entry.startsWith("close:handle-1:")),
2000,
);
assert.ok(lateClose, `expected first OPEN to close after gate release, log=${eventLog.join(",")}`);
const retryOpenReady = await waitUntil(
() => eventLog.some((entry) => entry.startsWith("open-created-2:")),
3000,
);
assert.ok(
retryOpenReady,
`expected retry OPEN after prior OPEN settled, log=${eventLog.join(",")}`,
);
assert.ok(
remoteFiles.get(deterministicStagePath)?.equals(Buffer.from("retry-after-gate")),
"retry stage must be written by retry OPEN after gate, not wiped by ordering race",
);
releaseRetryWrites?.();
await transferBridge.cancelTransfer(null, { transferId });
const retryResult = await Promise.race([
retryRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("retry hung after path-gate test")), 5000);
}),
]);
assert.ok(
retryResult.cancelled === true || /cancel/i.test(String(retryResult.error || "")),
`expected retry cancel settle, result=${JSON.stringify(retryResult)}`,
);
// First attempt may still be pending cancel/error; do not leave it hanging.
void firstRunning.catch(() => {});
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
});
test("remoteOpenPathMatchesStaged compares logical and encoded OPEN paths", () => {
// Codex: filePath can be a session-encoded Buffer while stagedRemote.path is
// the logical string — strict === would fail and wrongly allow unlink of the
// retry-owned stage.
const match = transferBridge._remoteOpenPathMatchesStagedForTests;
assert.equal(typeof match, "function");
const logical = "/tmp/.upload.bin.netcatty-tid.part";
assert.equal(
match(logical, { path: logical, sftpId: null }),
true,
"identical logical strings match",
);
assert.equal(
match(Buffer.from(logical), { path: logical, sftpId: null }),
true,
"OPEN Buffer of logical path matches staged logical path",
);
assert.equal(
match(Buffer.from(logical), {
path: logical,
sftpId: "missing-session",
encoding: "utf-8",
}),
true,
"utf-8 encode of logical path still matches OPEN Buffer",
);
assert.equal(
match("/tmp/other.part", { path: logical, sftpId: null }),
false,
"different paths must not match",
);
assert.equal(
match(logical, null),
false,
"missing stagedRemote must not match",
);
assert.equal(
match(logical, { path: "" }),
false,
"empty staged path must not match",
);
});
test("late shared OPEN unlink still cleans stage when same-id retry is in-place", async (t) => {
// Codex P2 on 39e20bfa: skip late unlink only when active.stagedRemote.path
// matches. A same-id in-place retry (symlink destination → writeInPlace)
// keeps resumable + targetPath but stagedRemote is null; a stale late OPEN
// on the leftover `.netcatty-<id>.part` must still unlink the orphan.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-open-late-unlink-inplace-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(TRANSFER_CHUNK_SIZE, 61));
const targetPath = "/tmp/upload-late-unlink-inplace.bin";
const transferId = "upload-shared-open-late-unlink-inplace";
const deterministicStagePath = `/tmp/.upload-late-unlink-inplace.bin.netcatty-${transferId}.part`;
const remoteFiles = new Map();
// After the first attempt fails, the destination appears as a symlink so the
// retry is planned in-place (no stagedRemote).
let destinationIsSymlink = false;
const eventLog = [];
/** @type {null | (() => void)} */
let releaseFirstOpen = null;
let openGeneration = 0;
let endCalls = 0;
/** @type {null | (() => void)} */
let releaseRetryWrites = null;
const retryWritesReleased = new Promise((resolve) => {
releaseRetryWrites = resolve;
});
const sharedSftp = createFastSftp({
lstat(remotePath, callback) {
const key = String(remotePath);
if (destinationIsSymlink && key === targetPath) {
callback(null, {
size: 0,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
return;
}
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o100644,
isDirectory: () => false,
isSymbolicLink: () => false,
});
},
open(remotePath, flags, callback) {
assert.equal(flags, "w");
const key = String(remotePath);
openGeneration += 1;
const generation = openGeneration;
if (generation === 1) {
assert.equal(key, deterministicStagePath, "first attempt must OPEN deterministic resume stage");
releaseFirstOpen = () => {
if (!remoteFiles.has(key)) {
remoteFiles.set(key, Buffer.alloc(0));
}
eventLog.push(`open-created-1:${key}`);
callback(null, Buffer.from(`handle-1:${key}`));
};
return;
}
// In-place retry opens the final target, not the stage.
assert.equal(key, targetPath, "in-place retry must OPEN final target");
remoteFiles.set(key, Buffer.from("retry-inplace"));
eventLog.push(`open-created-${generation}:${key}`);
callback(null, Buffer.from(`handle-${generation}:${key}`));
},
write(handle, buffer, offset, length, position, callback) {
void retryWritesReleased.then(() => {
const key = String(handle).replace(/^handle-\d+:/, "");
const current = remoteFiles.get(key) || Buffer.alloc(0);
const end = position + length;
const next = Buffer.alloc(Math.max(current.length, end));
current.copy(next);
buffer.copy(next, position, offset, offset + length);
remoteFiles.set(key, next);
eventLog.push(`write:${key}:${length}@${position}`);
setImmediate(() => callback(null));
});
},
close(handle, callback) {
eventLog.push(`close:${String(handle)}`);
callback(null);
},
unlink(remotePath, callback) {
const key = String(remotePath);
eventLog.push(`unlink:${key}`);
remoteFiles.delete(key);
callback(null);
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete(remotePath) {
const key = String(remotePath);
eventLog.push(`delete:${key}`);
remoteFiles.delete(key);
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const firstSender = createSender();
const firstRunning = transferBridge.startTransfer(
{ sender: firstSender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const firstOpenReady = await waitUntil(() => typeof releaseFirstOpen === "function", 2000);
assert.ok(firstOpenReady, "expected first shared write OPEN to stall");
sharedSftp.emit("error", new Error("shared SFTP channel died before first OPEN callback"));
const firstResult = await Promise.race([
firstRunning,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("first transfer hung awaiting shared write OPEN drain after channel error")),
5000,
);
}),
]);
assert.ok(firstResult.error, "expected first transfer to fail after channel error");
assert.match(firstResult.error, /shared SFTP channel died before first OPEN callback/i);
// Force the retry into writeInPlace (symlink) while still resumable.
destinationIsSymlink = true;
// Seed a leftover stage orphan the late OPEN would otherwise protect via
// resumable+targetPath heuristics.
remoteFiles.set(deterministicStagePath, Buffer.from("orphan-stage"));
transferBridge.clearPendingCancel(transferId);
const retrySender = createSender();
const retryRunning = transferBridge.startTransfer(
{ sender: retrySender },
{
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: TRANSFER_CHUNK_SIZE,
resumable: true,
skipAdmission: true,
},
);
const retryOpenReady = await waitUntil(
() => eventLog.some((entry) => entry.startsWith("open-created-2:")),
3000,
);
assert.ok(retryOpenReady, `expected in-place retry OPEN, log=${eventLog.join(",")}`);
assert.ok(
remoteFiles.has(deterministicStagePath),
"orphan stage must still exist before stale late OPEN",
);
// Stale late OPEN from attempt 1 on the stage path: must close + unlink even
// though a same-id resumable retry owns activeTransfers (in-place, no stage).
releaseFirstOpen?.();
const lateCleanup = await waitUntil(
() => eventLog.some((entry) => entry.startsWith("close:handle-1:"))
&& eventLog.some((entry) => entry === `unlink:${deterministicStagePath}`),
2000,
);
assert.ok(
lateCleanup,
`expected late close+unlink of orphan stage under in-place retry, log=${eventLog.join(",")}`,
);
assert.equal(
remoteFiles.has(deterministicStagePath),
false,
"orphan stage must be unlinked when retry is not using it",
);
assert.ok(
remoteFiles.has(targetPath),
"in-place retry final target must not be unlinked",
);
releaseRetryWrites?.();
await transferBridge.cancelTransfer(null, { transferId });
const retryResult = await Promise.race([
retryRunning,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("retry transfer hung after cancel")), 5000);
}),
]);
assert.ok(
retryResult.cancelled === true || /cancel/i.test(String(retryResult.error || "")),
`expected retry cancel settle, result=${JSON.stringify(retryResult)}`,
);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
});
test("sudo SFTP downloads prefer concurrent shared READ over serial createReadStream", async (t) => {
// Sudo cannot open an isolated channel, so downloads used to fall straight to
// createReadStream (1-in-flight READs → hundreds of KB/s). Uploads already
// keep pipelined WRITE fanout on the shared browse channel; downloads must
// mirror that for the elevated SFTP path (#2719).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-download-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(DOWNLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE, 47);
for (let index = 0; index < payload.length; index += 1) payload[index] = index % 251;
const targetPath = path.join(tempDir, "download.bin");
let maxInFlight = 0;
let activeReads = 0;
let createReadStreamCalls = 0;
let endCalls = 0;
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("shared-read-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
activeReads += 1;
maxInFlight = Math.max(maxInFlight, activeReads);
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => {
activeReads -= 1;
callback(null, slice.length);
});
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not be used when concurrent READ works");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-sudo-shared-concurrent",
sourcePath: "/root/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(result.error, undefined, result.error);
assert.equal(createReadStreamCalls, 0);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
assert.ok(
maxInFlight >= 2,
`expected pipelined READ concurrency >= 2, got ${maxInFlight}`,
);
assert.ok(
maxInFlight <= DOWNLOAD_TRANSFER_CONCURRENCY,
`expected concurrency <= ${DOWNLOAD_TRANSFER_CONCURRENCY}, got ${maxInFlight}`,
);
const downloaded = await fs.promises.readFile(targetPath);
assert.deepEqual(downloaded, payload);
});
test("non-sudo downloads use concurrent shared READ when isolated channel is unavailable", async (t) => {
// Isolated-channel miss (pool full / open unavailable) must still pipeline
// READs on the browse channel — same fail-closed contract as uploads (#2449).
// Serial createReadStream is never a silent bulk-transfer fallback (#2719).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-nonsudo-shared-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(4 * TRANSFER_CHUNK_SIZE, 19);
for (let index = 0; index < payload.length; index += 1) payload[index] = index % 197;
const targetPath = path.join(tempDir, "download.bin");
let openCalls = 0;
let maxInFlight = 0;
let activeReads = 0;
let createReadStreamCalls = 0;
let endCalls = 0;
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
openCalls += 1;
assert.equal(flags, "r");
callback(null, Buffer.from("shared-read-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
activeReads += 1;
maxInFlight = Math.max(maxInFlight, activeReads);
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => {
activeReads -= 1;
callback(null, slice.length);
});
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run after isolated miss");
},
end() {
endCalls += 1;
},
});
const client = {
// No client.client → openIsolatedSftpChannel returns null (isolated miss).
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-nonsudo-isolated-miss-shared",
sourcePath: "/home/user/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(result.error, undefined, result.error);
assert.ok(openCalls >= 1, "non-sudo isolated miss must use shared concurrent OPEN");
assert.equal(createReadStreamCalls, 0, "serial createReadStream must not run");
assert.equal(endCalls, 0, "browse channel must not be ended");
assert.ok(
maxInFlight >= 2,
`expected pipelined READ concurrency >= 2, got ${maxInFlight}`,
);
const downloaded = await fs.promises.readFile(targetPath);
assert.deepEqual(downloaded, payload);
});
test("second concurrent sudo download waits for shared fast slot (no serial stream)", async (t) => {
// FAST_DOWNLOAD_CHANNELS_PER_SESSION (#1507) still caps concurrent fanout to
// one file; a second download waits for the slot instead of crawling via
// serial createReadStream (#2719 fail-closed alignment).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-slot-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payloadA = Buffer.alloc(4 * TRANSFER_CHUNK_SIZE, 11);
for (let index = 0; index < payloadA.length; index += 1) payloadA[index] = index % 211;
const payloadB = Buffer.alloc(2 * TRANSFER_CHUNK_SIZE, 22);
for (let index = 0; index < payloadB.length; index += 1) payloadB[index] = (index * 3) % 223;
const targetA = path.join(tempDir, "a.bin");
const targetB = path.join(tempDir, "b.bin");
let endCalls = 0;
let createReadStreamCalls = 0;
let openBCalls = 0;
let activeBReads = 0;
let maxInFlightB = 0;
const pendingReadCallbacks = [];
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "r");
if (String(remotePath).includes("b.bin")) openBCalls += 1;
callback(null, Buffer.from(`handle:${remotePath}`));
},
read(handle, buffer, offset, length, position, callback) {
const key = handle.toString();
const payload = key.includes("b.bin") ? payloadB : payloadA;
// Stall first transfer READs so it keeps the shared fast slot until released.
if (!key.includes("b.bin")) {
pendingReadCallbacks.push(() => {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
callback(null, slice.length);
});
return;
}
activeBReads += 1;
maxInFlightB = Math.max(maxInFlightB, activeBReads);
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => {
activeBReads -= 1;
callback(null, slice.length);
});
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run while waiting for fast slot");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat(remotePath) {
const size = String(remotePath).includes("b.bin") ? payloadB.length : payloadA.length;
return Promise.resolve({
size,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const first = transferBridge.startTransfer(
{ sender },
{
transferId: "download-sudo-slot-first",
sourcePath: "/root/a.bin",
targetPath: targetA,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payloadA.length,
resumable: true,
skipAdmission: true,
},
);
const firstReady = await waitUntil(() => pendingReadCallbacks.length >= 1, 2000);
assert.ok(firstReady, "expected first sudo download to hold shared READ slot");
const secondPromise = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-sudo-slot-second",
sourcePath: "/root/b.bin",
targetPath: targetB,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payloadB.length,
resumable: true,
skipAdmission: true,
},
);
// Condition-based wait: second must enter the session slot queue.
assert.ok(
await waitUntil(() => transferBridge._getSessionFastDownloadWaiterCountForTests(client) >= 1, 2000),
"second download must enqueue on the session fast slot",
);
assert.equal(openBCalls, 0, "second download must wait for shared fast slot");
assert.equal(createReadStreamCalls, 0, "serial stream must not run while waiting");
// Release first transfer's stalled READs so it can finish and free the slot.
const drainPending = () => {
for (const release of pendingReadCallbacks.splice(0)) {
try { release(); } catch { /* ignore */ }
}
};
// Keep draining late-arriving first-transfer READs until it settles.
const drainTimer = setInterval(drainPending, 5);
t.after(() => clearInterval(drainTimer));
drainPending();
const firstResult = await Promise.race([
first,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("first transfer did not settle after READ release")), 5000);
}),
]);
assert.equal(firstResult.error, undefined, firstResult.error);
const second = await Promise.race([
secondPromise,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("second transfer did not settle after slot free")), 5000);
}),
]);
assert.equal(second.error, undefined, second.error);
assert.ok(openBCalls >= 1, "second download must use concurrent OPEN after slot free");
assert.ok(
maxInFlightB >= 2,
`second download must pipeline READs after slot free, got maxInFlightB=${maxInFlightB}`,
);
assert.equal(createReadStreamCalls, 0, "serial createReadStream must never run");
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
assert.equal(transferBridge._getSessionFastDownloadWaiterCountForTests(client), 0);
const downloadedA = await fs.promises.readFile(targetA);
const downloadedB = await fs.promises.readFile(targetB);
assert.deepEqual(downloadedA, payloadA);
assert.deepEqual(downloadedB, payloadB);
});
test("cancel while waiting for session fast download slot settles without serial stream", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-slot-wait-cancel-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payloadA = Buffer.alloc(4 * TRANSFER_CHUNK_SIZE, 7);
const payloadB = Buffer.alloc(2 * TRANSFER_CHUNK_SIZE, 9);
const pendingA = [];
let endCalls = 0;
let createReadStreamCalls = 0;
let openBCalls = 0;
const sharedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "r");
if (String(remotePath).includes("b.bin")) openBCalls += 1;
callback(null, Buffer.from(`handle:${remotePath}`));
},
read(handle, buffer, offset, length, position, callback) {
const key = handle.toString();
if (key.includes("b.bin")) {
throw new Error("second download must not READ after cancel on slot wait");
}
pendingA.push(() => {
buffer.fill(7, offset, offset + length);
callback(null, length);
});
},
close(_handle, callback) { callback(null); },
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run");
},
end() { endCalls += 1; },
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat(remotePath) {
const size = String(remotePath).includes("b.bin") ? payloadB.length : payloadA.length;
return Promise.resolve({ size, mtimeMs: 1_000, ctimeMs: 1_000, mtime: 1, ctime: 1 });
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const first = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "slot-hold-first",
sourcePath: "/root/a.bin",
targetPath: path.join(tempDir, "a.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payloadA.length,
resumable: true,
skipAdmission: true,
},
);
assert.ok(await waitUntil(() => pendingA.length >= 1, 2000));
const secondPromise = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "slot-wait-cancel-second",
sourcePath: "/root/b.bin",
targetPath: path.join(tempDir, "b.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payloadB.length,
resumable: true,
skipAdmission: true,
},
);
assert.ok(
await waitUntil(() => transferBridge._getSessionFastDownloadWaiterCountForTests(client) >= 1, 2000),
"second must wait on session slot",
);
await transferBridge.cancelTransfer(null, { transferId: "slot-wait-cancel-second" });
const second = await Promise.race([
secondPromise,
new Promise((_, reject) => setTimeout(() => reject(new Error("slot-wait cancel hung")), 3000)),
]);
assert.match(second.error || "", /cancel/i);
assert.equal(openBCalls, 0);
assert.equal(createReadStreamCalls, 0);
assert.equal(endCalls, 0);
assert.equal(transferBridge._getSessionFastDownloadWaiterCountForTests(client), 0);
// Holder still completes after stalled READs are released.
const drain = setInterval(() => {
for (const release of pendingA.splice(0)) {
try { release(); } catch { /* ignore */ }
}
}, 5);
t.after(() => clearInterval(drain));
const firstResult = await Promise.race([
first,
new Promise((_, reject) => setTimeout(() => reject(new Error("holder hung")), 5000)),
]);
assert.equal(firstResult.error, undefined, firstResult.error);
});
test("resumable concurrent download range failure fails closed (no serial stream)", async (t) => {
// Mirror upload fail-closed: when concurrent strategies fail, never complete
// via createReadStream crawl.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-download-fail-closed-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(3 * TRANSFER_CHUNK_SIZE, 41);
const targetPath = path.join(tempDir, "download.bin");
await fs.promises.writeFile(targetPath, Buffer.from("original"));
let createReadStreamCalls = 0;
let secondReadCallback = null;
const isolated = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("iso-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
if (position === TRANSFER_CHUNK_SIZE) {
secondReadCallback = callback;
return;
}
payload.copy(buffer, offset, position, position + length);
callback(null, length);
if (position === 2 * TRANSFER_CHUNK_SIZE && secondReadCallback) {
const cb = secondReadCallback;
secondReadCallback = null;
queueMicrotask(() => cb(new Error("second range failed")));
}
},
close(_handle, callback) { callback(null); },
end() {},
});
const shared = createFastSftp({
open(_remotePath, _flags, callback) {
callback(new Error("shared open also fails"));
},
read() {
throw new Error("shared READ must not run after OPEN failure");
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not complete the download");
},
});
const client = {
sftp: shared,
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
client: {
sftp(callback) { callback(null, isolated); },
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-sparse-fail-closed",
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.match(result.error || "", /pipelined download failed|second range failed|shared open also fails/i);
assert.equal(createReadStreamCalls, 0);
assert.equal(await fs.promises.readFile(targetPath, "utf8"), "original");
});
test("sudo concurrent READ failure fails closed (no createReadStream)", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-fail-closed-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(2 * TRANSFER_CHUNK_SIZE, 55);
const targetPath = path.join(tempDir, "download.bin");
let createReadStreamCalls = 0;
let endCalls = 0;
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("sudo-handle"));
},
read(_handle, _buffer, _offset, _length, _position, callback) {
setImmediate(() => callback(new Error("sudo READ rejected")));
},
close(_handle, callback) { callback(null); },
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run after sudo concurrent fail");
},
end() { endCalls += 1; },
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-sudo-fail-closed",
sourcePath: "/root/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.match(result.error || "", /pipelined download failed|sudo READ rejected/i);
assert.equal(createReadStreamCalls, 0);
assert.equal(endCalls, 0, "shared sudo channel must not be ended on fail-closed");
});
test("isolated download CLOSE timeout disposes channel instead of returning it to the pool", async (t) => {
// Codex P2 on a3b11137: success-path CLOSE timeout with disposeChannel:true
// only logged and left remoteCloseError unset, so downloadFile released the
// isolated channel back to the pool without dispose.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-isolated-close-timeout-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(2 * TRANSFER_CHUNK_SIZE, 33);
for (let index = 0; index < payload.length; index += 1) payload[index] = index % 173;
const targetPath = path.join(tempDir, "download.bin");
let openedChannels = 0;
let endCalls = 0;
let closeCalls = 0;
const makeIsolated = () => createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from(`isolated-${openedChannels}`));
},
read(_handle, buffer, offset, length, position, callback) {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
close(_handle, _callback) {
closeCalls += 1;
// Never invoke callback — simulates a hung CLOSE on an isolated channel.
},
end() {
endCalls += 1;
},
});
const client = {
sftp: createFastSftp({
createReadStream() {
throw new Error("serial stream must not run when isolated concurrent READ succeeds");
},
}),
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
client: {
sftp(callback) {
openedChannels += 1;
callback(null, makeIsolated());
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const first = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-isolated-close-timeout",
sourcePath: "/tmp/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(first.error, undefined, first.error);
assert.ok(closeCalls >= 1, "expected isolated CLOSE to be attempted");
assert.ok(endCalls >= 1, "CLOSE timeout must dispose the isolated channel");
assert.deepEqual(await fs.promises.readFile(targetPath), payload);
// A follow-up download must open a fresh isolated channel, not reuse the timed-out one.
const targetPath2 = path.join(tempDir, "download-2.bin");
const second = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-isolated-close-timeout-2",
sourcePath: "/tmp/download.bin",
targetPath: targetPath2,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(second.error, undefined, second.error);
assert.ok(openedChannels >= 2, `expected a new isolated channel after dispose, got ${openedChannels}`);
assert.deepEqual(await fs.promises.readFile(targetPath2), payload);
});
test("cancel during stalled shared sudo download OPEN settles without sftp.end()", async (t) => {
// Codex P2 on 2c369403: disposeChannel:false made pre-OPEN abort a no-op while
// openSftpHandle had no cancel reject, so cancel hung forever and held the lease.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-open-stall-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const targetPath = path.join(tempDir, "download.bin");
const fileSize = DOWNLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE;
let endCalls = 0;
let createReadStreamCalls = 0;
let closeCalls = 0;
let releaseOpen = null;
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
releaseOpen = () => callback(null, Buffer.from("late-shared-handle"));
},
read() {
throw new Error("READ must not run after cancel during OPEN");
},
close(_handle, callback) {
closeCalls += 1;
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run after cancel");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: fileSize,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const transferId = "download-sudo-open-stall-cancel";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: "/root/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: fileSize,
resumable: true,
},
);
const ready = await waitUntil(() => typeof releaseOpen === "function", 2000);
assert.ok(ready, "expected shared OPEN to stall");
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("transfer did not settle after cancel during shared OPEN")), 3000);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended on cancel");
assert.equal(createReadStreamCalls, 0, "cancel must not fall through to serial stream");
assert.ok(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"));
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:error"), false);
// Late OPEN success after cancel should close the handle, not leave it open.
releaseOpen?.();
await new Promise((resolve) => setImmediate(resolve));
assert.ok(closeCalls >= 1, `expected late shared handle close, got ${closeCalls}`);
});
test("shared sudo download OPEN rejects on channel error without hanging", async (t) => {
// Codex P2 on 50ef8fac: outer onChannelError only recorded the error for a
// post-await check. If the shared channel emitted error while sftp.open was
// pending and never invoked the OPEN callback, the transfer hung forever and
// held the SFTP lease. openSftpHandleForTransfer must reject on channel error.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-open-channel-error-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const targetPath = path.join(tempDir, "download.bin");
const fileSize = DOWNLOAD_TRANSFER_CONCURRENCY * TRANSFER_CHUNK_SIZE;
let endCalls = 0;
let createReadStreamCalls = 0;
let closeCalls = 0;
let releaseOpen = null;
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
releaseOpen = () => callback(null, Buffer.from("late-shared-handle-after-error"));
},
read() {
throw new Error("READ must not run after channel error during OPEN");
},
close(_handle, callback) {
closeCalls += 1;
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run after pipelined OPEN failure");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: fileSize,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const transferId = "download-sudo-open-channel-error";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: "/root/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: fileSize,
resumable: true,
},
);
const ready = await waitUntil(() => typeof releaseOpen === "function", 2000);
assert.ok(ready, "expected shared OPEN to stall");
sharedSftp.emit("error", new Error("shared SFTP channel died during OPEN"));
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer did not settle after shared channel error during OPEN")),
3000,
);
}),
]);
assert.ok(result.error, "expected transfer to fail after channel error");
assert.match(
result.error,
/shared SFTP channel died during OPEN|pipelined download failed/i,
);
assert.equal(endCalls, 0, "shared sudo channel must not be ended on channel error");
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), false);
assert.equal(
createReadStreamCalls,
0,
"concurrent shared OPEN failure must fail closed (no serial stream fallback)",
);
// Late OPEN success after channel-error reject should close the handle.
releaseOpen?.();
await new Promise((resolve) => setImmediate(resolve));
assert.ok(closeCalls >= 1, `expected late shared handle close, got ${closeCalls}`);
});
test("cancel during stalled shared sudo READ settles without sftp.end()", async (t) => {
// Codex P2 on d855e69c: disposeChannel:false passed forceSettleOnError:false, so
// cancel while an sftp.read callback never arrives hung forever (abortChannel is
// a no-op on shared channels). Downloads have no remote WRITEs to drain, so they
// must force-settle on cancel without ending the sudo session.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-read-stall-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const targetPath = path.join(tempDir, "download.bin");
const fileSize = 4 * TRANSFER_CHUNK_SIZE;
let endCalls = 0;
let createReadStreamCalls = 0;
let activeReads = 0;
let maxInFlight = 0;
const pendingReadCallbacks = [];
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("shared-read-handle"));
},
read(_handle, _buffer, _offset, _length, _position, callback) {
activeReads += 1;
maxInFlight = Math.max(maxInFlight, activeReads);
// Stall forever — never invoke callback (simulates dead/hung READ).
pendingReadCallbacks.push(() => {
activeReads -= 1;
callback(new Error("late READ after cancel"));
});
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run after cancel");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: fileSize,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const transferId = "download-sudo-read-stall-cancel";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: "/root/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: fileSize,
resumable: true,
},
);
const ready = await waitUntil(() => pendingReadCallbacks.length >= 1, 2000);
assert.ok(ready, "expected at least one shared READ to stall");
assert.ok(maxInFlight >= 1, `expected in-flight READ, got ${maxInFlight}`);
await transferBridge.cancelTransfer(null, { transferId });
// forceSettleOnError grace is 2s; shared cancel/fail CLOSE is fire-and-forget.
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer did not settle after cancel during stalled shared READ")),
5000,
);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended on cancel during READ");
assert.equal(createReadStreamCalls, 0, "cancel must not fall through to serial stream");
assert.ok(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"));
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:error"), false);
// Late READ callbacks after settle must not crash the process.
for (const release of pendingReadCallbacks.splice(0)) {
try { release(); } catch { /* ignore */ }
}
await new Promise((resolve) => setImmediate(resolve));
});
test("cancel during stalled shared verification READ settles without sftp.end()", async (t) => {
// Codex P2 on cba714f3: after ranged READs finish, verifyFastDownloadSamples still
// awaits bare sftp.read outside forceSettleOnError. Shared cancel cannot
// sftp.end(), so a hung verification READ would hold the transfer and lease.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-verify-stall-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(3 * TRANSFER_CHUNK_SIZE, 61);
for (let index = 0; index < payload.length; index += 1) payload[index] = index % 251;
const targetPath = path.join(tempDir, "download.bin");
let endCalls = 0;
let createReadStreamCalls = 0;
let transferredBytes = 0;
const pendingVerifyCallbacks = [];
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("shared-verify-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
// Complete the concurrent download first; stall only verification samples.
if (transferredBytes < payload.length) {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
transferredBytes += slice.length;
setImmediate(() => callback(null, slice.length));
return;
}
pendingVerifyCallbacks.push(() => {
callback(new Error("late verification READ after cancel"));
});
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run after cancel");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const transferId = "download-sudo-verify-stall-cancel";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: "/root/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
const ready = await waitUntil(() => pendingVerifyCallbacks.length >= 1, 3000);
assert.ok(ready, "expected verification READ to stall after ranged download");
await transferBridge.cancelTransfer(null, { transferId });
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer did not settle after cancel during stalled verification READ")),
5000,
);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended during verification cancel");
assert.equal(createReadStreamCalls, 0, "cancel must not fall through to serial stream");
assert.ok(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"));
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:error"), false);
for (const release of pendingVerifyCallbacks.splice(0)) {
try { release(); } catch { /* ignore */ }
}
await new Promise((resolve) => setImmediate(resolve));
});
test("cancel during responsive last verification sample reports cancelled", async (t) => {
// Codex P2 on 74ae5e0a: cancel during the last sample can let the READ win the
// race before the 2s force-settle fires. Without a post-race cancelled check,
// verification returns successfully and sendComplete() reports done.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-verify-race-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(3 * TRANSFER_CHUNK_SIZE, 67);
for (let index = 0; index < payload.length; index += 1) payload[index] = index % 251;
const targetPath = path.join(tempDir, "download.bin");
let endCalls = 0;
let transferredBytes = 0;
let verifyReadCount = 0;
let releaseLastVerifyRead = null;
const lastVerifyReadHeld = new Promise((resolve) => {
releaseLastVerifyRead = resolve;
});
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("shared-verify-race-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
if (transferredBytes < payload.length) {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
transferredBytes += slice.length;
setImmediate(() => callback(null, slice.length));
return;
}
verifyReadCount += 1;
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
// Hold only the last sample briefly so cancel can land mid-READ, then
// complete successfully (responsive server — force-settle never fires).
if (verifyReadCount >= 3) {
lastVerifyReadHeld.then(() => {
setImmediate(() => callback(null, slice.length));
});
return;
}
setImmediate(() => callback(null, slice.length));
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
throw new Error("serial createReadStream must not run");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const transferId = "download-sudo-verify-race-cancel";
const running = transferBridge.startTransfer(
{ sender },
{
transferId,
sourcePath: "/root/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
const ready = await waitUntil(() => verifyReadCount >= 3, 3000);
assert.ok(ready, "expected last verification sample to be held");
await transferBridge.cancelTransfer(null, { transferId });
releaseLastVerifyRead();
const result = await Promise.race([
running,
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer did not settle after cancel during last verification sample")),
3000,
);
}),
]);
assert.match(result.error || "", /cancel/i);
assert.equal(endCalls, 0, "shared sudo channel must not be ended");
assert.ok(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"));
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:error"), false);
});
test("shared verification channel error between samples fails without hanging", async (t) => {
// Codex P2 on 14783168: channel error between sample races (after one sample
// resolves, before the next rejectPending is installed) was dropped; the next
// readSftpRange could hang forever on a dead shared channel.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-verify-gap-error-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(3 * TRANSFER_CHUNK_SIZE, 71);
for (let index = 0; index < payload.length; index += 1) payload[index] = index % 251;
const targetPath = path.join(tempDir, "download.bin");
let endCalls = 0;
let transferredBytes = 0;
let verifyReadCount = 0;
let sharedSftp = null;
const pendingSecondVerify = [];
sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("shared-verify-gap-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
if (transferredBytes < payload.length) {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
transferredBytes += slice.length;
setImmediate(() => callback(null, slice.length));
return;
}
verifyReadCount += 1;
if (verifyReadCount === 1) {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => {
callback(null, slice.length);
// Emit after the first sample resolves and before the next race installs
// rejectPending — the gap Codex identified.
setImmediate(() => {
sharedSftp.emit("error", new Error("shared channel died between samples"));
});
});
return;
}
// Subsequent verification READs never callback (dead channel).
pendingSecondVerify.push(callback);
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
throw new Error("serial createReadStream must not run after channel error");
},
end() {
endCalls += 1;
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const result = await Promise.race([
transferBridge.startTransfer(
{ sender },
{
transferId: "download-sudo-verify-gap-error",
sourcePath: "/root/download.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
),
new Promise((_, reject) => {
setTimeout(
() => reject(new Error("transfer hung after shared verification channel error between samples")),
4000,
);
}),
]);
// Concurrent path already staged the full file, so fallback may complete via
// the checkpoint>=fileSize short-circuit — either outcome is fine as long as
// we settle instead of hanging on the dead-channel READ.
assert.ok(result, "expected transfer to settle after inter-sample channel error");
assert.equal(endCalls, 0, "shared sudo channel must not be ended on verification error");
assert.ok(
sender.sent.some((entry) => (
entry.channel === "netcatty:transfer:complete"
|| entry.channel === "netcatty:transfer:error"
|| entry.channel === "netcatty:transfer:cancelled"
)),
"expected a terminal transfer event after channel error between samples",
);
});
test("shared upload errors wait for all in-flight WRITEs before returning", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-shared-error-drain-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const fileSize = 4 * TRANSFER_CHUNK_SIZE;
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, Buffer.alloc(fileSize, 13));
const pendingWriteCallbacks = [];
let triggerFirstError;
const firstErrorTriggered = new Promise((resolve) => {
triggerFirstError = resolve;
});
let firstErrorScheduled = false;
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "w");
callback(null, Buffer.from("shared-handle"));
},
write(_handle, _buffer, _offset, _length, _position, callback) {
pendingWriteCallbacks.push(callback);
if (!firstErrorScheduled && pendingWriteCallbacks.length === 4) {
firstErrorScheduled = true;
setImmediate(() => {
const fail = pendingWriteCallbacks.shift();
fail(new Error("shared WRITE failed"));
triggerFirstError();
});
}
},
close(_handle, callback) {
callback(null);
},
});
const client = {
__netcattySudoMode: true,
sftp: sharedSftp,
stat() {
return Promise.resolve({ size: 0 });
},
delete() {
return Promise.resolve();
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
let transferSettled = false;
const running = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "upload-shared-error-drain",
sourcePath: localPath,
targetPath: "/tmp/upload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: fileSize,
resumable: true,
},
).finally(() => {
transferSettled = true;
});
await firstErrorTriggered;
await new Promise((resolve) => setTimeout(resolve, 2100));
const settledBeforeRemainingWrites = transferSettled;
for (const callback of pendingWriteCallbacks.splice(0)) callback(null);
const result = await running;
assert.equal(settledBeforeRemainingWrites, false);
assert.match(result.error || "", /shared WRITE failed|pipelined upload failed/i);
});
test("sudo SFTP sessions without open/write fail closed (no serial WriteStream)", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sudo-fail-closed-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const localPath = path.join(tempDir, "payload.bin");
const payload = Buffer.alloc(64 * 1024, 7);
await fs.promises.writeFile(localPath, payload);
let createWriteStreamCalls = 0;
const streamOnlySftp = createFastSftp({
createWriteStream() {
createWriteStreamCalls += 1;
throw new Error("serial WriteStream must not run");
},
});
const client = {
// Isolated channel skipped; shared sftp has only createWriteStream.
__netcattySudoMode: true,
sftp: streamOnlySftp,
stat() {
return Promise.resolve({ size: 0 });
},
rename() {
return Promise.resolve();
},
delete() {
return Promise.resolve();
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const result = await transferBridge.startTransfer(
{ sender },
{
transferId: "upload-sudo-fail-closed",
sourcePath: localPath,
targetPath: "/tmp/payload.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
},
);
assert.match(result.error || "", /pipelined upload failed|open\/write missing/i);
assert.equal(createWriteStreamCalls, 0);
assert.ok(sender.sent.some((entry) => entry.channel === "netcatty:transfer:error"));
});
test("resumable SFTP downloads preserve a 2MB request window on high-latency paths", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-test-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(4 * 1024 * 1024, 7);
let activeReads = 0;
let observedConcurrency = 0;
let observedChunkSize = 0;
const fastSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("remote-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
activeReads += 1;
observedConcurrency = Math.max(observedConcurrency, activeReads);
observedChunkSize = Math.max(observedChunkSize, length);
payload.copy(buffer, offset, position, position + length);
setImmediate(() => {
activeReads -= 1;
callback(null, length, buffer, position);
});
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({
createReadStream() {
return Readable.from(payload);
},
}),
stat(_path) {
return Promise.resolve({ size: payload.length });
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const result = await transferBridge.startTransfer(
{ sender },
{
transferId: "download-large",
sourcePath: "/tmp/large.bin",
targetPath: path.join(tempDir, "large.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
resumable: true,
},
);
assert.equal(result.error, undefined);
assert.equal(observedChunkSize, 32 * 1024);
assert.equal(observedConcurrency * observedChunkSize, 2 * 1024 * 1024);
assert.deepEqual(await fs.promises.readFile(path.join(tempDir, "large.bin")), payload);
});
function runIsolatedStagedDownloadRecovery(mode) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), `netcatty-transfer-root-rebind-${mode}-`));
const home = path.join(root, "home");
fs.mkdirSync(home);
try {
const script = `
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { EventEmitter } = require("node:events");
const transferBridge = require("./electron/bridges/transferBridge.cjs");
const tempDirBridge = require("./electron/bridges/tempDirBridge.cjs");
const mode = process.env.NETCATTY_TEST_REBIND_MODE;
const transferId = mode === "after-open"
? "download-root-rebind-after-open"
: "download-root-rebind-before-write";
const payload = Buffer.from(
mode === "after-open" ? "recovered after stage open" : "recovered staged download",
);
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), "target-"));
const targetPath = path.join(targetDir, "download.bin");
const initialStagedPath = tempDirBridge.getTransferTempFilePath(transferId, "download.bin");
const tempRoot = path.dirname(initialStagedPath);
const initialGeneration = tempDirBridge.getTempDirRebindGeneration();
let triggered = false;
const originalGetTransferTempFilePath = tempDirBridge.getTransferTempFilePath;
const originalOpen = fs.promises.open;
if (mode === "after-open") {
fs.promises.open = async (filePath, ...args) => {
const opened = await originalOpen(filePath, ...args);
if (!triggered && path.resolve(filePath) === path.resolve(initialStagedPath)) {
triggered = true;
fs.renameSync(tempRoot, tempRoot + ".after-open-old");
fs.mkdirSync(tempRoot, { recursive: true, mode: 0o700 });
}
return opened;
};
} else {
tempDirBridge.getTransferTempFilePath = (id, fileName) => {
const stagedPath = originalGetTransferTempFilePath(id, fileName);
if (!triggered) {
triggered = true;
fs.rmSync(tempRoot, { recursive: true, force: true });
}
return stagedPath;
};
}
const sftp = new EventEmitter();
sftp.open = (_remotePath, flags, callback) => {
if (typeof flags === "function") callback = flags;
callback(null, Buffer.from("read-handle"));
};
sftp.read = (_handle, buffer, offset, length, position, callback) => {
const end = Math.min(position + length, payload.length);
payload.copy(buffer, offset, position, end);
setImmediate(() => callback(null, end - position));
};
sftp.close = (_handle, callback) => callback(null);
sftp.readdir = (_remotePath, callback) => callback(null, []);
sftp.stat = (_remotePath, callback) => callback(null, { size: payload.length });
sftp.lstat = (_remotePath, callback) => {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
};
sftp.mkdir = (_remotePath, callback) => callback(null);
sftp.unlink = (_remotePath, callback) => callback(null);
sftp.end = () => {};
const client = {
sftp,
stat: () => Promise.resolve({ size: payload.length }),
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
(async () => {
const result = await transferBridge.startTransfer(
{ sender: { send() {} } },
{
transferId,
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
const content = fs.existsSync(targetPath)
? await fs.promises.readFile(targetPath, "utf8")
: null;
console.log("RESULT:" + JSON.stringify({
error: result.error || null,
content,
triggered,
tempRoot,
generation: tempDirBridge.getTempDirRebindGeneration(),
initialGeneration,
}));
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
`;
const result = spawnSync(process.execPath, ["-e", script], {
cwd: path.resolve(__dirname, "../.."),
env: {
...process.env,
TMPDIR: root,
TEMP: root,
TMP: root,
HOME: home,
NETCATTY_TEST_REBIND_MODE: mode,
},
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr || result.stdout);
const match = result.stdout.match(/^RESULT:(.+)$/m);
assert.ok(match, result.stdout);
const report = JSON.parse(match[1]);
assert.equal(path.dirname(report.tempRoot), root);
return report;
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
test("staged SFTP downloads recover when the temp root disappears before the first write", () => {
const result = runIsolatedStagedDownloadRecovery("before-write");
assert.equal(result.error, null);
assert.equal(result.triggered, true);
assert.equal(result.generation, result.initialGeneration + 1);
assert.equal(result.content, "recovered staged download");
});
test("staged SFTP downloads recover when the temp root is replaced after opening the stage", () => {
const result = runIsolatedStagedDownloadRecovery("after-open");
assert.equal(result.error, null);
assert.equal(result.triggered, true);
assert.equal(result.generation, result.initialGeneration + 1);
assert.equal(result.content, "recovered after stage open");
});
test("fast resumable downloads pause only at a complete checkpoint", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-fast-pause-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(4 * 1024 * 1024, 11);
const pendingReads = [];
let holdReads = true;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const complete = () => {
payload.copy(buffer, offset, position, position + length);
callback(null, length, buffer, position);
};
if (holdReads) pendingReads.push(complete);
else setImmediate(complete);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({
createReadStream() {
return Readable.from(payload);
},
}),
stat() {
return Promise.resolve({ size: payload.length });
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const targetPath = path.join(tempDir, "large.bin");
const running = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-fast-paused",
sourcePath: "/tmp/large.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
const readyDeadline = Date.now() + 1000;
while (pendingReads.length < 64 && Date.now() < readyDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(pendingReads.length, 64);
const originalStat = fs.promises.stat;
fs.promises.stat = async (filePath) => {
if (String(filePath).includes("download-fast-paused")) {
throw new Error("fast range checkpoints must not be inferred from file size");
}
return originalStat(filePath);
};
const pausing = transferBridge.pauseTransfer(null, { transferId: "download-fast-paused" });
holdReads = false;
for (const complete of pendingReads.splice(0)) complete();
let paused;
try {
paused = await pausing;
} finally {
fs.promises.stat = originalStat;
}
assert.equal(paused.success, true);
assert.equal(paused.checkpointBytes, 2 * 1024 * 1024);
{
const resumed = await transferBridge.resumeTransfer(null, { transferId: "download-fast-paused" });
assert.equal(resumed.success, true);
assert.ok(Number.isFinite(resumed.lifecycleEpoch));
}
assert.equal((await running).error, undefined);
assert.deepEqual(await fs.promises.readFile(targetPath), payload);
});
test("fast resumable downloads fall back from the highest contiguous checkpoint", async (t) => {
// Isolated concurrent ranges fail mid-file; concurrent-shared resumes from the
// highest contiguous checkpoint (no serial createReadStream fallback).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-contiguous-fallback-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(3 * 32 * 1024, 17);
let sharedOpenCheckpoint = null;
let secondReadCallback = null;
let targetPath = null;
let stagedPath = null;
const progress = [];
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
if (position === 32 * 1024) {
secondReadCallback = callback;
return;
}
payload.copy(buffer, offset, position, position + length);
callback(null, length, buffer, position);
if (position === 2 * 32 * 1024) {
queueMicrotask(() => secondReadCallback(new Error("second range failed")));
}
},
close(_handle, callback) {
callback(null);
},
});
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
try {
sharedOpenCheckpoint = fs.statSync(stagedPath).size;
} catch {
sharedOpenCheckpoint = -1;
}
callback(null, Buffer.from("shared-read-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
throw new Error("serial createReadStream must not run after isolated range failure");
},
});
const client = {
sftp: sharedSftp,
stat() {
return Promise.resolve({ size: payload.length });
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
targetPath = path.join(tempDir, "fallback.bin");
stagedPath = tempDirBridge.getTransferTempFilePath(
"download-contiguous-fallback",
path.basename(targetPath),
);
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-contiguous-fallback",
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
(transferred) => progress.push(transferred),
);
assert.equal(result.error, undefined, result.error);
assert.equal(sharedOpenCheckpoint, 32 * 1024);
const firstPastCheckpoint = progress.findIndex((transferred) => transferred > sharedOpenCheckpoint);
assert.ok(firstPastCheckpoint >= 0);
assert.ok(progress.slice(firstPastCheckpoint + 1).includes(sharedOpenCheckpoint));
assert.deepEqual(await fs.promises.readFile(targetPath), payload);
});
test("resumable download fallback rejects a remote source changed during concurrent-shared", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-download-fallback-change-"));
const transferId = "download-fallback-change";
const targetPath = path.join(tempDir, "download.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagedPath, { force: true });
});
await fs.promises.writeFile(targetPath, "original");
const payload = Buffer.alloc(32 * 1024, 51);
let sourceChanged = false;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
read(_handle, _buffer, _offset, _length, _position, callback) {
callback(new Error("range download unavailable"));
},
});
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
sourceChanged = true;
callback(null, Buffer.from("shared-read-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
throw new Error("serial createReadStream must not run");
},
});
const client = {
sftp: sharedSftp,
stat() {
return Promise.resolve({
size: payload.length,
mtimeMs: sourceChanged ? 2 : 1,
ctimeMs: sourceChanged ? 2 : 1,
});
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(sourceChanged, true);
assert.match(result.error || "", /source.*changed/i);
assert.equal(await fs.promises.readFile(targetPath, "utf8"), "original");
assert.equal(fs.existsSync(stagedPath), false);
});
test("range-failure fallback truncates sparse local tail before concurrent-shared", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-sparse-tail-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
// Two chunks: finish the second first, then fail the first so contiguous stays 0
// while the local file already has a sparse tail past the durable checkpoint.
const chunk = 32 * 1024;
const payload = Buffer.alloc(2 * chunk, 17);
let firstReadCallback = null;
let sizeAtSharedOpen = null;
const transferId = "download-sparse-tail";
const targetPath = path.join(tempDir, "sparse.bin");
// Resumable downloads stage under the transfer temp path, not the final target.
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
if (position === 0) {
firstReadCallback = () => callback(new Error("first range failed"));
// Complete the later range first, then fail the first.
queueMicrotask(() => {
// second range is already in flight separately
});
return;
}
payload.copy(buffer, offset, position, position + length);
callback(null, length, buffer, position);
queueMicrotask(() => firstReadCallback?.());
},
close(_handle, callback) {
callback(null);
},
});
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
// Capture *staged* size when concurrent-shared opens (post-truncate).
try {
sizeAtSharedOpen = fs.statSync(stagedPath).size;
} catch {
sizeAtSharedOpen = -1;
}
callback(null, Buffer.from("shared-read-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
throw new Error("serial createReadStream must not run after range failure");
},
});
const client = {
sftp: sharedSftp,
stat() {
return Promise.resolve({ size: payload.length });
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(result.error, undefined, result.error);
// Contiguous checkpoint never advanced past 0; sparse tail must be truncated
// on the staged .part before concurrent-shared resumes (final target is only
// written at promote time).
assert.equal(sizeAtSharedOpen, 0);
assert.deepEqual(await fs.promises.readFile(targetPath), payload);
});
test("S2S upload-phase concurrent failure does not truncate the complete local temp source", async (t) => {
const transferId = `s2s-no-truncate-${crypto.randomUUID()}`;
const payload = Buffer.alloc(64 * 1024, 23);
const localStage = tempDirBridge.getTransferTempFilePath(transferId, "payload.bin");
await fs.promises.writeFile(localStage, payload);
t.after(async () => {
await fs.promises.unlink(localStage).catch(() => {});
});
let sizeAfterFail = null;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, _length, position, callback) {
if (position === 0) {
callback(new Error("first upload range failed"));
return;
}
callback(null);
},
close(_handle, callback) {
callback(null);
},
end() {},
});
const sourceClient = {
sftp: createFastSftp({}),
stat: async () => ({ size: payload.length }),
};
const targetClient = {
sftp: createFastSftp({
open(_remotePath, _flags, callback) {
callback(new Error("shared open also fails"));
},
createWriteStream() {
throw new Error("serial WriteStream must not run");
},
}),
stat: async () => ({ size: 0 }),
rename: async () => {},
delete: async () => {},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({
sftpClients: new Map([
["source", sourceClient],
["target", targetClient],
]),
});
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/source/payload.bin",
targetPath: "/target/payload.bin",
sourceType: "sftp",
targetType: "sftp",
sourceSftpId: "source",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
resumeStage: "upload",
downloadCheckpointBytes: payload.length,
uploadCheckpointBytes: 0,
checkpointBytes: 0,
},
);
try {
sizeAfterFail = fs.statSync(localStage).size;
} catch {
sizeAfterFail = -1;
}
// Fail closed — local S2S temp is the fully-downloaded source, never truncated.
assert.match(result.error || "", /pipelined upload failed|first upload range failed/i);
assert.equal(sizeAfterFail, payload.length);
});
test("cancelled fast resumable downloads release their isolated channel", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-cancel-release-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE * 2, 19);
let openedChannels = 0;
let fallbackReads = 0;
const pendingReads = [];
let lateReadScheduled = false;
const firstChannel = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("first-handle"));
},
read(_handle, _buffer, _offset, _length, _position, callback) {
pendingReads.push(callback);
},
end() {
if (lateReadScheduled) return;
lateReadScheduled = true;
setTimeout(() => {
pendingReads.shift()?.(new Error("late channel cancellation"));
}, 1500);
},
});
const secondChannel = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("second-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
payload.copy(buffer, offset, position, position + length);
callback(null, length, buffer, position);
},
close(_handle, callback) {
callback(null);
},
});
const client = {
sftp: createFastSftp({
createReadStream(_remotePath, options) {
if (options?.start !== undefined) fallbackReads += 1;
return Readable.from(payload);
},
}),
stat() {
return Promise.resolve({ size: payload.length });
},
client: {
sftp(callback) {
openedChannels += 1;
callback(null, openedChannels === 1 ? firstChannel : secondChannel);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const first = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-cancel-release-first",
sourcePath: "/tmp/source.bin",
targetPath: path.join(tempDir, "first.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
const readyDeadline = Date.now() + 1000;
while (pendingReads.length < 2 && Date.now() < readyDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(pendingReads.length, 2);
const cancelledAt = Date.now();
await transferBridge.cancelTransfer(null, { transferId: "download-cancel-release-first" });
assert.equal((await first).error, "Transfer cancelled");
const cancelDuration = Date.now() - cancelledAt;
assert.ok(cancelDuration >= 1900);
assert.ok(cancelDuration < 3000);
const second = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-cancel-release-second",
sourcePath: "/tmp/source.bin",
targetPath: path.join(tempDir, "second.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
resumable: true,
},
);
assert.equal(second.error, undefined);
assert.equal(openedChannels, 2);
assert.equal(fallbackReads, 0);
});
test("SFTP downloads fall back to concurrent shared READ after fastGet fails", async (t) => {
// Non-resumable fastGet failure must not crawl via serial createReadStream;
// the next pipelined strategy is concurrent-shared READ on the browse channel.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-fallback-test-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const expected = Buffer.from("complete fallback download");
let fastGetAttempts = 0;
let createReadStreamCalls = 0;
let maxInFlight = 0;
let activeReads = 0;
const fastSftp = createFastSftp({
fastGet(_remotePath, localPath, _options, done) {
fastGetAttempts += 1;
fs.promises.writeFile(localPath, "partial").then(
() => done(new Error("server rejected concurrent reads")),
done,
);
},
});
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("shared-read-handle"));
},
read(_handle, buffer, offset, length, position, callback) {
activeReads += 1;
maxInFlight = Math.max(maxInFlight, activeReads);
const slice = expected.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => {
activeReads -= 1;
callback(null, slice.length);
});
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run after fastGet failure");
},
});
const client = {
sftp: sharedSftp,
stat() {
return Promise.resolve({ size: expected.length });
},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const targetPath = path.join(tempDir, "fallback.bin");
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-fallback",
sourcePath: "/tmp/fallback.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: expected.length,
},
);
assert.equal(result.error, undefined, result.error);
assert.equal(fastGetAttempts, 1);
assert.equal(createReadStreamCalls, 0);
assert.ok(maxInFlight >= 1, "expected concurrent shared READ after fastGet failure");
assert.deepEqual(await fs.promises.readFile(targetPath), expected);
});
test("SFTP downloads serialize concurrent files on the session fast-path slot", async (t) => {
// FAST_DOWNLOAD_CHANNELS_PER_SESSION caps concurrent 64-READ fanout to one
// file per session. A second download waits for the slot (pipelined path)
// instead of crawling via serial createReadStream (#2719 / #1507).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-budget-test-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const completions = [];
let activeFastGets = 0;
let maxActiveFastGets = 0;
let openedChannels = 0;
let createReadStreamCalls = 0;
// Shared browse channel has open/read so a slot-waiter can still pipeline if
// the isolated pool is empty after the first channel is disposed. For this
// test both files complete via isolated fastGet after serializing on the slot.
const sharedSftp = createFastSftp({
open(_remotePath, flags, callback) {
assert.equal(flags, "r");
callback(null, Buffer.from("shared-read-handle"));
},
read(_handle, buffer, offset, length, _position, callback) {
buffer.fill(0x64, offset, offset + length);
setImmediate(() => callback(null, length));
},
close(_handle, callback) {
callback(null);
},
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not run under session slot wait");
},
});
const client = {
sftp: sharedSftp,
stat() {
return Promise.resolve({ size: 10 });
},
client: {
sftp(callback) {
openedChannels += 1;
// Fresh channel per open so the second transfer can acquire isolated
// after the first returns its channel to the pool (or opens again).
const channel = createFastSftp({
fastGet(_remotePath, localPath, _options, done) {
activeFastGets += 1;
maxActiveFastGets = Math.max(maxActiveFastGets, activeFastGets);
completions.push(async () => {
await fs.promises.writeFile(localPath, "downloaded");
activeFastGets -= 1;
done();
});
},
});
callback(null, channel);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const start = (id) => transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: id,
sourcePath: `/tmp/${id}.bin`,
targetPath: path.join(tempDir, `${id}.bin`),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 10,
// Isolate the fast-channel budget test from global file admission.
skipAdmission: true,
},
);
const first = start("download-one");
const firstDeadline = Date.now() + 1000;
while (completions.length < 1 && Date.now() < firstDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(completions.length, 1);
assert.equal(openedChannels, 1);
const secondPromise = start("download-two");
// Second must wait on the session slot — no second fastGet yet, no serial stream.
assert.ok(
await waitUntil(() => transferBridge._getSessionFastDownloadWaiterCountForTests(client) >= 1, 2000),
"second download must enqueue on the session fast slot",
);
assert.equal(completions.length, 1, "second download must wait for session fast slot");
assert.equal(createReadStreamCalls, 0);
await completions[0]();
assert.equal((await first).error, undefined);
// After first releases the slot, second proceeds on the pipelined path.
const secondDeadline = Date.now() + 1000;
while (completions.length < 2 && Date.now() < secondDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(completions.length, 2, "second download should start after slot free");
await completions[1]();
const secondResult = await secondPromise;
assert.equal(secondResult.error, undefined, secondResult.error);
assert.equal(createReadStreamCalls, 0, "serial createReadStream must never run");
assert.equal(maxActiveFastGets, 1, "only one concurrent fastGet at a time");
});
test("idle fast-download channels are discarded when a delayed error arrives", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-idle-error-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.from("downloaded");
const channels = [];
let endedChannels = 0;
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: payload.length });
},
client: {
sftp(callback) {
const channel = createFastSftp({
fastGet(_remotePath, localPath, _options, done) {
fs.promises.writeFile(localPath, payload).then(() => done(), done);
},
end() {
endedChannels += 1;
},
});
channels.push(channel);
callback(null, channel);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const start = (id) => transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: id,
sourcePath: `/tmp/${id}.bin`,
targetPath: path.join(tempDir, `${id}.bin`),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: payload.length,
},
);
assert.equal((await start("idle-error-first")).error, undefined);
assert.equal(channels.length, 1);
assert.ok(channels[0].listenerCount("error") > 0);
channels[0].emit("error", new Error("delayed idle failure"));
assert.equal(endedChannels, 1);
assert.equal((await start("idle-error-second")).error, undefined);
assert.equal(channels.length, 2);
channels[1].emit("error", new Error("test cleanup"));
});
test("SFTP downloads cancelled while opening do not block the session", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-open-cancel-test-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
let delayedOpen = null;
let abandonedChannelClosed = false;
let openCalls = 0;
let physicalEndCalls = 0;
let physicalDestroyCalls = 0;
const abandonedSftp = createFastSftp({
end() {
abandonedChannelClosed = true;
},
});
const workingSftp = createFastSftp({
fastGet(_remotePath, localPath, _options, done) {
fs.promises.writeFile(localPath, "downloaded").then(
() => done(),
done,
);
},
});
const client = {
sftp: createFastSftp({}),
stat() {
return Promise.resolve({ size: 10 });
},
client: {
end() { physicalEndCalls++; },
destroy() { physicalDestroyCalls++; },
sftp(callback) {
openCalls += 1;
if (openCalls === 1) {
delayedOpen = callback;
} else {
callback(null, workingSftp);
}
},
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const start = (id) => transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: id,
sourcePath: `/tmp/${id}.bin`,
targetPath: path.join(tempDir, `${id}.bin`),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 10,
},
);
const cancelledPromise = start("download-opening");
const deadline = Date.now() + 1000;
while (!delayedOpen && Date.now() < deadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(typeof delayedOpen, "function");
await transferBridge.cancelTransfer(null, { transferId: "download-opening" });
delayedOpen(null, abandonedSftp);
const cancelled = await cancelledPromise;
assert.equal(cancelled.error, "Transfer cancelled");
assert.equal(abandonedChannelClosed, true);
assert.equal(physicalEndCalls, 0, "cancelling one download must preserve the shared SSH connection");
assert.equal(physicalDestroyCalls, 0, "terminal and browsing channels must not be destroyed");
const next = await Promise.race([
start("download-after-cancel"),
new Promise((_, reject) => setTimeout(() => reject(new Error("next download remained blocked")), 1000)),
]);
assert.equal(next.error, undefined);
assert.equal(openCalls, 2);
});
test("shared concurrent downloads fail closed when open/read are missing (no createReadStream)", async (t) => {
// Code-level lock: bulk download must not reintroduce serial createReadStream.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-no-stream-body-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
let createReadStreamCalls = 0;
const sftp = createFastSftp({
// Intentionally no open/read — pipelined path unavailable.
createReadStream() {
createReadStreamCalls += 1;
throw new Error("serial createReadStream must not be used for bulk SFTP download");
},
});
const client = {
sftp,
stat() { return Promise.resolve({ size: 6 }); },
client: { sftp(callback) { callback(new Error("isolated channel unavailable")); } },
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId: "download-no-stream-body",
sourcePath: "/tmp/source.bin",
targetPath: path.join(tempDir, "target.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 6,
resumable: true,
},
);
assert.match(result.error || "", /pipelined download failed|open\/read missing/i);
assert.equal(createReadStreamCalls, 0, "bulk path must never call createReadStream");
});
test("stream local-copy pause survives write-stream drain without auto-resuming the pipe", async (t) => {
// Regression for transfer-list pause (#2458): Node's .pipe() resumes the source
// on destination 'drain'. SFTP upload no longer uses serial WriteStream; cover
// unpipe on local→local copies (same pauseTransfer path). Mock write stream
// still persists bytes so pause checkpoint stat succeeds.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-pipe-pause-local-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(256 * 1024, 91);
const sourcePath = path.join(tempDir, "source.bin");
const targetPath = path.join(tempDir, "target.bin");
await fs.promises.writeFile(sourcePath, payload);
let durableBytes = 0;
const pendingWriteCallbacks = [];
let holdWrites = true;
let activeWriteStream = null;
const originalCreateWriteStream = fs.createWriteStream;
t.after(() => {
fs.createWriteStream = originalCreateWriteStream;
});
fs.createWriteStream = (filePath, options) => {
const real = originalCreateWriteStream(filePath, options);
activeWriteStream = new Writable({
highWaterMark: 16,
write(chunk, encoding, callback) {
real.write(chunk, encoding, (err) => {
if (err) return callback(err);
durableBytes += chunk.length;
activeWriteStream.bytesWritten = durableBytes;
if (holdWrites) {
pendingWriteCallbacks.push(callback);
return;
}
callback();
});
},
final(callback) {
real.end(callback);
},
destroy(err, callback) {
real.destroy(err);
callback(err);
},
});
activeWriteStream.bytesWritten = 0;
return activeWriteStream;
};
transferBridge.init({ sftpClients: new Map() });
const sender = createSender();
const running = transferBridge.startTransfer(
{ sender },
{
transferId: "local-pipe-pause",
sourcePath,
targetPath,
sourceType: "local",
targetType: "local",
totalBytes: payload.length,
resumable: true,
},
);
const backpressureDeadline = Date.now() + 2000;
while (pendingWriteCallbacks.length === 0 && Date.now() < backpressureDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.ok(pendingWriteCallbacks.length > 0, "write stream should be backpressured");
const bytesWhenPauseRequested = durableBytes;
const pausing = transferBridge.pauseTransfer(null, { transferId: "local-pipe-pause" });
holdWrites = false;
for (const callback of pendingWriteCallbacks.splice(0)) callback();
const paused = await pausing;
assert.equal(paused.success, true, paused.reason);
const checkpoint = paused.checkpointBytes;
assert.ok(checkpoint >= bytesWhenPauseRequested);
assert.ok(
checkpoint < payload.length,
"pause must stop before the copy finishes under backpressure",
);
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(
durableBytes,
checkpoint,
"paused stream copy must not keep writing after drain",
);
{
const resumed = await transferBridge.resumeTransfer(null, { transferId: "local-pipe-pause" });
assert.equal(resumed.success, true);
assert.ok(Number.isFinite(resumed.lifecycleEpoch));
}
assert.equal((await running).error, undefined);
assert.equal(durableBytes, payload.length);
});
test("repeated resume does not double-pipe the same stream pair", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-double-resume-local-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const payload = Buffer.alloc(256 * 1024, 77);
const sourcePath = path.join(tempDir, "source.bin");
const targetPath = path.join(tempDir, "target.bin");
await fs.promises.writeFile(sourcePath, payload);
let durableBytes = 0;
const pendingWriteCallbacks = [];
let holdWrites = true;
let pipeCount = 0;
let activeWriteStream = null;
const originalCreateWriteStream = fs.createWriteStream;
t.after(() => {
fs.createWriteStream = originalCreateWriteStream;
});
fs.createWriteStream = (filePath, options) => {
const real = originalCreateWriteStream(filePath, options);
activeWriteStream = new Writable({
highWaterMark: 16,
write(chunk, encoding, callback) {
real.write(chunk, encoding, (err) => {
if (err) return callback(err);
durableBytes += chunk.length;
activeWriteStream.bytesWritten = durableBytes;
if (holdWrites) {
pendingWriteCallbacks.push(callback);
return;
}
callback();
});
},
final(callback) {
real.end(callback);
},
destroy(err, callback) {
real.destroy(err);
callback(err);
},
});
activeWriteStream.bytesWritten = 0;
return activeWriteStream;
};
const originalReadablePipe = Readable.prototype.pipe;
t.after(() => {
Readable.prototype.pipe = originalReadablePipe;
});
Readable.prototype.pipe = function patchedPipe(dest, ...args) {
if (dest === activeWriteStream) pipeCount += 1;
return originalReadablePipe.apply(this, [dest, ...args]);
};
transferBridge.init({ sftpClients: new Map() });
const sender = createSender();
const running = transferBridge.startTransfer(
{ sender },
{
transferId: "local-double-resume",
sourcePath,
targetPath,
sourceType: "local",
targetType: "local",
totalBytes: payload.length,
resumable: true,
},
);
const backpressureDeadline = Date.now() + 2000;
while (pendingWriteCallbacks.length === 0 && Date.now() < backpressureDeadline) {
await new Promise((resolve) => setImmediate(resolve));
}
assert.ok(pendingWriteCallbacks.length > 0, "write stream should be backpressured");
const pausing = transferBridge.pauseTransfer(null, { transferId: "local-double-resume" });
holdWrites = false;
for (const callback of pendingWriteCallbacks.splice(0)) callback();
const paused = await pausing;
assert.equal(paused.success, true, paused.reason);
const pipesAfterPause = pipeCount;
{
const resumed = await transferBridge.resumeTransfer(null, { transferId: "local-double-resume" });
assert.equal(resumed.success, true);
assert.ok(Number.isFinite(resumed.lifecycleEpoch));
}
{
const resumed = await transferBridge.resumeTransfer(null, { transferId: "local-double-resume" });
assert.equal(resumed.success, true);
assert.ok(Number.isFinite(resumed.lifecycleEpoch));
}
assert.equal(
pipeCount,
pipesAfterPause + 1,
"second resume must not call pipe() again on the same pair",
);
assert.equal((await running).error, undefined);
assert.equal(durableBytes, payload.length);
});
test("resumable downloads never promote a partial staged file", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-partial-test-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const targetPath = path.join(tempDir, "target.bin");
await fs.promises.writeFile(targetPath, Buffer.from("original"));
// Remote pretends size=6 but EOF after 3 bytes — concurrent READ must fail closed
// without promoting a partial stage over the existing target.
const partial = Buffer.from("abc");
const { sftp } = createPipelinedDownloadSftp(partial, {
read(_handle, buffer, offset, length, position, callback) {
if (position >= partial.length) {
setImmediate(() => callback(null, 0));
return;
}
const slice = partial.subarray(position, Math.min(position + length, partial.length));
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
});
const client = {
sftp,
stat() { return Promise.resolve({ size: 6 }); },
client: { sftp(callback) { callback(new Error("isolated channel unavailable")); } },
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const sender = createSender();
const running = transferBridge.startTransfer(
{ sender },
{
transferId: "download-partial",
sourcePath: "/tmp/source.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 6,
resumable: true,
},
);
const result = await running;
assert.match(result.error || "", /full source|size mismatch|pipelined download failed/i);
assert.equal(await fs.promises.readFile(targetPath, "utf8"), "original");
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("old-style transfers explicitly reject pause", async () => {
transferBridge.init({ sftpClients: new Map() });
assert.deepEqual(
await transferBridge.pauseTransfer(null, { transferId: "missing" }),
{ success: false, reason: "Transfer is no longer active" },
);
});
test("server-to-server upload resume uses its own checkpoint instead of overall progress", async (t) => {
const transferId = `server-copy-${crypto.randomUUID()}`;
const sourcePath = "/source/payload.bin";
const targetPath = "/target/payload.bin";
const payload = Buffer.from("abcdef");
const localStage = tempDirBridge.getTransferTempFilePath(transferId, "payload.bin");
await fs.promises.writeFile(localStage, payload);
t.after(async () => { await fs.promises.unlink(localStage).catch(() => {}); });
let remote = Buffer.alloc(0);
let promoted = false;
const targetSftp = createFastSftp({
open(_path, flags, callback) {
callback(null, Buffer.from("target-handle"));
},
write(_handle, buffer, offset, length, position, callback) {
const chunk = buffer.subarray(offset, offset + length);
if (remote.length < position) {
remote = Buffer.concat([remote, Buffer.alloc(position - remote.length)]);
}
remote = Buffer.concat([
remote.subarray(0, position),
chunk,
remote.subarray(position + chunk.length),
]);
callback(null);
},
close(_handle, callback) {
callback(null);
},
});
const sourceClient = { sftp: createFastSftp({}), stat: async () => ({ size: payload.length }) };
const targetClient = {
__netcattySudoMode: true,
sftp: targetSftp,
stat: async () => ({ size: remote.length }),
rename: async () => { promoted = true; },
delete: async () => {},
};
transferBridge.init({ sftpClients: new Map([["source", sourceClient], ["target", targetClient]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath,
targetPath,
sourceType: "sftp",
targetType: "sftp",
sourceSftpId: "source",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
resumeStage: "upload",
checkpointBytes: payload.length / 2,
downloadCheckpointBytes: payload.length,
uploadCheckpointBytes: 0,
});
assert.equal(result.error, undefined, result.error);
assert.deepEqual(remote, payload);
assert.equal(promoted, true);
});
test("server-to-server staged upload does not promote after AbortSignal-only cancellation", async (t) => {
const transferId = `server-copy-signal-cancel-${crypto.randomUUID()}`;
const sourcePath = "/source/payload.bin";
const targetPath = "/target/payload.bin";
const payload = Buffer.from("abcdef");
const localStage = tempDirBridge.getTransferTempFilePath(transferId, "payload.bin");
await fs.promises.writeFile(localStage, payload);
t.after(async () => { await fs.promises.unlink(localStage).catch(() => {}); });
const controller = new AbortController();
let remote = Buffer.alloc(0);
let promoted = false;
const removed = [];
const targetSftp = createFastSftp({
open(_path, _flags, callback) {
callback(null, Buffer.from("target-handle"));
},
write(_handle, buffer, offset, length, position, callback) {
const chunk = buffer.subarray(offset, offset + length);
remote = Buffer.concat([
remote.subarray(0, position),
chunk,
remote.subarray(position + chunk.length),
]);
callback(null);
},
close(_handle, callback) {
callback(null);
controller.abort();
},
});
const sourceClient = { sftp: createFastSftp({}), stat: async () => ({ size: payload.length }) };
const targetClient = {
__netcattySudoMode: true,
sftp: targetSftp,
stat: async () => ({ size: remote.length }),
rename: async () => { promoted = true; },
delete: async (remotePath) => { removed.push(String(remotePath)); },
};
transferBridge.init({ sftpClients: new Map([["source", sourceClient], ["target", targetClient]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath,
targetPath,
sourceType: "sftp",
targetType: "sftp",
sourceSftpId: "source",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
resumeStage: "upload",
downloadCheckpointBytes: payload.length,
uploadCheckpointBytes: 0,
abortSignal: controller.signal,
});
assert.match(result.error || "", /cancel/i);
assert.equal(promoted, false);
assert.ok(removed.some((remotePath) => remotePath.includes(".netcatty-")));
});
test("server-to-server SCP fallback uses the followed symlink content size", async (t) => {
const transferId = `scp-symlink-s2s-${crypto.randomUUID()}`;
const sourcePath = "/source/link";
const targetPath = "/target/copied.bin";
const payload = Buffer.from("followed-symlink-target-content");
const localStage = tempDirBridge.getTransferTempFilePath(transferId, path.basename(sourcePath));
t.after(async () => { await fs.promises.unlink(localStage).catch(() => {}); });
const sourceBackend = {
async stat() {
return { type: "symlink", isSymbolicLink: true, size: 4 };
},
async downloadFile(_remotePath, localPath, options = {}) {
await fs.promises.writeFile(localPath, payload);
const halfway = Math.floor(payload.length / 2);
options.onProgress?.(halfway, payload.length);
options.onProgress?.(payload.length, payload.length);
return { fileSize: payload.length, transferred: payload.length };
},
};
const remoteFiles = new Map([[targetPath, Buffer.from("old target")]]);
let releaseFinalRename;
let markFinalRenameStarted;
const finalRenameStarted = new Promise((resolve) => { markFinalRenameStarted = resolve; });
const missing = () => {
const error = new Error("No such file");
error.code = "ENOENT";
return error;
};
const targetBackend = {
async stat(remotePath) {
if (!remoteFiles.has(remotePath)) throw missing();
return { type: "file", isDirectory: false, size: remoteFiles.get(remotePath).length };
},
async mkdir() {},
async uploadFile(localPath, remotePath) {
remoteFiles.set(remotePath, await fs.promises.readFile(localPath));
return true;
},
async rename(from, to) {
if (!remoteFiles.has(from)) throw missing();
if (from.includes(".netcatty-upload-") && to === targetPath) {
markFinalRenameStarted();
await new Promise((resolve) => { releaseFinalRename = resolve; });
}
remoteFiles.set(to, remoteFiles.get(from));
remoteFiles.delete(from);
},
async remove(remotePath) {
remoteFiles.delete(remotePath);
},
async chmod() {},
};
const sourceClient = {
__netcattyFileProtocol: "scp",
__netcattyScpBackend: sourceBackend,
};
const targetClient = {
__netcattyFileProtocol: "scp",
__netcattyScpBackend: targetBackend,
};
transferBridge.init({
sftpClients: new Map([["source-scp", sourceClient], ["target-scp", targetClient]]),
});
const sender = createSender();
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath,
targetPath,
sourceType: "sftp",
targetType: "sftp",
sourceSftpId: "source-scp",
targetSftpId: "target-scp",
resumable: false,
sameHost: false,
});
await finalRenameStarted;
await transferBridge.cancelTransfer(null, { transferId });
releaseFinalRename();
const result = await running;
assert.equal(result.error, undefined, result.error);
assert.deepEqual(remoteFiles.get(targetPath), payload);
assert.equal([...remoteFiles.keys()].some((key) => key.includes(".netcatty-upload-")), false);
assert.equal([...remoteFiles.keys()].some((key) => key.includes(".netcatty-backup-")), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:cancelled"), false);
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), true);
const progress = sender.sent
.filter((entry) => entry.channel === "netcatty:transfer:progress")
.map((entry) => entry.payload);
for (let index = 1; index < progress.length; index += 1) {
assert.ok(
progress[index].transferred >= progress[index - 1].transferred,
`progress moved backwards: ${JSON.stringify(progress)}`,
);
}
for (const entry of progress.slice(0, -1)) {
assert.ok(
entry.totalBytes === 0 || entry.transferred < entry.totalBytes,
`progress completed before the transfer: ${JSON.stringify(progress)}`,
);
}
assert.ok(
progress.some((entry) => (
entry.totalBytes === payload.length
&& entry.transferred === Math.floor(payload.length / 2)
)),
`missing completed-download midpoint: ${JSON.stringify(progress)}`,
);
});
test("server-to-server concurrent failure does not complete via serial stream", async (t) => {
const transferId = `server-copy-fail-closed-${crypto.randomUUID()}`;
const sourcePath = "/source/payload.bin";
const targetPath = "/target/payload.bin";
const payload = Buffer.alloc(3 * 32 * 1024, 53);
const localStage = tempDirBridge.getTransferTempFilePath(transferId, path.basename(sourcePath));
await fs.promises.writeFile(localStage, payload);
t.after(async () => { await fs.promises.unlink(localStage).catch(() => {}); });
let secondWriteCallback = null;
let thirdWriteCompleted = false;
let secondWriteFailureScheduled = false;
const scheduleSecondWriteFailure = () => {
if (secondWriteFailureScheduled || !thirdWriteCompleted || !secondWriteCallback) return;
secondWriteFailureScheduled = true;
const callback = secondWriteCallback;
secondWriteCallback = null;
queueMicrotask(() => callback(new Error("second range failed")));
};
let remoteBytes = 0;
let createWriteStreamCalls = 0;
const fastSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(null, Buffer.from("remote-handle"));
},
write(_handle, _buffer, _offset, length, position, callback) {
if (position === 32 * 1024) {
secondWriteCallback = callback;
scheduleSecondWriteFailure();
return;
}
remoteBytes = Math.max(remoteBytes, position + length);
callback(null);
if (position === 2 * 32 * 1024) {
thirdWriteCompleted = true;
scheduleSecondWriteFailure();
}
},
close(_handle, callback) {
callback(null);
},
end() {},
});
const targetSftp = createFastSftp({
open(_remotePath, _flags, callback) {
callback(new Error("shared open also fails"));
},
createWriteStream() {
createWriteStreamCalls += 1;
throw new Error("serial WriteStream must not run");
},
});
const sourceClient = {
sftp: createFastSftp({}),
stat: async () => ({ size: payload.length }),
};
const targetClient = {
sftp: targetSftp,
stat: async () => ({ size: remoteBytes }),
rename: async () => {},
delete: async () => {},
client: {
sftp(callback) {
callback(null, fastSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["source", sourceClient], ["target", targetClient]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath,
targetPath,
sourceType: "sftp",
targetType: "sftp",
sourceSftpId: "source",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
resumeStage: "upload",
downloadCheckpointBytes: payload.length,
uploadCheckpointBytes: 0,
},
);
assert.match(result.error || "", /pipelined upload failed|second range failed/i);
assert.equal(createWriteStreamCalls, 0);
// Local S2S temp (full download) must survive failed upload attempts.
assert.equal(fs.statSync(localStage).size, payload.length);
});
test("upload recovery without a full source identity restarts from zero", async (t) => {
// Simulates force-quit mid-upload: UI progress saved a high checkpoint, but
// only a shorter prefix made it into the remote staged .part file.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-crash-resume-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const transferId = `crash-resume-${crypto.randomUUID()}`;
const sourcePath = path.join(tempDir, "source.bin");
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE * 2 + 19);
for (let index = 0; index < payload.length; index += 1) payload[index] = index % 251;
await fs.promises.writeFile(sourcePath, payload);
// The durable prefix is deliberately not chunk-aligned. The first resumed
// request must stop at the digest boundary, and the final bytes must match.
let remote = Buffer.from(payload.subarray(0, 4));
let promoted = false;
let minWritePosition = Infinity;
const concurrentSftp = createFastSftp({
open(_path, flags, callback) {
callback(null, Buffer.from("resume-handle"));
},
write(_handle, buffer, offset, length, position, callback) {
minWritePosition = Math.min(minWritePosition, position);
const chunk = buffer.subarray(offset, offset + length);
if (remote.length < position) {
remote = Buffer.concat([remote, Buffer.alloc(position - remote.length)]);
}
remote = Buffer.concat([
remote.subarray(0, position),
chunk,
remote.subarray(position + chunk.length),
]);
callback(null);
},
close(_handle, callback) {
callback(null);
},
// Resume safety samples the durable remote prefix via createReadStream.
createReadStream(_path, options = {}) {
const start = options.start || 0;
const end = options.end;
const slice = end === undefined ? remote.subarray(start) : remote.subarray(start, end + 1);
return Readable.from([slice]);
},
createWriteStream() {
throw new Error("serial WriteStream must not run");
},
});
const client = {
__netcattySudoMode: true, // shared concurrent path only
sftp: concurrentSftp,
// First stats clamp checkpoint from claimed 8 down to durable remote size 4.
stat: async () => ({ size: remote.length }),
rename: async () => { promoted = true; },
delete: async () => {},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath,
targetPath: "/root/source.bin",
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: true,
checkpointBytes: 8, // ahead of durable remote size 4
});
assert.equal(result.error, undefined, result.error);
assert.deepEqual(remote, payload);
assert.equal(promoted, true);
// A checkpoint without a full source digest cannot prove the source suffix
// stayed unchanged across the crash, so the safe recovery is a full restart.
assert.equal(minWritePosition, 0);
});
test("local resume after hard quit clamps checkpoint to durable staged file size", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-local-crash-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const transferId = `local-crash-${crypto.randomUUID()}`;
const sourcePath = path.join(tempDir, "source.bin");
const targetPath = path.join(tempDir, "target.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, "target.bin");
await fs.promises.writeFile(sourcePath, Buffer.from("abcdef"));
// Durable staged file is shorter than claimed checkpoint (crash / unflushed write).
await fs.promises.writeFile(stagedPath, Buffer.from("ab"));
t.after(async () => { await fs.promises.unlink(stagedPath).catch(() => {}); });
transferBridge.init({ sftpClients: new Map() });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath,
targetPath,
sourceType: "local",
targetType: "local",
totalBytes: 6,
resumable: true,
checkpointBytes: 5,
});
assert.equal(result.error, undefined, result.error);
assert.equal(await fs.promises.readFile(targetPath, "utf8"), "abcdef");
});
test("resume rejects a same-size temporary prefix that does not match the source", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-prefix-test-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const transferId = `prefix-${crypto.randomUUID()}`;
const sourcePath = path.join(tempDir, "source.bin");
const targetPath = path.join(tempDir, "target.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, "target.bin");
await fs.promises.writeFile(sourcePath, Buffer.from("abcdef"));
await fs.promises.writeFile(stagedPath, Buffer.from("xyz"));
t.after(async () => { await fs.promises.unlink(stagedPath).catch(() => {}); });
transferBridge.init({ sftpClients: new Map() });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId,
sourcePath,
targetPath,
sourceType: "local",
targetType: "local",
totalBytes: 6,
resumable: true,
checkpointBytes: 3,
sourceFingerprint: `sha256:${crypto.createHash("sha256").update("abcdef").digest("hex")}`,
});
assert.match(result.error || "", /saved content does not match/i);
assert.equal(await fs.promises.readFile(sourcePath, "utf8"), "abcdef");
});
test("bridge admission applies one global concurrency limit across callers", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-admission-test-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const pendingByPath = new Map();
const payloads = {
"/first": Buffer.from("a"),
"/second": Buffer.from("b"),
};
const sftp = createFastSftp({
open(remotePath, flags, callback) {
const cb = typeof flags === "function" ? flags : callback;
cb(null, Buffer.from(`handle:${remotePath}`));
},
read(handle, buffer, offset, length, position, callback) {
const key = handle.toString().replace(/^handle:/, "");
const payload = payloads[key] || Buffer.from("x");
// Stall only the first READ per path so admission can observe "started".
// Later verification samples complete immediately.
if (!pendingByPath.has(key)) {
pendingByPath.set(key, []);
pendingByPath.get(key).push(() => {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
callback(null, slice.length);
});
return;
}
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
close(_handle, callback) { callback(null); },
// Content-hash helpers only — bulk body uses open/read above.
createReadStream(remotePath, options = {}) {
const payload = payloads[remotePath] || Buffer.from("x");
const start = Number.isFinite(options.start) ? options.start : 0;
const end = Number.isFinite(options.end) ? options.end : payload.length - 1;
return Readable.from([Buffer.from(payload.subarray(start, end + 1))]);
},
});
const client = { sftp, stat: async () => ({ size: 1 }) };
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const start = (id, remotePath) => transferBridge.startTransfer({ sender: createSender() }, {
transferId: id,
sourcePath: remotePath,
targetPath: path.join(tempDir, `${id}.bin`),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 1,
resumable: true,
globalConcurrency: 1,
});
const first = start("admission-first", "/first");
assert.equal(
await waitUntil(() => (pendingByPath.get("/first") || []).length > 0),
true,
"first admitted transfer did not start",
);
const second = start("admission-second", "/second");
await new Promise((resolve) => setImmediate(resolve));
assert.equal(pendingByPath.has("/second"), false);
for (const deliver of (pendingByPath.get("/first") || []).splice(0)) deliver();
assert.equal((await first).error, undefined);
assert.equal(
await waitUntil(() => (pendingByPath.get("/second") || []).length > 0),
true,
"second admitted transfer did not start after the first completed",
);
for (const deliver of (pendingByPath.get("/second") || []).splice(0)) deliver();
assert.equal((await second).error, undefined);
});
test("bridge admission gives different remote sessions independent concurrency", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-per-session-test-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const makeClient = (label) => {
const pending = [];
let firstRead = true;
const payload = Buffer.from(label);
const { sftp } = createPipelinedDownloadSftp(payload, {
read(_handle, buffer, offset, length, position, callback) {
const deliver = () => {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
callback(null, slice.length);
};
if (firstRead) {
firstRead = false;
pending.push(deliver);
return;
}
setImmediate(deliver);
},
});
return {
client: { sftp, stat: async () => ({ size: 1 }) },
pending,
release() {
for (const deliver of pending.splice(0)) deliver();
},
};
};
const a = makeClient("a");
const b = makeClient("b");
transferBridge.init({ sftpClients: new Map([
["source-a", a.client],
["source-b", b.client],
]) });
const start = (id, sourceSftpId) => transferBridge.startTransfer({ sender: createSender() }, {
transferId: id,
sourcePath: `/${id}`,
targetPath: path.join(tempDir, `${id}.bin`),
sourceType: "sftp",
targetType: "local",
sourceSftpId,
totalBytes: 1,
resumable: true,
globalConcurrency: 1,
});
const first = start("per-session-a", "source-a");
const second = start("per-session-b", "source-b");
const bothStarted = await waitUntil(
() => a.pending.length > 0 && b.pending.length > 0,
500,
);
a.release();
b.release();
await Promise.all([first, second]);
assert.equal(bothStarted, true);
});
test("clearPendingCancel allows intentional same-id start after a pre-start cancel", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-clear-pending-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const { sftp } = createPipelinedDownloadSftp(Buffer.from("a"));
transferBridge.init({ sftpClients: new Map([["source", { sftp, stat: async () => ({ size: 1 }) }]]) });
await transferBridge.cancelTransfer(null, { transferId: "retry-same-id" });
transferBridge.clearPendingCancel("retry-same-id");
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "retry-same-id",
sourcePath: "/remote",
targetPath: path.join(tempDir, "out.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 1,
skipAdmission: true,
});
assert.equal(result.cancelled, undefined);
assert.equal(result.error, undefined, result.error);
});
test("cancel before skipAdmission start rejects the transfer without writing", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-pending-cancel-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
let openCalls = 0;
const { sftp } = createPipelinedDownloadSftp(Buffer.from("a"), {
open(_remotePath, flags, callback) {
openCalls += 1;
const cb = typeof flags === "function" ? flags : callback;
cb(null, Buffer.from("read-handle"));
},
});
transferBridge.init({ sftpClients: new Map([["source", { sftp, stat: async () => ({ size: 1 }) }]]) });
await transferBridge.cancelTransfer(null, { transferId: "pending-cancel-1" });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "pending-cancel-1",
sourcePath: "/remote",
targetPath: path.join(tempDir, "out.bin"),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 1,
skipAdmission: true,
});
assert.equal(result.cancelled, true);
assert.equal(openCalls, 0, "pre-start cancel must not open a remote handle");
});
test("pre-start cancel latches stay hard bounded when cancelled work never starts", async (t) => {
t.after(() => {
for (let index = 0; index < 5_000; index += 1) {
transferBridge.clearPendingCancel(`orphan-cancel-${index}`);
}
});
for (let index = 0; index < 5_000; index += 1) {
await transferBridge.cancelTransfer(null, { transferId: `orphan-cancel-${index}` });
}
assert.ok(
transferBridge._getPendingCancelCountForTests() <= 4_096,
`pending cancel latches grew to ${transferBridge._getPendingCancelCountForTests()}`,
);
});
test("pausing a queued admission job preserves the payload checkpoint", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-queued-checkpoint-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const pendingByPath = new Map();
const payloads = { "/first": Buffer.from("a"), "/second": Buffer.from("b") };
const sftp = createFastSftp({
open(remotePath, flags, callback) {
const cb = typeof flags === "function" ? flags : callback;
cb(null, Buffer.from(`handle:${remotePath}`));
},
read(handle, buffer, offset, length, position, callback) {
const key = handle.toString().replace(/^handle:/, "");
const payload = payloads[key] || Buffer.from("x");
if (!pendingByPath.has(key)) {
pendingByPath.set(key, []);
pendingByPath.get(key).push(() => {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
callback(null, slice.length);
});
return;
}
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
close(_handle, callback) { callback(null); },
createReadStream(remotePath, options = {}) {
const payload = payloads[remotePath] || Buffer.from("x");
const start = Number.isFinite(options.start) ? options.start : 0;
const end = Number.isFinite(options.end) ? options.end : payload.length - 1;
return Readable.from([Buffer.from(payload.subarray(start, end + 1))]);
},
});
transferBridge.init({ sftpClients: new Map([["source", { sftp, stat: async () => ({ size: 1 }) }]]) });
const start = (id, remotePath, checkpointBytes) => transferBridge.startTransfer({ sender: createSender() }, {
transferId: id,
sourcePath: remotePath,
targetPath: path.join(tempDir, `${id}.bin`),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 1,
resumable: true,
checkpointBytes,
globalConcurrency: 1,
});
const first = start("queued-ckpt-first", "/first", 0);
assert.ok(await waitUntil(() => (pendingByPath.get("/first") || []).length > 0));
const second = start("queued-ckpt-second", "/second", 42);
const paused = await transferBridge.pauseTransfer(null, { transferId: "queued-ckpt-second" });
assert.equal(paused.success, true);
assert.equal(paused.checkpointBytes, 42);
assert.equal((await transferBridge.cancelTransfer(null, { transferId: "queued-ckpt-second" })).success, true);
assert.equal((await second).cancelled, true);
for (const deliver of (pendingByPath.get("/first") || []).splice(0)) deliver();
assert.equal((await first).error, undefined);
});
test("queued admission jobs can be paused, resumed, prioritized, and cancelled before opening a stream", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-queued-controls-"));
t.after(async () => { await fs.promises.rm(tempDir, { recursive: true, force: true }); });
const pendingByPath = new Map();
let secondOpenCalls = 0;
const payloads = { "/first": Buffer.from("a"), "/second": Buffer.from("b") };
const sftp = createFastSftp({
open(remotePath, flags, callback) {
const cb = typeof flags === "function" ? flags : callback;
if (String(remotePath).includes("second")) secondOpenCalls += 1;
cb(null, Buffer.from(`handle:${remotePath}`));
},
read(handle, buffer, offset, length, position, callback) {
const key = handle.toString().replace(/^handle:/, "");
const payload = payloads[key] || Buffer.from("x");
if (!pendingByPath.has(key)) {
pendingByPath.set(key, []);
pendingByPath.get(key).push(() => {
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
callback(null, slice.length);
});
return;
}
const slice = payload.subarray(position, position + length);
slice.copy(buffer, offset);
setImmediate(() => callback(null, slice.length));
},
close(_handle, callback) { callback(null); },
createReadStream(remotePath, options = {}) {
const payload = payloads[remotePath] || Buffer.from("x");
const start = Number.isFinite(options.start) ? options.start : 0;
const end = Number.isFinite(options.end) ? options.end : payload.length - 1;
return Readable.from([Buffer.from(payload.subarray(start, end + 1))]);
},
});
transferBridge.init({ sftpClients: new Map([["source", { sftp, stat: async () => ({ size: 1 }) }]]) });
const start = (id, remotePath) => transferBridge.startTransfer({ sender: createSender() }, {
transferId: id,
sourcePath: remotePath,
targetPath: path.join(tempDir, `${id}.bin`),
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
totalBytes: 1,
resumable: true,
globalConcurrency: 1,
});
const first = start("queued-control-first", "/first");
assert.ok(await waitUntil(() => (pendingByPath.get("/first") || []).length > 0));
const second = start("queued-control-second", "/second");
assert.equal((await transferBridge.pauseTransfer(null, { transferId: "queued-control-second" })).success, true);
assert.equal(secondOpenCalls, 0, "queued second must not open while first holds admission");
assert.equal((await transferBridge.resumeTransfer(null, { transferId: "queued-control-second" })).success, true);
assert.equal((await transferBridge.prioritizeTransfer(null, { transferId: "queued-control-second" })).success, true);
assert.equal((await transferBridge.cancelTransfer(null, { transferId: "queued-control-second" })).success, true);
assert.equal((await second).cancelled, true);
for (const deliver of (pendingByPath.get("/first") || []).splice(0)) deliver();
assert.equal((await first).error, undefined);
assert.equal(secondOpenCalls, 0, "cancelled queued job must never open a remote handle");
});
test("transfer session leases hold SFTP ids across soft-close until release", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
sftpTransferSessionLeaseStore.resetForTests();
t.after(() => sftpTransferSessionLeaseStore.resetForTests());
let hardCloseCalls = 0;
const sftpBridge = require("./sftpBridge.cjs");
const originalClose = sftpBridge.closeSftp;
sftpBridge.closeSftp = async (_event, payload) => {
if (payload?.force) {
hardCloseCalls += 1;
sftpTransferSessionLeaseStore.clear(payload.sftpId);
return { success: true, deferred: false };
}
if (sftpTransferSessionLeaseStore.markSoftClosed(payload.sftpId)) {
return {
success: true,
deferred: true,
leaseCount: sftpTransferSessionLeaseStore.getLeaseCount(payload.sftpId),
};
}
return { success: true, deferred: false };
};
t.after(() => {
sftpBridge.closeSftp = originalClose;
});
assert.deepEqual(
transferBridge.listTransferSftpIds({ sourceSftpId: "s1", targetSftpId: "s2", sourceHostId: "h" }),
["s1", "s2"],
);
// Hold two transfers on s1 before soft-close (re-acquire after soft-close
// would clear the deferred flag by design).
transferBridge.acquireTransferSessionLeases("xfer-1", {
sourceSftpId: "s1",
targetSftpId: "s2",
});
transferBridge.acquireTransferSessionLeases("xfer-2", { sourceSftpId: "s1" });
assert.equal(sftpTransferSessionLeaseStore.getLeaseCount("s1"), 2);
assert.equal(sftpTransferSessionLeaseStore.getLeaseCount("s2"), 1);
const soft = await sftpBridge.closeSftp(null, { sftpId: "s1" });
assert.equal(soft.deferred, true);
assert.equal(sftpTransferSessionLeaseStore.isSoftClosed("s1"), true);
assert.equal(hardCloseCalls, 0);
transferBridge.releaseTransferSessionLeases("xfer-1", ["s1", "s2"]);
assert.equal(hardCloseCalls, 0);
assert.equal(sftpTransferSessionLeaseStore.isHeld("s1"), true);
assert.equal(sftpTransferSessionLeaseStore.isHeld("s2"), false);
// Last release on soft-closed session triggers hard close.
transferBridge.releaseTransferSessionLeases("xfer-2", ["s1"]);
await new Promise((resolve) => setImmediate(resolve));
assert.equal(hardCloseCalls, 1);
assert.equal(sftpTransferSessionLeaseStore.isHeld("s1"), false);
});
test("a directory pool lease survives terminal close between child files", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
sftpTransferSessionLeaseStore.resetForTests();
t.after(() => sftpTransferSessionLeaseStore.resetForTests());
let hardCloseCalls = 0;
const sftpBridge = require("./sftpBridge.cjs");
const originalClose = sftpBridge.closeSftp;
sftpBridge.closeSftp = async (_event, payload) => {
if (payload?.force) {
hardCloseCalls += 1;
sftpTransferSessionLeaseStore.clear(payload.sftpId);
return { success: true, deferred: false };
}
if (sftpTransferSessionLeaseStore.markSoftClosed(payload.sftpId)) {
return {
success: true,
deferred: true,
leaseCount: sftpTransferSessionLeaseStore.getLeaseCount(payload.sftpId),
};
}
return { success: true, deferred: false };
};
t.after(() => {
sftpBridge.closeSftp = originalClose;
});
transferBridge.init({ sftpClients: new Map([["folder-sftp", {}]]) });
assert.deepEqual(
transferBridge.retainSftpTransferSession(null, {
sftpId: "folder-sftp",
leaseId: "pool:folder-sftp",
}),
{ success: true },
);
transferBridge.acquireTransferSessionLeases("child-1", { targetSftpId: "folder-sftp" });
const soft = await sftpBridge.closeSftp(null, { sftpId: "folder-sftp" });
assert.equal(soft.deferred, true);
transferBridge.releaseTransferSessionLeases("child-1", ["folder-sftp"]);
await new Promise((resolve) => setImmediate(resolve));
assert.equal(hardCloseCalls, 0, "the directory pool must keep the session alive between files");
assert.doesNotThrow(() => {
transferBridge.acquireTransferSessionLeases("child-2", { targetSftpId: "folder-sftp" });
});
transferBridge.releaseTransferSessionLeases("child-2", ["folder-sftp"]);
assert.deepEqual(
transferBridge.releaseSftpTransferSession(null, {
sftpId: "folder-sftp",
leaseId: "pool:folder-sftp",
}),
{ success: true },
);
await new Promise((resolve) => setImmediate(resolve));
assert.equal(hardCloseCalls, 1, "the deferred close should run only after the pool releases");
});
test("directory child lifecycle events preserve their parent id", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-child-event-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const sourcePath = path.join(tempDir, "child.txt");
const targetPath = path.join(tempDir, "copied.txt");
await fs.promises.writeFile(sourcePath, "child");
transferBridge.init({ sftpClients: new Map() });
const sender = createSender();
const result = await transferBridge.startTransfer({ sender }, {
transferId: "folder-child-event",
parentTaskId: "folder-root-event",
sourcePath,
targetPath,
sourceType: "local",
targetType: "local",
totalBytes: 5,
resumable: false,
skipAdmission: true,
});
assert.equal(result.error, undefined);
const started = sender.sent.find((entry) => entry.channel === "netcatty:transfer:started");
assert.equal(started?.payload?.parentTaskId, "folder-root-event");
});
test("a committed soft-close never lends the SFTP client again while client.end is pending", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
const sftpBridge = require("./sftpBridge.cjs");
sftpTransferSessionLeaseStore.resetForTests();
let releaseEnd;
const endGate = new Promise((resolve) => { releaseEnd = resolve; });
let markEndStarted;
const endStarted = new Promise((resolve) => { markEndStarted = resolve; });
let endCalls = 0;
const client = {
async end() {
endCalls += 1;
markEndStarted();
await endGate;
},
};
const sftpClients = new Map([["sftp-soft-close-race", client]]);
transferBridge.init({ sftpClients });
sftpBridge.init({ sftpClients, sessions: new Map(), electronModule: {} });
t.after(() => {
releaseEnd();
sftpTransferSessionLeaseStore.resetForTests();
sftpClients.clear();
});
transferBridge.acquireTransferSessionLeases("directory-child-1", {
sourceSftpId: "sftp-soft-close-race",
});
const softClose = await sftpBridge.closeSftp(null, { sftpId: "sftp-soft-close-race" });
assert.equal(softClose.deferred, true);
transferBridge.releaseTransferSessionLeases("directory-child-1", ["sftp-soft-close-race"]);
await endStarted;
assert.throws(
() => transferBridge.acquireTransferSessionLeases("directory-child-2", {
sourceSftpId: "sftp-soft-close-race",
}),
/closing|closed/i,
);
assert.equal(sftpTransferSessionLeaseStore.isHeld("sftp-soft-close-race"), false);
releaseEnd();
const deadline = Date.now() + 1_000;
while (sftpClients.has("sftp-soft-close-race")) {
if (Date.now() >= deadline) throw new Error("Timed out waiting for committed SFTP close");
await new Promise((resolve) => setImmediate(resolve));
}
assert.equal(endCalls, 1);
});
test("a direct panel close never lends the SFTP client while client.end is pending", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
const sftpBridge = require("./sftpBridge.cjs");
sftpTransferSessionLeaseStore.resetForTests();
let releaseEnd;
const endGate = new Promise((resolve) => { releaseEnd = resolve; });
let markEndStarted;
const endStarted = new Promise((resolve) => { markEndStarted = resolve; });
const client = {
async end() {
markEndStarted();
await endGate;
},
};
const sftpClients = new Map([["sftp-direct-close-race", client]]);
transferBridge.init({ sftpClients });
sftpBridge.init({ sftpClients, sessions: new Map(), electronModule: {} });
t.after(() => {
releaseEnd();
sftpTransferSessionLeaseStore.resetForTests();
sftpClients.clear();
});
const closePromise = sftpBridge.closeSftp(null, { sftpId: "sftp-direct-close-race" });
await endStarted;
assert.throws(
() => transferBridge.acquireTransferSessionLeases("new-transfer", {
sourceSftpId: "sftp-direct-close-race",
}),
/closing|closed/i,
);
releaseEnd();
const closeResult = await closePromise;
assert.equal(closeResult.deferred, false);
assert.equal(sftpClients.has("sftp-direct-close-race"), false);
assert.equal(sftpTransferSessionLeaseStore.isHeld("sftp-direct-close-race"), false);
});
test("a transfer rejected by a committed SFTP close leaves no active transfer entry", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
const sftpId = "sftp-rejected-before-lease";
const closeToken = sftpTransferSessionLeaseStore.beginHardClose(sftpId);
assert.equal(sftpTransferSessionLeaseStore.commitHardClose(sftpId, closeToken), true);
transferBridge.init({ sftpClients: new Map([[sftpId, {}]]) });
t.after(() => sftpTransferSessionLeaseStore.resetForTests());
await assert.rejects(
transferBridge.startTransfer({ sender: createSender() }, {
transferId: "rejected-before-lease",
sourcePath: "/remote/file.bin",
targetPath: "/tmp/rejected-before-lease.bin",
sourceType: "sftp",
targetType: "local",
sourceSftpId: sftpId,
totalBytes: 1,
skipAdmission: true,
}),
/closing|closed/i,
);
assert.equal(transferBridge._getActiveTransferCountForTests(), 0);
});
test("same-host directory copy rejected by a committed close leaves no lease or active transfer", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
const sftpId = "same-host-directory-closing";
sftpTransferSessionLeaseStore.resetForTests();
const closeToken = sftpTransferSessionLeaseStore.beginHardClose(sftpId);
assert.equal(sftpTransferSessionLeaseStore.commitHardClose(sftpId, closeToken), true);
transferBridge.init({ sftpClients: new Map([[sftpId, {}]]) });
t.after(() => sftpTransferSessionLeaseStore.resetForTests());
await assert.rejects(
transferBridge.sameHostCopyDirectory(
{ sender: createSender() },
{
transferId: "same-host-directory-rejected",
sftpId,
sourcePath: "/source",
targetPath: "/target",
encoding: "utf-8",
},
),
/closing|closed/i,
);
assert.equal(transferBridge._getActiveTransferCountForTests(), 0);
assert.equal(sftpTransferSessionLeaseStore.getLeaseCount(sftpId), 0);
});
test("same-host directory copy cancellation aborts cp and releases its lease", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
sftpTransferSessionLeaseStore.resetForTests();
t.after(() => sftpTransferSessionLeaseStore.resetForTests());
const sftpId = "same-host-directory-cancel";
let markExecOpened;
const execOpened = new Promise((resolve) => { markExecOpened = resolve; });
const stream = new EventEmitter();
stream.stderr = new EventEmitter();
stream.close = () => {};
stream.end = () => {};
stream.destroy = () => {};
const client = {
client: {
exec(_command, callback) {
callback(null, stream);
markExecOpened();
},
},
};
transferBridge.init({ sftpClients: new Map([[sftpId, client]]) });
const running = transferBridge.sameHostCopyDirectory(
{ sender: createSender() },
{
transferId: "same-host-directory-cancelled",
sftpId,
sourcePath: "/source",
targetPath: "/target",
encoding: "utf-8",
},
);
await execOpened;
assert.equal(transferBridge._getActiveTransferCountForTests(), 1);
assert.equal(sftpTransferSessionLeaseStore.getLeaseCount(sftpId), 1);
await transferBridge.cancelTransfer(null, { transferId: "same-host-directory-cancelled" });
await assert.rejects(running, /cancel/i);
assert.equal(transferBridge._getActiveTransferCountForTests(), 0);
assert.equal(sftpTransferSessionLeaseStore.getLeaseCount(sftpId), 0);
});
test("a pre-start cancellation prevents same-host directory registration and lease acquisition", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
sftpTransferSessionLeaseStore.resetForTests();
t.after(() => sftpTransferSessionLeaseStore.resetForTests());
const sftpId = "same-host-directory-pre-cancel";
let execCalls = 0;
transferBridge.init({
sftpClients: new Map([[sftpId, {
client: {
exec() { execCalls += 1; },
},
}]]),
});
const transferId = "same-host-directory-pre-cancelled";
await transferBridge.cancelTransfer(null, { transferId });
const result = await transferBridge.sameHostCopyDirectory(
{ sender: createSender() },
{
transferId,
sftpId,
sourcePath: "/source",
targetPath: "/target",
encoding: "utf-8",
},
);
assert.equal(result.cancelled, true);
assert.equal(execCalls, 0);
assert.equal(transferBridge._getActiveTransferCountForTests(), 0);
assert.equal(sftpTransferSessionLeaseStore.getLeaseCount(sftpId), 0);
assert.equal(transferBridge._getPendingCancelCountForTests(), 0);
});
test("same-host file cp cancellation releases the shared session lease", async (t) => {
const {
sftpTransferSessionLeaseStore,
} = require("./sftpTransferSessionLease.cjs");
sftpTransferSessionLeaseStore.resetForTests();
t.after(() => sftpTransferSessionLeaseStore.resetForTests());
const sftpId = "same-host-file-cancel";
let markExecOpened;
const execOpened = new Promise((resolve) => { markExecOpened = resolve; });
const stream = new EventEmitter();
stream.stderr = new EventEmitter();
stream.close = () => {};
stream.end = () => {};
stream.destroy = () => {};
const client = {
client: {
exec(_command, callback) {
callback(null, stream);
markExecOpened();
},
},
};
transferBridge.init({ sftpClients: new Map([[sftpId, client]]) });
const transferId = "same-host-file-cancelled";
const running = transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/source/file.bin",
targetPath: "/target/file.bin",
sourceType: "sftp",
targetType: "sftp",
sourceSftpId: sftpId,
targetSftpId: sftpId,
totalBytes: 1,
sameHost: true,
skipAdmission: true,
},
);
await execOpened;
assert.equal(transferBridge._getActiveTransferCountForTests(), 1);
assert.equal(sftpTransferSessionLeaseStore.getLeaseCount(sftpId), 1);
await transferBridge.cancelTransfer(null, { transferId });
const result = await running;
assert.equal(result.error, "Transfer cancelled");
assert.equal(transferBridge._getActiveTransferCountForTests(), 0);
assert.equal(sftpTransferSessionLeaseStore.getLeaseCount(sftpId), 0);
});
test("local promotion restores concurrent replacement moved into the backup", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-local-promo-backup-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const stagedPath = path.join(tempDir, "download.staged");
const targetPath = path.join(tempDir, "target.bin");
const original = Buffer.from("original-target-content");
const replacement = Buffer.from("concurrent-replacement");
const downloaded = Buffer.from("downloaded-replacement");
await fs.promises.writeFile(targetPath, original);
await fs.promises.writeFile(stagedPath, downloaded);
const originalStat = await fs.promises.lstat(targetPath);
const expectedStable = transferBridge._stableLocalFileIdentityForTests(originalStat);
// validateTarget claims the original identity, but a concurrent process has
// already replaced the live path. rename then moves the wrong file to backup.
await assert.rejects(
() => transferBridge._promoteLocalTransferForTests(stagedPath, targetPath, {
async validateTarget() {
await fs.promises.unlink(targetPath);
await fs.promises.writeFile(targetPath, replacement);
return {
// Keep existingMode null so promotion does not re-validate after chmod.
existingMode: null,
stableIdentity: expectedStable,
targetIdentity: [
originalStat.dev,
originalStat.ino,
originalStat.size,
originalStat.mtimeMs,
originalStat.ctimeMs,
].join(":"),
};
},
}),
/changed during replacement/i,
);
// Replacement must still be at the target (restored from backup), download not published.
assert.deepEqual(await fs.promises.readFile(targetPath), replacement);
assert.equal(
(await fs.promises.readdir(tempDir)).some((name) => name.includes(".backup")),
false,
);
});
test("local promotion does not restore mismatched backup over a recreated target", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-local-promo-mismatch-recreate-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const stagedPath = path.join(tempDir, "download.staged");
const targetPath = path.join(tempDir, "target.bin");
const original = Buffer.from("original-target-content");
const concurrent = Buffer.from("recreated-during-mismatch");
const downloaded = Buffer.from("downloaded-replacement");
await fs.promises.writeFile(targetPath, original);
await fs.promises.writeFile(stagedPath, downloaded);
// Claim a stable identity that will not match whatever lands in backup.
const fakeStable = "999:999:999";
const originalRename = fs.promises.rename.bind(fs.promises);
let injected = false;
fs.promises.rename = async (from, to) => {
const result = await originalRename(from, to);
if (!injected && path.resolve(from) === path.resolve(targetPath) && String(to).includes(".backup")) {
injected = true;
// Recreate target after backup move; mismatch branch must not restore over it.
await fs.promises.writeFile(targetPath, concurrent);
}
return result;
};
t.after(() => {
fs.promises.rename = originalRename;
});
await assert.rejects(
() => transferBridge._promoteLocalTransferForTests(stagedPath, targetPath, {
async validateTarget() {
return {
existingMode: null,
stableIdentity: fakeStable,
targetIdentity: `${fakeStable}:0:0`,
};
},
}),
/changed during replacement/i,
);
assert.deepEqual(await fs.promises.readFile(targetPath), concurrent);
const backupName = (await fs.promises.readdir(tempDir)).find((name) => name.includes(".backup"));
assert.ok(backupName);
assert.deepEqual(await fs.promises.readFile(path.join(tempDir, backupName)), original);
});
test("local promotion does not clobber a target recreated after backup", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-local-promo-recreate-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const stagedPath = path.join(tempDir, "download.staged");
const targetPath = path.join(tempDir, "target.bin");
const original = Buffer.from("original-target-content");
const concurrent = Buffer.from("recreated-after-backup");
const downloaded = Buffer.from("downloaded-replacement");
await fs.promises.writeFile(targetPath, original);
await fs.promises.writeFile(stagedPath, downloaded);
const originalStat = await fs.promises.lstat(targetPath);
const originalRename = fs.promises.rename.bind(fs.promises);
let injected = false;
fs.promises.rename = async (from, to) => {
const result = await originalRename(from, to);
if (!injected && path.resolve(from) === path.resolve(targetPath) && String(to).includes(".backup")) {
injected = true;
await fs.promises.writeFile(targetPath, concurrent);
}
return result;
};
t.after(() => {
fs.promises.rename = originalRename;
});
await assert.rejects(
() => transferBridge._promoteLocalTransferForTests(stagedPath, targetPath, {
async validateTarget() {
return {
existingMode: null,
stableIdentity: transferBridge._stableLocalFileIdentityForTests(originalStat),
targetIdentity: [
originalStat.dev,
originalStat.ino,
originalStat.size,
originalStat.mtimeMs,
originalStat.ctimeMs,
].join(":"),
};
},
}),
/changed during replacement/i,
);
assert.deepEqual(await fs.promises.readFile(targetPath), concurrent);
// Original preserved in a backup artifact for recovery.
const backupName = (await fs.promises.readdir(tempDir)).find((name) => name.includes(".backup"));
assert.ok(backupName);
assert.deepEqual(await fs.promises.readFile(path.join(tempDir, backupName)), original);
});
test("local promotion commits before a late cancel and never deletes a concurrent replacement", async (t) => {
const tempDir = await fs.promises.mkdtemp(`${tempDirBridge.getTempFilePath("local-promo-postpub")}-`);
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const stagedPath = path.join(tempDir, "download.staged");
const targetPath = path.join(tempDir, "target.bin");
await fs.promises.writeFile(targetPath, "original");
await fs.promises.writeFile(stagedPath, "download");
let cancelled = false;
let committed = false;
const link = fs.promises.link;
t.after(() => { fs.promises.link = link; });
fs.promises.link = async (...args) => {
await link.apply(fs.promises, args);
if (args[1] === targetPath && String(args[0]).endsWith(".ready")) {
await fs.promises.unlink(targetPath);
await fs.promises.writeFile(targetPath, "concurrent");
cancelled = true;
}
};
await transferBridge._promoteLocalTransferForTests(stagedPath, targetPath, {
assertNotCancelled() { if (cancelled) throw new Error("Transfer cancelled"); },
onCommit() { committed = true; },
});
assert.equal(committed, true);
assert.equal(await fs.promises.readFile(targetPath, "utf8"), "concurrent");
});
test("local promotion succeeds when backup still matches validated identity", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-local-promo-ok-"));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
});
const stagedPath = path.join(tempDir, "download.staged");
const targetPath = path.join(tempDir, "target.bin");
await fs.promises.writeFile(targetPath, Buffer.from("old"));
await fs.promises.writeFile(stagedPath, Buffer.from("new"));
const originalStat = await fs.promises.lstat(targetPath);
await transferBridge._promoteLocalTransferForTests(stagedPath, targetPath, {
async validateTarget() {
const latest = await fs.promises.lstat(targetPath);
return {
existingMode: latest.mode & 0o7777,
stableIdentity: transferBridge._stableLocalFileIdentityForTests(latest),
targetIdentity: [
latest.dev,
latest.ino,
latest.size,
latest.mtimeMs,
latest.ctimeMs,
].join(":"),
};
},
});
assert.deepEqual(await fs.promises.readFile(targetPath), Buffer.from("new"));
assert.notEqual(
transferBridge._stableLocalFileIdentityForTests(await fs.promises.lstat(targetPath)),
transferBridge._stableLocalFileIdentityForTests(originalStat),
);
});
test("preserveTransferredDestinationMtime stamps local targets from sourceSoftIdentity", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-preserve-mtime-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const targetPath = path.join(tempDir, "copied.bin");
await fs.promises.writeFile(targetPath, Buffer.from("payload"));
const before = await fs.promises.stat(targetPath);
const sourceMtimeMs = 1_700_000_000_000; // 2023-11-14T22:13:20.000Z
await transferBridge._preserveTransferredDestinationMtimeForTests({
targetType: "local",
targetPath,
sourceSoftIdentity: { size: 7, mtimeMs: sourceMtimeMs },
});
const after = await fs.promises.stat(targetPath);
assert.equal(Math.floor(after.mtimeMs / 1000), Math.floor(sourceMtimeMs / 1000));
assert.notEqual(Math.floor(after.mtimeMs / 1000), Math.floor(before.mtimeMs / 1000));
});
test("preserveTransferredDestinationMtime skips a concurrently replaced local target", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-preserve-mtime-race-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const targetPath = path.join(tempDir, "copied.bin");
await fs.promises.writeFile(targetPath, Buffer.from("concurrent"));
const before = await fs.promises.stat(targetPath);
const sourceMtimeMs = 1_700_000_000_000; // 2023-11-14T22:13:20.000Z
// Identity recorded at publication no longer matches the pathname's file.
await transferBridge._preserveTransferredDestinationMtimeForTests({
targetType: "local",
targetPath,
publishedLocalIdentity: "999999:999999:7",
sourceSoftIdentity: { size: 7, mtimeMs: sourceMtimeMs },
});
const after = await fs.promises.stat(targetPath);
assert.equal(Math.floor(after.mtimeMs / 1000), Math.floor(before.mtimeMs / 1000));
// A vanished destination is likewise left alone.
await transferBridge._preserveTransferredDestinationMtimeForTests({
targetType: "local",
targetPath: path.join(tempDir, "missing.bin"),
publishedLocalIdentity: "999999:999999:7",
sourceSoftIdentity: { size: 7, mtimeMs: sourceMtimeMs },
});
});
test("preserveTransferredDestinationMtime stamps the published local inode", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-preserve-mtime-inode-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const targetPath = path.join(tempDir, "copied.bin");
await fs.promises.writeFile(targetPath, Buffer.from("payload"));
const publishedStat = await fs.promises.lstat(targetPath);
const sourceMtimeMs = 1_700_000_000_000; // 2023-11-14T22:13:20.000Z
await transferBridge._preserveTransferredDestinationMtimeForTests({
targetType: "local",
targetPath,
publishedLocalIdentity: transferBridge._stableLocalFileIdentityForTests(publishedStat),
sourceSoftIdentity: { size: 7, mtimeMs: sourceMtimeMs },
});
const after = await fs.promises.stat(targetPath);
assert.equal(Math.floor(after.mtimeMs / 1000), Math.floor(sourceMtimeMs / 1000));
});
test("local publication returns an identity usable for timestamp preservation", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-published-time-"));
t.after(() => fs.promises.rm(tempDir, { recursive: true, force: true }));
const stagedPath = path.join(tempDir, "stage");
const targetPath = path.join(tempDir, "target");
await fs.promises.writeFile(stagedPath, "payload");
let publishedLocalIdentity;
await transferBridge._promoteLocalTransferForTests(stagedPath, targetPath, {
onCommit(identity) { publishedLocalIdentity = identity; },
});
await transferBridge._preserveTransferredDestinationMtimeForTests({
targetType: "local", targetPath, publishedLocalIdentity,
sourceSoftIdentity: { mtimeMs: 1_700_000_000_000 },
});
assert.equal(Math.floor((await fs.promises.stat(targetPath)).mtimeMs / 1000), 1_700_000_000);
});
test("local timestamp preservation cannot stamp a replacement after identity verification", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stamp-race-"));
t.after(() => fs.promises.rm(tempDir, { recursive: true, force: true }));
const targetPath = path.join(tempDir, "target");
const oldPath = path.join(tempDir, "published");
await fs.promises.writeFile(targetPath, "payload");
const publishedLocalIdentity = transferBridge._stableLocalFileIdentityForTests(await fs.promises.stat(targetPath));
let replaced = false;
const replace = async () => {
if (replaced) return;
replaced = true;
await fs.promises.rename(targetPath, oldPath);
await fs.promises.writeFile(targetPath, "concurrent");
await originalUtimes(targetPath, 1_600_000_000, 1_600_000_000);
};
const originalUtimes = fs.promises.utimes;
const originalOpen = fs.promises.open;
t.mock.method(fs.promises, "utimes", async (name, ...args) => {
if (name === targetPath) await replace();
return originalUtimes(name, ...args);
});
t.mock.method(fs.promises, "open", async (...args) => {
const handle = await originalOpen(...args);
if (args[0] === targetPath) {
const utimes = handle.utimes.bind(handle);
handle.utimes = async (...times) => { await replace(); return utimes(...times); };
}
return handle;
});
await transferBridge._preserveTransferredDestinationMtimeForTests({
targetType: "local", targetPath, publishedLocalIdentity,
sourceSoftIdentity: { mtimeMs: 1_700_000_000_000 },
});
assert.equal(replaced, true);
assert.equal(Math.floor((await fs.promises.stat(targetPath)).mtimeMs / 1000), 1_600_000_000);
});
test("restoreRemoteUploadModeBestEffort times out hanging chmod", async () => {
const hangingClient = {
async chmod() {
await new Promise(() => {});
},
};
const warnings = [];
const originalWarn = console.warn;
console.warn = (...args) => { warnings.push(args.map(String).join(" ")); };
const startedAt = Date.now();
try {
await transferBridge._restoreRemoteUploadModeBestEffortForTests(
hangingClient,
"target",
"/usr/local/bin/tool",
"utf-8",
0o755,
{ timeoutMs: 40 },
);
} finally {
console.warn = originalWarn;
}
const elapsed = Date.now() - startedAt;
assert.ok(elapsed < 1500, `expected bounded chmod, elapsed=${elapsed}`);
assert.ok(
warnings.some((message) => /Failed to restore permissions|timed out/i.test(message)),
`expected chmod timeout warning, got ${JSON.stringify(warnings)}`,
);
});
test("preserveTransferredDestinationMtime times out hanging remote setStat", async () => {
let setStatStarted = false;
const hangingClient = {
sftp: {
readdir() {},
stat() {},
mkdir() {},
unlink() {},
},
async setStat() {
setStatStarted = true;
await new Promise(() => {});
},
};
transferBridge.init({ sftpClients: new Map([["target", hangingClient]]) });
const startedAt = Date.now();
await transferBridge._preserveTransferredDestinationMtimeForTests({
targetType: "sftp",
targetSftpId: "target",
targetPath: "/tmp/mtime-hang.bin",
sourceSoftIdentity: { size: 1, mtimeMs: 1_700_000_000_000 },
}, { timeoutMs: 40 });
const elapsed = Date.now() - startedAt;
assert.equal(setStatStarted, true);
assert.ok(elapsed < 1500, `expected bounded mtime stamp, elapsed=${elapsed}`);
});
test("local-to-local transfer preserves source mtime on the destination", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-local-mtime-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const sourcePath = path.join(tempDir, "source.bin");
const targetPath = path.join(tempDir, "target.bin");
await fs.promises.writeFile(sourcePath, Buffer.from("hello-mtime"));
const sourceMtimeMs = 1_600_000_000_000; // 2020-09-13T12:26:40.000Z
await fs.promises.utimes(sourcePath, new Date(sourceMtimeMs), new Date(sourceMtimeMs));
transferBridge.init({ sftpClients: new Map() });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "local-mtime-preserve",
sourcePath,
targetPath,
sourceType: "local",
targetType: "local",
totalBytes: 11,
resumable: false,
});
assert.equal(result.error, undefined);
const targetStat = await fs.promises.stat(targetPath);
assert.equal(Math.floor(targetStat.mtimeMs / 1000), Math.floor(sourceMtimeMs / 1000));
assert.deepEqual(await fs.promises.readFile(targetPath), Buffer.from("hello-mtime"));
});
test("waitForPendingWriteOpenPathGate timeout fails closed without clearing poison", async () => {
let resolveGate;
const gate = new Promise((resolve) => { resolveGate = resolve; });
let failCalls = 0;
const transfer = {
pendingWriteOpenPathGate: gate,
_resolvePendingWriteOpenPathGate() {
resolveGate();
transfer.pendingWriteOpenPathGate = null;
transfer._resolvePendingWriteOpenPathGate = null;
},
_failPendingWriteOpenPathGate() {
failCalls += 1;
},
};
await assert.rejects(
() => transferBridge._waitForPendingWriteOpenPathGateForTests(transfer, { timeoutMs: 30 }),
/Timed out waiting for prior write OPEN to settle/i,
);
assert.equal(transfer.pendingWriteOpenPathGate, gate, "timeout must not clear the published gate poison");
assert.equal(failCalls, 1, "timeout must poison the shared path gate for later waiters");
resolveGate();
});
test("in-place isolated OPEN channel error fails closed without waiting on shared fallback", async (t) => {
// Codex P1 on 3d4cecfa: keeping in-place poison without force-release must not
// fall through into concurrent-shared, which waits forever on the prior gate.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-inplace-open-terminal-poison-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 73);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/inplace-terminal-poison.bin";
const remoteFiles = new Map([[targetPath, Buffer.from("keep-original")]]);
let openCalls = 0;
let sharedOpenCalls = 0;
const isolatedSftp = createFastSftp({
open(_remotePath, flags, _callback) {
assert.equal(flags, "w");
openCalls += 1;
// Never invoke OPEN callback (dead channel after error).
},
write() {
throw new Error("WRITE must not run while OPEN is pending");
},
end() {},
fastPut() {
throw new Error("fastPut must not run after in-place OPEN poison");
},
});
const sharedSftp = createFastSftp({
open() {
sharedOpenCalls += 1;
throw new Error("shared OPEN must not run after terminal in-place poison");
},
createWriteStream() {
throw new Error("serial WriteStream must not run");
},
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
},
});
const client = {
sftp: sharedSftp,
async lstat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
};
},
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete() {},
client: {
sftp(callback) {
callback(null, isolatedSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const running = transferBridge.startTransfer({ sender }, {
transferId: "inplace-open-terminal-poison",
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
});
const opened = await waitUntil(() => openCalls >= 1, 2000);
assert.ok(opened, "expected isolated in-place OPEN to stall");
isolatedSftp.emit("error", new Error("isolated channel died during in-place OPEN"));
const result = await Promise.race([
running,
new Promise((_, reject) => setTimeout(
() => reject(new Error("transfer hung behind permanent in-place OPEN poison")),
4000,
)),
]);
assert.ok(result.error, "expected fail-closed transfer");
assert.match(result.error, /isolated channel died|pipelined upload failed/i);
assert.equal(sharedOpenCalls, 0, "concurrent-shared must not wait on poisoned in-place gate");
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
});
test("in-place isolated OPEN keeps path gate until late callback after channel error", async (t) => {
// Codex P1 on e2cc8241: force-releasing an in-place truncating OPEN lets
// fastPut complete, then a late OPEN truncates the reported-success file.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-inplace-open-no-force-release-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const payload = Buffer.alloc(TRANSFER_CHUNK_SIZE, 71);
const localPath = path.join(tempDir, "upload.bin");
await fs.promises.writeFile(localPath, payload);
const targetPath = "/tmp/inplace-no-force.bin";
const existingPayload = Buffer.from("keep-original");
const remoteFiles = new Map([[targetPath, Buffer.from(existingPayload)]]);
const eventLog = [];
let releaseOpen = null;
let openCalls = 0;
let fastPutCalls = 0;
const isolatedSftp = createFastSftp({
open(remotePath, flags, callback) {
assert.equal(flags, "w");
openCalls += 1;
const key = String(remotePath);
releaseOpen = () => {
remoteFiles.set(key, Buffer.alloc(0));
eventLog.push(`late-open-truncate:${key}`);
callback(null, Buffer.from(`handle:${key}`));
};
},
write() {
throw new Error("WRITE must not run while OPEN is pending");
},
close(_handle, callback) {
eventLog.push("close");
callback(null);
},
end() {
eventLog.push("isolated-end");
},
fastPut() {
fastPutCalls += 1;
throw new Error("fastPut must not run while in-place OPEN poison is held");
},
});
const sharedSftp = createFastSftp({
open() {
throw new Error("shared OPEN must not run while isolated in-place gate is held");
},
createWriteStream() {
throw new Error("serial WriteStream must not run");
},
lstat(remotePath, callback) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
callback(error);
return;
}
callback(null, {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
});
},
});
const client = {
sftp: sharedSftp,
async lstat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return {
size: remoteFiles.get(key).length,
mode: 0o120777,
isDirectory: () => false,
isSymbolicLink: () => true,
};
},
async stat(remotePath) {
const key = String(remotePath);
if (!remoteFiles.has(key)) {
const error = new Error("ENOENT");
error.code = 2;
throw error;
}
return { size: remoteFiles.get(key).length };
},
rename() {
return Promise.resolve();
},
async delete() {},
client: {
sftp(callback) {
callback(null, isolatedSftp);
},
},
};
transferBridge.init({ sftpClients: new Map([["target", client]]) });
const sender = createSender();
const transferId = "inplace-open-no-force-release";
const running = transferBridge.startTransfer({ sender }, {
transferId,
sourcePath: localPath,
targetPath,
sourceType: "local",
targetType: "sftp",
targetSftpId: "target",
totalBytes: payload.length,
resumable: false,
skipAdmission: true,
});
const opened = await waitUntil(() => openCalls >= 1, 2000);
assert.ok(opened, "expected isolated in-place OPEN to stall");
isolatedSftp.emit("error", new Error("isolated channel died during in-place OPEN"));
// Former force-release window (2s). Path gate must still block fallbacks.
await new Promise((resolve) => setTimeout(resolve, 2500));
assert.equal(fastPutCalls, 0, "fastPut must not run while in-place OPEN is unsettled");
assert.deepEqual(remoteFiles.get(targetPath), existingPayload, "destination must stay intact before late OPEN");
assert.equal(typeof releaseOpen, "function");
releaseOpen();
const result = await Promise.race([
running,
new Promise((_, reject) => setTimeout(() => reject(new Error("transfer hung after late in-place OPEN")), 8000)),
]);
assert.ok(result.error, "expected transfer to fail closed rather than report success after late truncate");
assert.equal(sender.sent.some((entry) => entry.channel === "netcatty:transfer:complete"), false);
assert.ok(eventLog.includes(`late-open-truncate:${targetPath}`) || eventLog.includes("close"));
});
test("preserveTransferredDestinationMtime stamps a write-only local target", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-preserve-mtime-wo-"));
t.after(async () => fs.promises.rm(tempDir, { recursive: true, force: true }));
const targetPath = path.join(tempDir, "copied.bin");
await fs.promises.writeFile(targetPath, Buffer.from("payload"));
await fs.promises.chmod(targetPath, 0o200);
const sourceMtimeMs = 1_700_000_000_000;
await transferBridge._preserveTransferredDestinationMtimeForTests({
targetType: "local",
targetPath,
sourceSoftIdentity: { size: 7, mtimeMs: sourceMtimeMs },
});
const after = await fs.promises.stat(targetPath);
assert.equal(Math.floor(after.mtimeMs / 1000), 1_700_000_000);
assert.equal(after.mode & 0o777, 0o200);
});
for (const failureMode of ["rejection", "timeout"]) {
test(`resumable local transfer retries a failed prepared-file timestamp: ${failureMode}`, async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-mtime-retry-"));
t.after(() => fs.promises.rm(tempDir, { recursive: true, force: true }));
const sourcePath = path.join(tempDir, "source");
const targetPath = path.join(tempDir, "target");
await fs.promises.writeFile(sourcePath, "payload");
await fs.promises.utimes(sourcePath, 1_600_000_000, 1_600_000_000);
let preparationFailed = false;
const utimes = fs.promises.utimes;
t.mock.method(fs.promises, "utimes", async (name, ...args) => {
if (String(name).endsWith(".ready")) {
preparationFailed = true;
if (failureMode === "timeout") return new Promise(() => {});
throw Object.assign(new Error("transient metadata failure"), { code: "EIO" });
}
return utimes(name, ...args);
});
transferBridge.init({ sftpClients: new Map() });
const result = await transferBridge.startTransfer({ sender: createSender() }, {
transferId: "local-mtime-retry", sourcePath, targetPath,
sourceType: "local", targetType: "local", totalBytes: 7, resumable: true,
});
assert.equal(result.error, undefined);
assert.equal(preparationFailed, true);
assert.equal(Math.floor((await fs.promises.stat(targetPath)).mtimeMs / 1000), 1_600_000_000);
assert.equal(await fs.promises.readFile(targetPath, "utf8"), "payload");
});
}