[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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,354 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const {
YMODEM,
createYmodemFileInfoPacket,
createYmodemDataPackets,
createYmodemEndSessionPacket,
receiveYmodemFiles,
sendYmodemCancel,
sendYmodemBuffer,
} = require("./ymodemTransfer.cjs");
class FakeSerialPort extends EventEmitter {
constructor() {
super();
this.writes = [];
}
write(buffer, callback) {
this.writes.push(Buffer.from(buffer));
callback?.();
return true;
}
drain(callback) {
callback?.();
}
}
test("builds a YMODEM file info packet with filename, size, and CRC", () => {
const packet = createYmodemFileInfoPacket({
filename: "/tmp/firmware.bin",
size: 1234,
mtime: 0o1234567,
});
assert.equal(packet.length, 133);
assert.equal(packet[0], YMODEM.SOH);
assert.equal(packet[1], 0);
assert.equal(packet[2], 0xff);
assert.equal(packet.subarray(3, 15).toString("ascii"), "firmware.bin");
assert.equal(packet[15], 0);
assert.match(packet.subarray(16, 32).toString("ascii"), /^1234 1234567/);
assert.notEqual(packet.readUInt16BE(packet.length - 2), 0);
});
test("uses a 1K file info packet when metadata fills a 128 byte packet", () => {
const packet = createYmodemFileInfoPacket({
filename: `${"a".repeat(117)}`,
size: 1,
mtime: 0,
});
assert.equal(packet.length, 1029);
assert.equal(packet[0], YMODEM.STX);
});
test("builds 1K data packets padded like terminal YMODEM senders", () => {
const packets = createYmodemDataPackets(Buffer.from("abc"));
assert.equal(packets.length, 1);
assert.equal(packets[0].length, 1029);
assert.equal(packets[0][0], YMODEM.STX);
assert.equal(packets[0][1], 1);
assert.equal(packets[0][2], 0xfe);
assert.equal(packets[0].subarray(3, 6).toString("ascii"), "abc");
assert.equal(packets[0][6], 0x1a);
});
test("ignores a lone cancel byte without dropping the following receiver response", async () => {
const serial = new FakeSerialPort();
const transfer = sendYmodemBuffer(serial, {
filename: "firmware.bin",
buffer: Buffer.from("abc"),
timeoutMs: 200,
});
serial.emit("data", Buffer.from([YMODEM.CRC16]));
await waitForWrites(serial, 1);
serial.emit("data", Buffer.from([YMODEM.CAN, YMODEM.ACK, YMODEM.CRC16]));
await waitForWrites(serial, 2);
assert.equal(serial.writes[1][0], YMODEM.STX);
serial.emit("data", Buffer.from([YMODEM.ACK]));
await waitForWrites(serial, 3);
serial.emit("data", Buffer.from([YMODEM.ACK, YMODEM.CRC16]));
await waitForWrites(serial, 4);
serial.emit("data", Buffer.from([YMODEM.ACK]));
await transfer;
});
test("resends the file info packet when the receiver NAKs before data", async () => {
const serial = new FakeSerialPort();
const transfer = sendYmodemBuffer(serial, {
filename: "firmware.bin",
buffer: Buffer.from("abc"),
timeoutMs: 200,
});
serial.emit("data", Buffer.from([YMODEM.CRC16]));
await waitForWrites(serial, 1);
assert.equal(serial.writes[0][0], YMODEM.SOH);
serial.emit("data", Buffer.from([YMODEM.ACK, YMODEM.NAK]));
await waitForWrites(serial, 2);
assert.equal(serial.writes[1][0], YMODEM.SOH);
serial.emit("data", Buffer.from([YMODEM.ACK, YMODEM.CRC16]));
await waitForWrites(serial, 3);
assert.equal(serial.writes[2][0], YMODEM.STX);
serial.emit("data", Buffer.from([YMODEM.ACK]));
await waitForWrites(serial, 4);
serial.emit("data", Buffer.from([YMODEM.ACK, YMODEM.CRC16]));
await waitForWrites(serial, 5);
serial.emit("data", Buffer.from([YMODEM.ACK]));
await transfer;
});
test("sends with the Tera Term compatible YMODEM handshake", async () => {
const serial = new FakeSerialPort();
const transfer = sendYmodemBuffer(serial, {
filename: "firmware.bin",
buffer: Buffer.from("abc"),
timeoutMs: 200,
});
serial.emit("data", Buffer.from([YMODEM.CRC16]));
await waitForWrites(serial, 1);
assert.equal(serial.writes[0][0], YMODEM.SOH);
serial.emit("data", Buffer.from([YMODEM.ACK, YMODEM.CRC16]));
await waitForWrites(serial, 2);
assert.equal(serial.writes[1][0], YMODEM.STX);
serial.emit("data", Buffer.from([YMODEM.ACK]));
await waitForWrites(serial, 3);
assert.deepEqual([...serial.writes[2]], [YMODEM.EOT]);
serial.emit("data", Buffer.from([YMODEM.NAK]));
await waitForWrites(serial, 4);
assert.deepEqual([...serial.writes[3]], [YMODEM.EOT]);
serial.emit("data", Buffer.from([YMODEM.ACK, YMODEM.CRC16]));
await waitForWrites(serial, 5);
assert.deepEqual(serial.writes[4], createYmodemEndSessionPacket());
serial.emit("data", Buffer.from([YMODEM.ACK]));
const result = await transfer;
assert.deepEqual(result, {
fileName: "firmware.bin",
totalBytes: 3,
writtenBytes: 3,
packetsSent: 2,
});
assert.equal(serial.listenerCount("data"), 0);
});
test("fails immediately when the serial port closes during transfer", async () => {
const serial = new FakeSerialPort();
const transfer = sendYmodemBuffer(serial, {
filename: "firmware.bin",
buffer: Buffer.from("abc"),
timeoutMs: 5_000,
});
serial.emit("data", Buffer.from([YMODEM.CRC16]));
await waitForWrites(serial, 1);
serial.emit("close");
await assert.rejects(transfer, /Serial port closed during YMODEM transfer/);
assert.equal(serial.listenerCount("data"), 0);
});
test("sends the Tera Term style cancel sequence", async () => {
const serial = new FakeSerialPort();
await sendYmodemCancel(serial);
assert.deepEqual(
[...serial.writes[0]],
[
YMODEM.CAN,
YMODEM.CAN,
YMODEM.CAN,
YMODEM.CAN,
YMODEM.CAN,
YMODEM.BACKSPACE,
YMODEM.BACKSPACE,
YMODEM.BACKSPACE,
YMODEM.BACKSPACE,
YMODEM.BACKSPACE,
],
);
});
test("receives a YMODEM file into the selected directory", async () => {
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-ymodem-receive-"));
try {
const serial = new FakeSerialPort();
const transfer = receiveYmodemFiles(serial, {
destinationDir: targetDir,
timeoutMs: 200,
});
await waitForWrites(serial, 1);
assert.deepEqual([...serial.writes[0]], [YMODEM.CRC16]);
serial.emit("data", createYmodemFileInfoPacket({
filename: "device.log",
size: 3,
mtime: 0,
}));
await waitForWrites(serial, 3);
assert.deepEqual([...serial.writes[1]], [YMODEM.ACK]);
assert.deepEqual([...serial.writes[2]], [YMODEM.CRC16]);
serial.emit("data", createYmodemDataPackets(Buffer.from("abc"))[0]);
await waitForWrites(serial, 4);
assert.deepEqual([...serial.writes[3]], [YMODEM.ACK]);
serial.emit("data", Buffer.from([YMODEM.EOT]));
await waitForWrites(serial, 5);
assert.deepEqual([...serial.writes[4]], [YMODEM.NAK]);
serial.emit("data", Buffer.from([YMODEM.EOT]));
await waitForWrites(serial, 7);
assert.deepEqual([...serial.writes[5]], [YMODEM.ACK]);
assert.deepEqual([...serial.writes[6]], [YMODEM.CRC16]);
serial.emit("data", createYmodemEndSessionPacket());
await waitForWrites(serial, 8);
assert.deepEqual([...serial.writes[7]], [YMODEM.ACK]);
const result = await transfer;
assert.deepEqual(result, {
files: [{
fileName: "device.log",
filePath: path.join(targetDir, "device.log"),
totalBytes: 3,
writtenBytes: 3,
}],
fileCount: 1,
totalBytes: 3,
writtenBytes: 3,
fileName: "device.log",
filePath: path.join(targetDir, "device.log"),
});
assert.equal(fs.readFileSync(path.join(targetDir, "device.log"), "utf8"), "abc");
assert.equal(serial.listenerCount("data"), 0);
} finally {
fs.rmSync(targetDir, { recursive: true, force: true });
}
});
test("rejects an incomplete received file and removes the partial file", async () => {
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-ymodem-short-"));
try {
const serial = new FakeSerialPort();
const transfer = receiveYmodemFiles(serial, {
destinationDir: targetDir,
timeoutMs: 200,
});
const rejectedTransfer = assert.rejects(transfer, /incomplete/i);
await waitForWrites(serial, 1);
serial.emit("data", createYmodemFileInfoPacket({
filename: "short.log",
size: 1500,
mtime: 0,
}));
await waitForWrites(serial, 3);
serial.emit("data", createYmodemDataPackets(Buffer.alloc(1024, 0x61))[0]);
await waitForWrites(serial, 4);
serial.emit("data", Buffer.from([YMODEM.EOT]));
await waitForWrites(serial, 5);
serial.emit("data", Buffer.from([YMODEM.EOT]));
await waitForWrites(serial, 6);
await rejectedTransfer;
assert.equal(fs.existsSync(path.join(targetDir, "short.log")), false);
assert.equal(serial.listenerCount("data"), 0);
} finally {
fs.rmSync(targetDir, { recursive: true, force: true });
}
});
test("does not delete an existing file if creating the receive target fails", async () => {
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-ymodem-race-"));
const targetPath = path.join(targetDir, "race.log");
const originalOpen = fs.promises.open;
try {
fs.promises.open = async (filePath, flags, ...args) => {
if (filePath === targetPath && flags === "wx") {
fs.writeFileSync(targetPath, "existing");
const error = new Error("file exists");
error.code = "EEXIST";
throw error;
}
return originalOpen.call(fs.promises, filePath, flags, ...args);
};
const serial = new FakeSerialPort();
const transfer = receiveYmodemFiles(serial, {
destinationDir: targetDir,
timeoutMs: 200,
});
const rejectedTransfer = assert.rejects(transfer, /file exists/i);
await waitForWrites(serial, 1);
serial.emit("data", createYmodemFileInfoPacket({
filename: "race.log",
size: 3,
mtime: 0,
}));
await waitForWrites(serial, 3);
await rejectedTransfer;
assert.equal(fs.readFileSync(targetPath, "utf8"), "existing");
} finally {
fs.promises.open = originalOpen;
fs.rmSync(targetDir, { recursive: true, force: true });
}
});
function waitForWrites(serial, count) {
return new Promise((resolve, reject) => {
const startedAt = Date.now();
const tick = () => {
if (serial.writes.length >= count) {
resolve();
return;
}
if (Date.now() - startedAt > 500) {
reject(new Error(`Timed out waiting for ${count} serial writes`));
return;
}
setTimeout(tick, 5);
};
tick();
});
}