Files
NetMesh/electron/bridges/transferBridge.globalFanout.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

384 lines
14 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const test = require("node:test");
const Module = require("node:module");
const path = require("node:path");
/**
* broadcastGlobalTransferEvent only loads electron when process.versions.electron
* is set (avoids install.js downloads in bare Node unit tests).
*/
function withElectronVersionStub() {
const previous = process.versions.electron;
Object.defineProperty(process.versions, "electron", {
configurable: true,
enumerable: true,
value: previous || "test",
});
return () => {
if (previous === undefined) {
delete process.versions.electron;
} else {
Object.defineProperty(process.versions, "electron", {
configurable: true,
enumerable: true,
value: previous,
});
}
};
}
/**
* Drive the shipped broadcastGlobalTransferEvent entry point with a stubbed
* electron BrowserWindow — proves fan-out does not require a panel sender.
*/
test("broadcastGlobalTransferEvent fans progress to all live BrowserWindows", () => {
const sent = [];
const restoreElectronVersion = withElectronVersionStub();
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "electron") {
return {
BrowserWindow: {
getAllWindows() {
return [
{
isDestroyed: () => false,
webContents: {
isDestroyed: () => false,
send(channel, payload) {
sent.push({ channel, payload });
},
},
},
{
isDestroyed: () => true,
webContents: { isDestroyed: () => false, send() { throw new Error("dead"); } },
},
];
},
},
};
}
return originalLoad(request, parent, isMain);
};
try {
// Fresh load so the stub is used.
const bridgePath = path.join(__dirname, "transferBridge.cjs");
delete require.cache[require.resolve(bridgePath)];
const bridge = require(bridgePath);
assert.equal(typeof bridge.broadcastGlobalTransferEvent, "function");
bridge.broadcastGlobalTransferEvent({
type: "progress",
transferId: "t-fanout",
transferred: 50,
totalBytes: 100,
speed: 10,
});
assert.equal(sent.length, 1);
assert.equal(sent[0].channel, "netcatty:sftp:global-transfer");
assert.equal(sent[0].payload.transferId, "t-fanout");
assert.equal(sent[0].payload.type, "progress");
assert.equal(sent[0].payload.transferred, 50);
} finally {
Module._load = originalLoad;
restoreElectronVersion();
try {
delete require.cache[require.resolve(path.join(__dirname, "transferBridge.cjs"))];
} catch { /* ignore */ }
}
});
test("broadcastGlobalTransferEvent no-ops without transferId", () => {
const bridge = require("./transferBridge.cjs");
assert.doesNotThrow(() => bridge.broadcastGlobalTransferEvent({ type: "progress" }));
assert.doesNotThrow(() => bridge.broadcastGlobalTransferEvent(null));
});
test("same-host cp capability cache follows the live SFTP client, not a recycled session id", async () => {
const bridge = require("./transferBridge.cjs");
const sftpClients = new Map();
bridge.init({ sftpClients });
const makeClient = (exitCode, calls) => ({
client: {
exec(_command, callback) {
calls.count += 1;
const stream = new EventEmitter();
stream.stderr = new EventEmitter();
stream.close = () => {};
callback(null, stream);
queueMicrotask(() => stream.emit("close", exitCode));
},
},
});
const firstCalls = { count: 0 };
const secondCalls = { count: 0 };
const event = { sender: { send() {} } };
const payload = {
sftpId: "recycled-sftp-id",
sourcePath: "/source",
targetPath: "/target",
encoding: "utf-8",
};
sftpClients.set(payload.sftpId, makeClient(127, firstCalls));
assert.equal((await bridge.sameHostCopyDirectory(event, payload)).success, false);
assert.equal(firstCalls.count, 1);
sftpClients.set(payload.sftpId, makeClient(0, secondCalls));
assert.equal((await bridge.sameHostCopyDirectory(event, payload)).success, true);
assert.equal(secondCalls.count, 1, "a replacement connection must probe cp again");
});
test("worker transfer lifecycle epochs survive a reused id and clear after the latest start settles", async () => {
const bridgePath = path.join(__dirname, "transferBridge.cjs");
delete require.cache[require.resolve(bridgePath)];
const bridge = require(bridgePath);
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;
bridge.registerHandlers({
handle(channel, handler) { handlers.set(channel, handler); },
}, {
terminalWorkerManager: {
request(channel) {
if (channel === "netcatty:transfer:start") {
startCalls += 1;
return startCalls === 1 ? firstGate : secondGate;
}
return Promise.resolve({ success: true });
},
},
});
const firstStarting = handlers.get("netcatty:transfer:start")(
{ sender: { id: 1 } },
{ transferId: "worker-cache-lifetime", skipAdmission: true },
);
const secondStarting = handlers.get("netcatty:transfer:start")(
{ sender: { id: 1 } },
{ transferId: "worker-cache-lifetime", skipAdmission: true },
);
assert.equal(bridge._getWorkerTransferLifecycleEpochCountForTests(), 1);
finishFirst({ transferId: "worker-cache-lifetime" });
await firstStarting;
assert.equal(
bridge._getWorkerTransferLifecycleEpochCountForTests(),
1,
"an older completion must not clear the newer start's lifecycle state",
);
finishSecond({ transferId: "worker-cache-lifetime" });
await secondStarting;
assert.equal(bridge._getWorkerTransferLifecycleEpochCountForTests(), 0);
});
test("worker transfer lifecycle epoch clears after a synchronous start failure", () => {
const bridgePath = path.join(__dirname, "transferBridge.cjs");
delete require.cache[require.resolve(bridgePath)];
const bridge = require(bridgePath);
const handlers = new Map();
bridge.registerHandlers({
handle(channel, handler) { handlers.set(channel, handler); },
}, {
terminalWorkerManager: {
request(channel) {
if (channel === "netcatty:transfer:start") throw new Error("worker unavailable");
return Promise.resolve({ success: true });
},
},
});
assert.throws(() => handlers.get("netcatty:transfer:start")(
{ sender: { id: 1 } },
{ transferId: "worker-cache-sync-failure", skipAdmission: true },
), /worker unavailable/);
assert.equal(bridge._getWorkerTransferLifecycleEpochCountForTests(), 0);
});
test("worker-backed pause and resume fan authoritative lifecycle to every window", async () => {
const sent = [];
const restoreElectronVersion = withElectronVersionStub();
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "electron") {
return {
BrowserWindow: {
getAllWindows: () => [{
isDestroyed: () => false,
webContents: {
isDestroyed: () => false,
send(channel, payload) { sent.push({ channel, payload }); },
},
}],
},
};
}
return originalLoad(request, parent, isMain);
};
const bridgePath = path.join(__dirname, "transferBridge.cjs");
try {
delete require.cache[require.resolve(bridgePath)];
const bridge = require(bridgePath);
const handlers = new Map();
let finishStart;
const startGate = new Promise((resolve) => { finishStart = resolve; });
bridge.registerHandlers({
handle(channel, handler) { handlers.set(channel, handler); },
}, {
terminalWorkerManager: {
async request(channel) {
if (channel === "netcatty:transfer:start") return startGate;
if (channel === "netcatty:transfer:pause") {
return { success: true, checkpointBytes: 50, resumeStage: "upload" };
}
return { success: true };
},
},
});
const starting = handlers.get("netcatty:transfer:start")(
{ sender: { id: 1 } },
{ transferId: "worker-transfer", skipAdmission: true },
);
await handlers.get("netcatty:transfer:pause")(
{ sender: { id: 1 } },
{ transferId: "worker-transfer" },
);
await handlers.get("netcatty:transfer:resume")(
{ sender: { id: 1 } },
{ transferId: "worker-transfer" },
);
const lifecycle = sent
.filter((entry) => entry.channel === "netcatty:sftp:global-transfer")
.map((entry) => entry.payload);
assert.deepEqual(lifecycle, [
{
type: "pausing",
transferId: "worker-transfer",
lifecycleEpoch: 1,
lifecycleState: "pausing",
},
{
type: "paused",
transferId: "worker-transfer",
checkpointBytes: 50,
resumeStage: "upload",
downloadCheckpointBytes: undefined,
uploadCheckpointBytes: undefined,
sourceFingerprint: undefined,
lifecycleEpoch: 1,
lifecycleState: "paused",
},
{
type: "resumed",
transferId: "worker-transfer",
lifecycleEpoch: 2,
lifecycleState: "transferring",
},
]);
finishStart({ transferId: "worker-transfer" });
await starting;
assert.equal(bridge._getWorkerTransferLifecycleEpochCountForTests(), 0);
} finally {
Module._load = originalLoad;
restoreElectronVersion();
delete require.cache[require.resolve(bridgePath)];
}
});
for (const latestAction of ["pause", "resume", "cancel"]) {
test(`stale worker pause retains latest ${latestAction} after that newer request has settled`, async (t) => {
const bridgePath = require.resolve("./transferBridge.cjs");
delete require.cache[bridgePath];
t.after(() => { delete require.cache[bridgePath]; });
const bridge = require(bridgePath);
const handlers = new Map();
let finishFirst;
const firstGate = new Promise((resolve) => { finishFirst = resolve; });
let requests = 0;
bridge.registerHandlers({ handle: (channel, fn) => handlers.set(channel, fn) }, {
terminalWorkerManager: { request: async () => ++requests === 1 ? firstGate : { success: true } },
});
const payload = { transferId: `worker-last-action-${latestAction}` };
const first = handlers.get("netcatty:transfer:pause")(null, payload);
await handlers.get(`netcatty:transfer:${latestAction}`)(null, payload);
finishFirst({ success: true });
const stale = await first;
assert.equal(stale.superseded, true);
assert.equal(stale.supersededBy, latestAction, "completed newer request must retain intent for older pending replies");
});
}
for (const outcome of ["resume-success", "pause-success", "pause-failure", "pause-error", "resume-error"]) {
test(`worker ${outcome} arriving after a newer pause cannot broadcast resumed`, async (t) => {
const sent = [];
const restore = withElectronVersionStub();
const originalLoad = Module._load;
Module._load = function (request, parent, isMain) {
if (request === "electron") return { BrowserWindow: { getAllWindows: () => [{
isDestroyed: () => false,
webContents: { isDestroyed: () => false, send: (_channel, payload) => sent.push(payload) },
}] } };
return originalLoad(request, parent, isMain);
};
const bridgePath = require.resolve("./transferBridge.cjs");
delete require.cache[bridgePath];
t.after(() => { Module._load = originalLoad; restore(); delete require.cache[bridgePath]; });
const bridge = require(bridgePath);
const handlers = new Map();
let resolveFirst;
let rejectFirst;
const firstGate = new Promise((resolve, reject) => { resolveFirst = resolve; rejectFirst = reject; });
let requests = 0;
bridge.registerHandlers({ handle: (channel, fn) => handlers.set(channel, fn) }, {
terminalWorkerManager: { request: async () => ++requests === 1 ? firstGate : { success: true } },
});
const payload = { transferId: `worker-order-${outcome}` };
const channel = outcome.startsWith("resume") ? "resume" : "pause";
const first = handlers.get(`netcatty:transfer:${channel}`)(null, payload).catch(() => null);
await handlers.get("netcatty:transfer:pause")(null, payload);
const afterPause = sent.length;
if (outcome.endsWith("error")) rejectFirst(new Error("channel closed"));
else resolveFirst({ success: outcome.endsWith("success"), reason: "pause unavailable" });
assert.deepEqual(await first, { success: false, superseded: true, supersededBy: "pause" });
assert.equal(sent.slice(afterPause).some((event) => event.type === "resumed"), false);
assert.equal(sent.at(-1).type, "paused");
});
}
for (const action of ["pause", "resume"]) {
test(`superseded result follows rollback after latest ${action} fails`, async (t) => {
const bridgePath = require.resolve("./transferBridge.cjs");
delete require.cache[bridgePath];
t.after(() => { delete require.cache[bridgePath]; });
const bridge = require(bridgePath);
const handlers = new Map();
let finish;
let calls = 0;
bridge.registerHandlers({ handle: (channel, fn) => handlers.set(channel, fn) }, {
terminalWorkerManager: { request: () => ++calls === 1
? new Promise(resolve => { finish = resolve; })
: Promise.resolve({ success: false, reason: "unavailable" }) },
});
const payload = { transferId: `rollback-${action}` };
const older = handlers.get("netcatty:transfer:pause")(null, payload);
await handlers.get(`netcatty:transfer:${action}`)(null, payload);
finish({ success: true });
assert.equal((await older).supersededBy, action === "pause" ? "resume" : "pause");
});
}