[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
532
electron/bridges/compressUploadBridge.lifecycle.test.cjs
Normal file
532
electron/bridges/compressUploadBridge.lifecycle.test.cjs
Normal file
@@ -0,0 +1,532 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { EventEmitter } = require("node:events");
|
||||
const test = require("node:test");
|
||||
|
||||
const compressUploadBridge = require("./compressUploadBridge.cjs");
|
||||
|
||||
test("compressed commits to the same remote folder are serialized", async () => {
|
||||
let active = 0;
|
||||
let maxActive = 0;
|
||||
const order = [];
|
||||
const run = compressUploadBridge._runWithCompressionTargetLockForTests;
|
||||
const first = run("sftp-1:/target/folder", null, async () => {
|
||||
active += 1;
|
||||
maxActive = Math.max(maxActive, active);
|
||||
order.push("first-start");
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
order.push("first-end");
|
||||
active -= 1;
|
||||
});
|
||||
const second = run("sftp-1:/target/folder", null, async () => {
|
||||
active += 1;
|
||||
maxActive = Math.max(maxActive, active);
|
||||
order.push("second-start");
|
||||
active -= 1;
|
||||
});
|
||||
await Promise.all([first, second]);
|
||||
assert.equal(maxActive, 1);
|
||||
assert.deepEqual(order, ["first-start", "first-end", "second-start"]);
|
||||
assert.equal(compressUploadBridge._getCompressionTargetLockCountForTests(), 0);
|
||||
});
|
||||
|
||||
test("compressed commits through different SFTP channels share the endpoint target lock", async () => {
|
||||
compressUploadBridge.init({
|
||||
sftpClients: new Map([
|
||||
["window-a-sftp", { __netcattyEndpointKey: "host|route|auth" }],
|
||||
["window-b-sftp", { __netcattyEndpointKey: "host|route|auth" }],
|
||||
]),
|
||||
transferBridge: {},
|
||||
});
|
||||
const resolveKey = compressUploadBridge._resolveCompressionTargetKeyForTests;
|
||||
const firstKey = resolveKey("window-a-sftp", "/target/./nested/..", "folder");
|
||||
const secondKey = resolveKey("window-b-sftp", "/target", "folder");
|
||||
assert.equal(firstKey, secondKey);
|
||||
|
||||
let active = 0;
|
||||
let maxActive = 0;
|
||||
let releaseFirst;
|
||||
const run = compressUploadBridge._runWithCompressionTargetLockForTests;
|
||||
const first = run(firstKey, null, async () => {
|
||||
active += 1;
|
||||
maxActive = Math.max(maxActive, active);
|
||||
await new Promise((resolve) => { releaseFirst = resolve; });
|
||||
active -= 1;
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const second = run(secondKey, null, async () => {
|
||||
active += 1;
|
||||
maxActive = Math.max(maxActive, active);
|
||||
active -= 1;
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(maxActive, 1);
|
||||
releaseFirst();
|
||||
await Promise.all([first, second]);
|
||||
assert.equal(maxActive, 1);
|
||||
});
|
||||
|
||||
test("an aborted lock waiter cannot let a later compressed commit bypass the active owner", async () => {
|
||||
const run = compressUploadBridge._runWithCompressionTargetLockForTests;
|
||||
let releaseFirst;
|
||||
let thirdStarted = false;
|
||||
const first = run("sftp-1:/target/abort", null, () => new Promise((resolve) => {
|
||||
releaseFirst = resolve;
|
||||
}));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const controller = new AbortController();
|
||||
const second = run("sftp-1:/target/abort", controller.signal, async () => {});
|
||||
controller.abort(new Error("cancelled waiter"));
|
||||
await assert.rejects(second, /cancelled waiter/);
|
||||
const third = run("sftp-1:/target/abort", null, async () => { thirdStarted = true; });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(thirdStarted, false);
|
||||
releaseFirst();
|
||||
await Promise.all([first, third]);
|
||||
assert.equal(thirdStarted, true);
|
||||
});
|
||||
|
||||
test("worker-backed compressed pause and resume publish ordered lifecycle", async () => {
|
||||
const handlers = new Map();
|
||||
const events = [];
|
||||
let finishStart;
|
||||
const startGate = new Promise((resolve) => { finishStart = resolve; });
|
||||
compressUploadBridge.init({
|
||||
sftpClients: new Map(),
|
||||
transferBridge: {
|
||||
acquireTransferSessionLeases(_leaseId, payload) { return [payload.targetSftpId]; },
|
||||
releaseTransferSessionLeases() {},
|
||||
broadcastGlobalTransferEvent(event) { events.push(event); },
|
||||
},
|
||||
});
|
||||
compressUploadBridge.registerHandlers({
|
||||
handle(channel, handler) { handlers.set(channel, handler); },
|
||||
}, {
|
||||
terminalWorkerManager: {
|
||||
async request(channel) {
|
||||
if (channel === "netcatty:compress:start") return startGate;
|
||||
if (channel === "netcatty:compress:pause") {
|
||||
return { success: true, deferred: false };
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const starting = handlers.get("netcatty:compress:start")(
|
||||
{ sender: { id: 1 } },
|
||||
{ compressionId: "compressed-worker" },
|
||||
);
|
||||
await handlers.get("netcatty:compress:pause")(
|
||||
{ sender: { id: 1 } },
|
||||
{ compressionId: "compressed-worker" },
|
||||
);
|
||||
await handlers.get("netcatty:compress:resume")(
|
||||
{ sender: { id: 1 } },
|
||||
{ compressionId: "compressed-worker" },
|
||||
);
|
||||
|
||||
assert.deepEqual(events, [
|
||||
{
|
||||
type: "pausing",
|
||||
transferId: "compressed-worker",
|
||||
lifecycleEpoch: 1,
|
||||
lifecycleState: "pausing",
|
||||
},
|
||||
{
|
||||
type: "paused",
|
||||
transferId: "compressed-worker",
|
||||
lifecycleEpoch: 1,
|
||||
lifecycleState: "paused",
|
||||
},
|
||||
{
|
||||
type: "resumed",
|
||||
transferId: "compressed-worker",
|
||||
lifecycleEpoch: 2,
|
||||
lifecycleState: "transferring",
|
||||
},
|
||||
]);
|
||||
finishStart({ compressionId: "compressed-worker", success: true });
|
||||
await starting;
|
||||
assert.equal(compressUploadBridge._getWorkerCompressionLifecycleEpochCountForTests(), 0);
|
||||
});
|
||||
|
||||
test("worker compression lifecycle epochs survive a reused id and clear after the latest start settles", async () => {
|
||||
const handlers = new Map();
|
||||
let finishFirst;
|
||||
let finishSecond;
|
||||
const firstGate = new Promise((resolve) => { finishFirst = resolve; });
|
||||
const secondGate = new Promise((resolve) => { finishSecond = resolve; });
|
||||
let startCalls = 0;
|
||||
compressUploadBridge.registerHandlers({
|
||||
handle(channel, handler) { handlers.set(channel, handler); },
|
||||
}, {
|
||||
terminalWorkerManager: {
|
||||
request(channel) {
|
||||
if (channel === "netcatty:compress:start") {
|
||||
startCalls += 1;
|
||||
return startCalls === 1 ? firstGate : secondGate;
|
||||
}
|
||||
return Promise.resolve({ success: true });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstStarting = handlers.get("netcatty:compress:start")(
|
||||
{ sender: { id: 1 } },
|
||||
{ compressionId: "compression-cache-lifetime" },
|
||||
);
|
||||
const secondStarting = handlers.get("netcatty:compress:start")(
|
||||
{ sender: { id: 1 } },
|
||||
{ compressionId: "compression-cache-lifetime" },
|
||||
);
|
||||
assert.equal(compressUploadBridge._getWorkerCompressionLifecycleEpochCountForTests(), 1);
|
||||
|
||||
finishFirst({ compressionId: "compression-cache-lifetime", success: true });
|
||||
await firstStarting;
|
||||
assert.equal(
|
||||
compressUploadBridge._getWorkerCompressionLifecycleEpochCountForTests(),
|
||||
1,
|
||||
"an older completion must not clear the newer start's lifecycle state",
|
||||
);
|
||||
|
||||
finishSecond({ compressionId: "compression-cache-lifetime", success: true });
|
||||
await secondStarting;
|
||||
assert.equal(compressUploadBridge._getWorkerCompressionLifecycleEpochCountForTests(), 0);
|
||||
});
|
||||
|
||||
test("worker compression lifecycle epoch clears after a synchronous start failure", () => {
|
||||
const handlers = new Map();
|
||||
compressUploadBridge.registerHandlers({
|
||||
handle(channel, handler) { handlers.set(channel, handler); },
|
||||
}, {
|
||||
terminalWorkerManager: {
|
||||
request(channel) {
|
||||
if (channel === "netcatty:compress:start") throw new Error("worker unavailable");
|
||||
return Promise.resolve({ success: true });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.throws(() => handlers.get("netcatty:compress:start")(
|
||||
{ sender: { id: 1 } },
|
||||
{ compressionId: "compression-cache-sync-failure" },
|
||||
), /worker unavailable/);
|
||||
assert.equal(compressUploadBridge._getWorkerCompressionLifecycleEpochCountForTests(), 0);
|
||||
});
|
||||
|
||||
test("cancelling while the remote tar probe is pending publishes cancelled", async () => {
|
||||
const handlers = new Map();
|
||||
const events = [];
|
||||
let remoteProbeStarted;
|
||||
const remoteProbeGate = new Promise((resolve) => { remoteProbeStarted = resolve; });
|
||||
const remoteStream = new (require("node:events").EventEmitter)();
|
||||
remoteStream.stderr = new (require("node:events").EventEmitter)();
|
||||
remoteStream.close = () => {};
|
||||
remoteStream.end = () => {};
|
||||
remoteStream.destroy = () => {};
|
||||
const sender = { id: 1, send() {} };
|
||||
|
||||
compressUploadBridge.init({
|
||||
sftpClients: new Map([["sftp-probe", {
|
||||
client: {
|
||||
exec(_command, callback) {
|
||||
callback(null, remoteStream);
|
||||
remoteProbeStarted();
|
||||
},
|
||||
},
|
||||
}]]),
|
||||
transferBridge: {
|
||||
acquireTransferSessionLeases(_leaseId, payload) { return [payload.targetSftpId]; },
|
||||
releaseTransferSessionLeases() {},
|
||||
broadcastGlobalTransferEvent(event) { events.push(event); },
|
||||
},
|
||||
});
|
||||
compressUploadBridge.registerHandlers({
|
||||
handle(channel, handler) { handlers.set(channel, handler); },
|
||||
});
|
||||
|
||||
const starting = handlers.get("netcatty:compress:start")({ sender }, {
|
||||
compressionId: "cancel-during-probe",
|
||||
folderPath: "/unused",
|
||||
targetPath: "/unused",
|
||||
sftpId: "sftp-probe",
|
||||
folderName: "unused",
|
||||
totalBytes: 1,
|
||||
});
|
||||
await remoteProbeGate;
|
||||
await handlers.get("netcatty:compress:cancel")({ sender }, {
|
||||
compressionId: "cancel-during-probe",
|
||||
});
|
||||
await starting;
|
||||
|
||||
assert.equal(events.at(-1)?.type, "cancelled");
|
||||
assert.equal(events.some((event) => event.type === "failed"), false);
|
||||
});
|
||||
|
||||
test("compressed upload holds its SFTP session until remote extraction settles", async (t) => {
|
||||
const handlers = new Map();
|
||||
const calls = [];
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-compress-lease-"));
|
||||
const folderPath = path.join(root, "folder");
|
||||
fs.mkdirSync(folderPath);
|
||||
fs.writeFileSync(path.join(folderPath, "file.txt"), "payload");
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
let extractionStarted;
|
||||
const extractionGate = new Promise((resolve) => { extractionStarted = resolve; });
|
||||
let cleanupStarted;
|
||||
const cleanupGate = new Promise((resolve) => { cleanupStarted = resolve; });
|
||||
const streams = [];
|
||||
const cleanupStreams = [];
|
||||
let extractionStream = null;
|
||||
const makeStream = () => {
|
||||
const stream = new EventEmitter();
|
||||
stream.stderr = new EventEmitter();
|
||||
stream.close = () => stream.emit("close", 1);
|
||||
stream.end = () => {};
|
||||
stream.destroy = () => {};
|
||||
streams.push(stream);
|
||||
return stream;
|
||||
};
|
||||
const sshClient = {
|
||||
exec(command, callback) {
|
||||
const stream = makeStream();
|
||||
callback(null, stream);
|
||||
if (command === "tar --version") {
|
||||
process.nextTick(() => {
|
||||
stream.emit("data", Buffer.from("tar"));
|
||||
stream.emit("close", 0);
|
||||
});
|
||||
} else if (command.includes("tar -xzf")) {
|
||||
extractionStream = stream;
|
||||
extractionStarted();
|
||||
} else {
|
||||
cleanupStreams.push(stream);
|
||||
cleanupStarted();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
compressUploadBridge.init({
|
||||
sftpClients: new Map([["sftp-compressed", {
|
||||
client: sshClient,
|
||||
__netcattyEndpointKey: "endpoint:compressed",
|
||||
}]]),
|
||||
transferBridge: {
|
||||
acquireTransferSessionLeases(leaseId, payload) {
|
||||
calls.push(`acquire:${leaseId}:${payload.targetSftpId}`);
|
||||
return [payload.targetSftpId];
|
||||
},
|
||||
releaseTransferSessionLeases(leaseId, sftpIds) {
|
||||
calls.push(`release:${leaseId}:${sftpIds.join(",")}`);
|
||||
},
|
||||
async startInternalTransfer() {
|
||||
calls.push("upload");
|
||||
return { success: true };
|
||||
},
|
||||
async cancelTransfer() { return { success: true }; },
|
||||
broadcastGlobalTransferEvent() {},
|
||||
},
|
||||
});
|
||||
compressUploadBridge.registerHandlers({
|
||||
handle(channel, handler) { handlers.set(channel, handler); },
|
||||
});
|
||||
|
||||
const sender = { id: 1, send() {} };
|
||||
const starting = handlers.get("netcatty:compress:start")({ sender }, {
|
||||
compressionId: "compressed-lifecycle",
|
||||
folderPath,
|
||||
targetPath: "/tmp",
|
||||
sftpId: "sftp-compressed",
|
||||
folderName: "folder",
|
||||
totalBytes: 7,
|
||||
});
|
||||
let startSettled = false;
|
||||
void starting.then(() => { startSettled = true; }, () => { startSettled = true; });
|
||||
await extractionGate;
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"acquire:compress-lifecycle:compressed-lifecycle:sftp-compressed",
|
||||
"upload",
|
||||
]);
|
||||
|
||||
extractionStream.emit("close", 0);
|
||||
await cleanupGate;
|
||||
assert.deepEqual(
|
||||
calls,
|
||||
[
|
||||
"acquire:compress-lifecycle:compressed-lifecycle:sftp-compressed",
|
||||
"upload",
|
||||
],
|
||||
"the outer SFTP lease must remain held while remote cleanup is still running",
|
||||
);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(startSettled, false, "compressed upload must await remote cleanup before releasing its lease");
|
||||
for (const stream of cleanupStreams) stream.emit("close", 0);
|
||||
await starting;
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"acquire:compress-lifecycle:compressed-lifecycle:sftp-compressed",
|
||||
"upload",
|
||||
"release:compress-lifecycle:compressed-lifecycle:sftp-compressed",
|
||||
]);
|
||||
});
|
||||
|
||||
test("compressed upload never deletes AppleDouble files across the target parent", async (t) => {
|
||||
const remoteCommands = [];
|
||||
const harness = createSuccessfulCompressionHarness(t, {
|
||||
onRemoteCommand(command) { remoteCommands.push(command); },
|
||||
});
|
||||
|
||||
const result = await harness.handlers.get("netcatty:compress:start")({ sender: harness.sender }, {
|
||||
compressionId: "no-parent-cleanup",
|
||||
folderPath: harness.folderPath,
|
||||
targetPath: "/srv/shared",
|
||||
sftpId: "sftp-harness",
|
||||
folderName: "folder",
|
||||
totalBytes: 7,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(
|
||||
remoteCommands.some((command) => /\bfind\b[\s\S]*\._\*/.test(command)),
|
||||
false,
|
||||
"cleanup must not scan and delete pre-existing ._* files under the whole target parent",
|
||||
);
|
||||
assert.equal(
|
||||
remoteCommands.some((command) => /^rm -f /.test(command)),
|
||||
true,
|
||||
"the uploaded archive still receives a bounded best-effort cleanup",
|
||||
);
|
||||
});
|
||||
|
||||
function createSuccessfulCompressionHarness(t, { onRemoteCommand, onUpload } = {}) {
|
||||
const handlers = new Map();
|
||||
const events = [];
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-compress-harness-"));
|
||||
const folderPath = path.join(root, "folder");
|
||||
fs.mkdirSync(folderPath);
|
||||
fs.writeFileSync(path.join(folderPath, "file.txt"), "payload");
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const sshClient = {
|
||||
writable: true,
|
||||
exec(command, callback) {
|
||||
onRemoteCommand?.(command);
|
||||
const stream = new EventEmitter();
|
||||
stream.stderr = new EventEmitter();
|
||||
stream.close = () => {};
|
||||
stream.end = () => {};
|
||||
stream.destroy = () => {};
|
||||
callback(null, stream);
|
||||
process.nextTick(() => {
|
||||
if (command === "tar --version") stream.emit("data", Buffer.from("tar"));
|
||||
stream.emit("close", 0);
|
||||
});
|
||||
},
|
||||
};
|
||||
compressUploadBridge._resetCompressionSupportCacheForTests?.();
|
||||
compressUploadBridge.init({
|
||||
sftpClients: new Map([["sftp-harness", {
|
||||
client: sshClient,
|
||||
__netcattyEndpointKey: "endpoint:harness",
|
||||
}]]),
|
||||
transferBridge: {
|
||||
acquireTransferSessionLeases(_leaseId, payload) { return [payload.targetSftpId]; },
|
||||
releaseTransferSessionLeases() {},
|
||||
broadcastGlobalTransferEvent(event) { events.push(event); },
|
||||
async startInternalTransfer(_event, payload) {
|
||||
onUpload?.(payload);
|
||||
return { success: true };
|
||||
},
|
||||
async cancelTransfer() { return { success: true }; },
|
||||
},
|
||||
});
|
||||
compressUploadBridge.registerHandlers({ handle(channel, handler) { handlers.set(channel, handler); } });
|
||||
return { handlers, events, folderPath, sender: { id: 1, send() {} } };
|
||||
}
|
||||
|
||||
test("support preflight is reused by the immediately following compressed start", async (t) => {
|
||||
let remoteProbeCalls = 0;
|
||||
const harness = createSuccessfulCompressionHarness(t, {
|
||||
onRemoteCommand(command) {
|
||||
if (command === "tar --version") remoteProbeCalls += 1;
|
||||
},
|
||||
});
|
||||
const support = await harness.handlers.get("netcatty:compress:checkSupport")(
|
||||
{ sender: harness.sender },
|
||||
{ sftpId: "sftp-harness" },
|
||||
);
|
||||
assert.equal(support.supported, true);
|
||||
|
||||
const result = await harness.handlers.get("netcatty:compress:start")({ sender: harness.sender }, {
|
||||
compressionId: "cached-support-start",
|
||||
folderPath: harness.folderPath,
|
||||
targetPath: "/tmp",
|
||||
sftpId: "sftp-harness",
|
||||
folderName: "folder",
|
||||
totalBytes: 7,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(remoteProbeCalls, 1);
|
||||
});
|
||||
|
||||
test("remote archive paths are unique for concurrent operation identities", async (t) => {
|
||||
const uploadedPaths = [];
|
||||
const harness = createSuccessfulCompressionHarness(t, {
|
||||
onUpload(payload) { uploadedPaths.push(payload.targetPath); },
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
harness.handlers.get("netcatty:compress:start")({ sender: harness.sender }, {
|
||||
compressionId: "archive-one",
|
||||
folderPath: harness.folderPath,
|
||||
targetPath: "/tmp",
|
||||
sftpId: "sftp-harness",
|
||||
folderName: "folder",
|
||||
totalBytes: 7,
|
||||
}),
|
||||
harness.handlers.get("netcatty:compress:start")({ sender: harness.sender }, {
|
||||
compressionId: "archive-two",
|
||||
folderPath: harness.folderPath,
|
||||
targetPath: "/tmp",
|
||||
sftpId: "sftp-harness",
|
||||
folderName: "folder",
|
||||
totalBytes: 7,
|
||||
}),
|
||||
]);
|
||||
|
||||
assert.equal(uploadedPaths.length, 2);
|
||||
assert.notEqual(uploadedPaths[0], uploadedPaths[1]);
|
||||
});
|
||||
|
||||
test("a late cancel after completion cannot overwrite the completed terminal event", async (t) => {
|
||||
const harness = createSuccessfulCompressionHarness(t);
|
||||
await harness.handlers.get("netcatty:compress:start")({ sender: harness.sender }, {
|
||||
compressionId: "completed-before-cancel",
|
||||
folderPath: harness.folderPath,
|
||||
targetPath: "/tmp",
|
||||
sftpId: "sftp-harness",
|
||||
folderName: "folder",
|
||||
totalBytes: 7,
|
||||
});
|
||||
const terminalBefore = harness.events.filter((event) => (
|
||||
event.transferId === "completed-before-cancel"
|
||||
&& ["completed", "cancelled", "failed"].includes(event.type)
|
||||
));
|
||||
assert.deepEqual(terminalBefore.map((event) => event.type), ["completed"]);
|
||||
|
||||
await harness.handlers.get("netcatty:compress:cancel")({ sender: harness.sender }, {
|
||||
compressionId: "completed-before-cancel",
|
||||
});
|
||||
const terminalAfter = harness.events.filter((event) => (
|
||||
event.transferId === "completed-before-cancel"
|
||||
&& ["completed", "cancelled", "failed"].includes(event.type)
|
||||
));
|
||||
assert.deepEqual(terminalAfter.map((event) => event.type), ["completed"]);
|
||||
});
|
||||
Reference in New Issue
Block a user