[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:
246
electron/preload/stageUploadFile.test.cjs
Normal file
246
electron/preload/stageUploadFile.test.cjs
Normal file
@@ -0,0 +1,246 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
const { stageRendererFileToTemp } = require("./stageUploadFile.cjs");
|
||||
|
||||
function createChunkedFile(chunks) {
|
||||
return {
|
||||
stream: () => new ReadableStream({
|
||||
start(controller) {
|
||||
for (const chunk of chunks) controller.enqueue(new Uint8Array(chunk));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createShortWriteFs(writeSizes, onWrite = null) {
|
||||
const writeCalls = [];
|
||||
let writeIndex = 0;
|
||||
return {
|
||||
writeCalls,
|
||||
fsImpl: {
|
||||
promises: {
|
||||
open: async (...args) => {
|
||||
const handle = await fs.promises.open(...args);
|
||||
return {
|
||||
write: async (buffer, offset = 0, length = buffer.length - offset, position = null) => {
|
||||
const requestedLength = Math.max(0, Number(length));
|
||||
const configuredLength = writeSizes[writeIndex] ?? requestedLength;
|
||||
const actualLength = Math.min(requestedLength, configuredLength);
|
||||
writeCalls.push({ offset, length: requestedLength, position, actualLength });
|
||||
const result = await handle.write(buffer, offset, actualLength, position);
|
||||
writeIndex += 1;
|
||||
await onWrite?.({ writeIndex, result });
|
||||
return result;
|
||||
},
|
||||
close: () => handle.close(),
|
||||
};
|
||||
},
|
||||
unlink: (...args) => fs.promises.unlink(...args),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("pathless renderer files stream into a controlled temp file without arrayBuffer", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
let arrayBufferCalls = 0;
|
||||
const chunks = [new Uint8Array([1, 2]), new Uint8Array([3, 4, 5])];
|
||||
const file = {
|
||||
arrayBuffer: async () => { arrayBufferCalls += 1; return new ArrayBuffer(0); },
|
||||
stream: () => new ReadableStream({
|
||||
start(controller) {
|
||||
for (const chunk of chunks) controller.enqueue(chunk);
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
};
|
||||
assert.equal(await stageRendererFileToTemp(file, localPath, fs), localPath);
|
||||
assert.deepEqual(await fs.promises.readFile(localPath), Buffer.from([1, 2, 3, 4, 5]));
|
||||
assert.equal(arrayBufferCalls, 0);
|
||||
});
|
||||
|
||||
test("a partial file-handle write retries the unwritten suffix", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-short-write-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
const { fsImpl, writeCalls } = createShortWriteFs([2]);
|
||||
|
||||
assert.equal(
|
||||
await stageRendererFileToTemp(createChunkedFile([[1, 2, 3, 4, 5]]), localPath, fsImpl),
|
||||
localPath,
|
||||
);
|
||||
assert.deepEqual(await fs.promises.readFile(localPath), Buffer.from([1, 2, 3, 4, 5]));
|
||||
assert.deepEqual(writeCalls.map(({ offset, length, position }) => ({ offset, length, position })), [
|
||||
{ offset: 0, length: 5, position: 0 },
|
||||
{ offset: 2, length: 3, position: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("multiple partial writes across chunks preserve every byte and advance offsets", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-many-short-writes-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
const { fsImpl, writeCalls } = createShortWriteFs([1, 2, 1, 1]);
|
||||
|
||||
await stageRendererFileToTemp(
|
||||
createChunkedFile([[10, 11, 12, 13], [20, 21]]),
|
||||
localPath,
|
||||
fsImpl,
|
||||
);
|
||||
|
||||
assert.deepEqual(await fs.promises.readFile(localPath), Buffer.from([10, 11, 12, 13, 20, 21]));
|
||||
assert.deepEqual(writeCalls.map(({ offset, length, position }) => ({ offset, length, position })), [
|
||||
{ offset: 0, length: 4, position: 0 },
|
||||
{ offset: 1, length: 3, position: 1 },
|
||||
{ offset: 3, length: 1, position: 3 },
|
||||
{ offset: 0, length: 2, position: 4 },
|
||||
{ offset: 1, length: 1, position: 5 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("a zero-byte file-handle write fails instead of spinning and removes the temp file", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-zero-write-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
const { fsImpl, writeCalls } = createShortWriteFs([0]);
|
||||
|
||||
await assert.rejects(
|
||||
stageRendererFileToTemp(createChunkedFile([[1, 2, 3]]), localPath, fsImpl),
|
||||
/Unable to stage the complete upload file/,
|
||||
);
|
||||
assert.equal(writeCalls.length, 1);
|
||||
await assert.rejects(fs.promises.stat(localPath), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("cancelling after a partial write stops the current chunk and removes the temp file", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-cancel-short-write-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
const controller = new AbortController();
|
||||
const { fsImpl, writeCalls } = createShortWriteFs([2], ({ writeIndex }) => {
|
||||
if (writeIndex === 1) controller.abort(new Error("cancel mid-chunk"));
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
stageRendererFileToTemp(
|
||||
createChunkedFile([[1, 2, 3, 4, 5]]),
|
||||
localPath,
|
||||
fsImpl,
|
||||
controller.signal,
|
||||
),
|
||||
/cancel mid-chunk/,
|
||||
);
|
||||
assert.equal(writeCalls.length, 1);
|
||||
await assert.rejects(fs.promises.stat(localPath), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("failed pathless-file staging removes its partial temp file", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-fail-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
let reads = 0;
|
||||
const file = {
|
||||
stream: () => ({
|
||||
getReader: () => ({
|
||||
read: async () => {
|
||||
reads += 1;
|
||||
if (reads === 1) return { done: false, value: new Uint8Array([1]) };
|
||||
throw new Error("source failed");
|
||||
},
|
||||
releaseLock: () => {},
|
||||
}),
|
||||
}),
|
||||
};
|
||||
await assert.rejects(stageRendererFileToTemp(file, localPath, fs), /source failed/);
|
||||
await assert.rejects(fs.promises.stat(localPath), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("a synchronous File.stream failure closes the new handle and removes the temp file", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-stream-init-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
|
||||
await assert.rejects(stageRendererFileToTemp({
|
||||
stream() {
|
||||
throw new Error("stream init failed");
|
||||
},
|
||||
}, localPath, fs), /stream init failed/);
|
||||
|
||||
await assert.rejects(fs.promises.stat(localPath), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("a synchronous getReader failure closes the new handle and removes the temp file", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-reader-init-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
|
||||
await assert.rejects(stageRendererFileToTemp({
|
||||
stream: () => ({
|
||||
getReader() {
|
||||
throw new Error("reader init failed");
|
||||
},
|
||||
}),
|
||||
}, localPath, fs), /reader init failed/);
|
||||
|
||||
await assert.rejects(fs.promises.stat(localPath), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("stream initialization failure still closes the handle when temp deletion fails", async () => {
|
||||
let closeCalls = 0;
|
||||
let unlinkCalls = 0;
|
||||
const fakeFs = {
|
||||
promises: {
|
||||
open: async () => ({
|
||||
close: async () => { closeCalls += 1; },
|
||||
}),
|
||||
unlink: async () => {
|
||||
unlinkCalls += 1;
|
||||
throw new Error("delete denied");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await assert.rejects(stageRendererFileToTemp({
|
||||
stream() {
|
||||
throw new Error("stream init failed");
|
||||
},
|
||||
}, "/controlled/upload.part", fakeFs), /stream init failed/);
|
||||
|
||||
assert.equal(unlinkCalls, 1);
|
||||
assert.ok(closeCalls >= 1, "the file handle must close even if cleanup unlink fails");
|
||||
});
|
||||
|
||||
test("cancelling a blocked pathless-file read closes it and removes the partial file", async (t) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-upload-cancel-"));
|
||||
t.after(() => fs.promises.rm(dir, { recursive: true, force: true }));
|
||||
const localPath = path.join(dir, "upload.part");
|
||||
const controller = new AbortController();
|
||||
let cancelCalls = 0;
|
||||
let finishRead;
|
||||
const file = {
|
||||
stream: () => ({
|
||||
getReader: () => ({
|
||||
read: () => new Promise((resolve) => { finishRead = resolve; }),
|
||||
cancel: async () => {
|
||||
cancelCalls += 1;
|
||||
finishRead?.({ done: true });
|
||||
},
|
||||
releaseLock: () => {},
|
||||
}),
|
||||
}),
|
||||
};
|
||||
const staging = stageRendererFileToTemp(file, localPath, fs, controller.signal);
|
||||
while (!finishRead) await new Promise((resolve) => setImmediate(resolve));
|
||||
controller.abort(new Error("cancel now"));
|
||||
await assert.rejects(staging, /cancel now/);
|
||||
assert.equal(cancelCalls, 1);
|
||||
await assert.rejects(fs.promises.stat(localPath), { code: "ENOENT" });
|
||||
});
|
||||
Reference in New Issue
Block a user