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

370 lines
10 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter, getEventListeners } = require("node:events");
const {
isSshChannelOpenRateLimitedError,
openBoundedForwardIn,
openBoundedForwardOut,
openBoundedSshShell,
} = require("./boundedSshChannelOpen.cjs");
function trackedTimerApi() {
const active = new Set();
const delays = [];
return {
active,
delays,
setTimeoutFn(callback, delay) {
delays.push(delay);
let timer;
timer = setTimeout(() => {
active.delete(timer);
callback();
}, delay);
active.add(timer);
return timer;
},
clearTimeoutFn(timer) {
clearTimeout(timer);
active.delete(timer);
},
};
}
function pendingClient(method) {
const client = new EventEmitter();
client.pending = [];
client.invalidations = 0;
client[method] = (...args) => client.pending.push(args.at(-1));
client.end = () => {};
client.destroy = () => {
client.invalidations += 1;
client.pending.length = 0;
};
return client;
}
test("unresponsive shell and forward opens invalidate transport and release pending callbacks", async () => {
for (const [method, open] of [
["shell", (client) => openBoundedSshShell(client, {}, {}, { timeoutMs: 2 })],
["forwardOut", (client) => openBoundedForwardOut(client, "127.0.0.1", 0, "host", 22, { timeoutMs: 2 })],
["forwardIn", (client) => openBoundedForwardIn(client, "127.0.0.1", 2222, { timeoutMs: 2 })],
]) {
const client = pendingClient(method);
await assert.rejects(open(client), /timed out/);
assert.equal(client.invalidations, 1, method);
assert.equal(client.pending.length, 0, method);
}
});
test("cancelled channel open invalidates transport and a late stream is closed", async () => {
let callback;
let invalidations = 0;
const timers = trackedTimerApi();
const client = {
shell(_window, _options, next) { callback = next; },
destroy() { invalidations += 1; },
};
const controller = new AbortController();
const pending = openBoundedSshShell(client, {}, {}, {
signal: controller.signal,
...timers,
});
assert.equal(timers.active.size, 1);
assert.equal([...timers.active][0].hasRef(), true);
controller.abort(new Error("cancelled"));
await assert.rejects(pending, /cancelled/);
assert.equal(invalidations, 1);
assert.equal(timers.active.size, 0);
assert.equal(getEventListeners(controller.signal, "abort").length, 0);
const stream = new EventEmitter();
stream.closed = 0;
stream.close = () => { stream.closed += 1; };
callback(new Error("late open failure"), stream);
assert.equal(stream.closed, 1);
assert.equal(timers.active.size, 0);
});
test("non-invalidating cancellation reports an abandoned open until its callback settles", async () => {
let callback;
let abandoned = 0;
let abandonedSettled = 0;
const controller = new AbortController();
const client = {
shell(_window, _options, next) { callback = next; },
};
const pending = openBoundedSshShell(client, {}, {}, {
signal: controller.signal,
invalidateOnAbort: false,
onAbandonedOpen: () => { abandoned += 1; },
onAbandonedOpenSettled: () => { abandonedSettled += 1; },
});
controller.abort(new Error("cancelled"));
await assert.rejects(pending, /cancelled/);
assert.equal(abandoned, 1);
assert.equal(abandonedSettled, 0);
const lateStream = new EventEmitter();
lateStream.closed = 0;
lateStream.close = () => { lateStream.closed += 1; };
callback(null, lateStream);
assert.equal(lateStream.closed, 1);
assert.equal(abandonedSettled, 1);
});
test("channel open keeps its deadline referenced and clears it after success", async () => {
let callback;
const timers = trackedTimerApi();
const controller = new AbortController();
const client = {
shell(_window, _options, next) { callback = next; },
};
const pending = openBoundedSshShell(client, {}, {}, {
signal: controller.signal,
timeoutMs: 1_000,
...timers,
});
assert.equal(timers.active.size, 1);
assert.equal([...timers.active][0].hasRef(), true);
const stream = new EventEmitter();
callback(null, stream);
assert.equal(await pending, stream);
assert.equal(timers.active.size, 0);
assert.equal(getEventListeners(controller.signal, "abort").length, 0);
});
test("shell open caps each attempt to the remaining rate-limit retry window", async () => {
const timers = trackedTimerApi();
const controller = new AbortController();
let elapsedMs = 0;
let attempts = 0;
const client = {
shell(_window, _options, next) {
attempts += 1;
if (attempts === 1) {
next(new Error("(SSH) Channel open failure: channelOpen too offen type=session"));
}
},
};
const pending = openBoundedSshShell(client, {}, {}, {
signal: controller.signal,
timeoutMs: 1_000,
rateLimitRetryTimeoutMs: 5,
rateLimitBackoffMs: 1,
nowFn: () => elapsedMs,
sleepFn: async (ms) => { elapsedMs += ms; },
...timers,
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(attempts, 2);
assert.deepEqual(timers.delays, [1_000, 4]);
controller.abort(new Error("cancelled"));
await assert.rejects(pending, /cancelled/);
});
test("a retry-budget timeout preserves the transport and returns the rate-limit error", async () => {
let attempts = 0;
let invalidations = 0;
let elapsedMs = 0;
let lateCallback = null;
let abandoned = 0;
let abandonedSettled = 0;
const client = {
shell(_window, _options, next) {
attempts += 1;
if (attempts === 1) {
next(new Error("(SSH) Channel open failure: channelOpen too offen type=session"));
} else {
lateCallback = next;
}
},
destroy() { invalidations += 1; },
};
await assert.rejects(
openBoundedSshShell(client, {}, {}, {
timeoutMs: 1_000,
rateLimitRetryTimeoutMs: 5,
rateLimitBackoffMs: 1,
nowFn: () => elapsedMs,
sleepFn: async (ms) => { elapsedMs += ms; },
setTimeoutFn(callback, ms) {
if (ms < 1_000) {
elapsedMs += ms;
setImmediate(callback);
}
return { unref() {} };
},
clearTimeoutFn() {},
onAbandonedOpen: () => { abandoned += 1; },
onAbandonedOpenSettled: () => { abandonedSettled += 1; },
}),
/channelOpen too offen/,
);
assert.equal(attempts, 2);
assert.equal(invalidations, 0);
assert.equal(abandoned, 1);
assert.equal(abandonedSettled, 0);
const lateStream = new EventEmitter();
lateStream.closed = 0;
lateStream.close = () => { lateStream.closed += 1; };
lateCallback(null, lateStream);
assert.equal(lateStream.closed, 1);
assert.equal(abandonedSettled, 1);
});
test("detects bastion channelOpen rate-limit errors including the offen typo", () => {
assert.equal(
isSshChannelOpenRateLimitedError(
new Error("(SSH) Channel open failure: channelOpen too offen type=session"),
),
true,
);
assert.equal(
isSshChannelOpenRateLimitedError(
new Error("channel open failure: channelOpen too often type=session"),
),
true,
);
assert.equal(
isSshChannelOpenRateLimitedError(new Error("Permission denied")),
false,
);
});
test("shell open retries bastion rate-limit failures with short backoff", async () => {
const delays = [];
let attempts = 0;
const client = {
shell(_window, _options, next) {
attempts += 1;
if (attempts < 3) {
next(new Error("(SSH) Channel open failure: channelOpen too offen type=session"));
return;
}
next(null, new EventEmitter());
},
};
const stream = await openBoundedSshShell(client, {}, {}, {
rateLimitRetries: 3,
rateLimitBackoffMs: 5,
sleepFn: async (ms) => { delays.push(ms); },
});
assert.ok(stream);
assert.equal(attempts, 3);
assert.deepEqual(delays, [5, 10]);
});
test("shell open can use a bounded retry window for variable bastion cooldowns", async () => {
const delays = [];
let elapsedMs = 0;
let attempts = 0;
const client = {
shell(_window, _options, next) {
attempts += 1;
if (attempts <= 4) {
next(new Error("(SSH) Channel open failure: channelOpen too offen type=session"));
return;
}
next(null, new EventEmitter());
},
};
const stream = await openBoundedSshShell(client, {}, {}, {
rateLimitRetryTimeoutMs: 11,
rateLimitBackoffMs: 1,
nowFn: () => elapsedMs,
sleepFn: async (ms) => {
delays.push(ms);
elapsedMs += ms;
},
});
assert.ok(stream);
assert.equal(attempts, 5);
assert.deepEqual(delays, [1, 2, 3, 4]);
});
test("shell open stops retrying when the bastion retry window is exhausted", async () => {
const delays = [];
let elapsedMs = 0;
let attempts = 0;
const client = {
shell(_window, _options, next) {
attempts += 1;
next(new Error("(SSH) Channel open failure: channelOpen too offen type=session"));
},
};
await assert.rejects(
openBoundedSshShell(client, {}, {}, {
rateLimitRetryTimeoutMs: 5,
rateLimitBackoffMs: 2,
nowFn: () => elapsedMs,
sleepFn: async (ms) => {
delays.push(ms);
elapsedMs += ms;
},
}),
/channelOpen too offen/,
);
assert.equal(attempts, 2);
assert.deepEqual(delays, [2]);
});
test("shell open does not start another attempt after a delayed retry wakes past the deadline", async () => {
let elapsedMs = 0;
let attempts = 0;
const client = {
shell(_window, _options, next) {
attempts += 1;
next(new Error("(SSH) Channel open failure: channelOpen too offen type=session"));
},
};
await assert.rejects(
openBoundedSshShell(client, {}, {}, {
rateLimitRetryTimeoutMs: 5,
rateLimitBackoffMs: 1,
nowFn: () => elapsedMs,
sleepFn: async () => { elapsedMs = 10; },
}),
/channelOpen too offen/,
);
assert.equal(attempts, 1);
});
test("shell open does not retry unrelated channel open failures", async () => {
const delays = [];
let attempts = 0;
const client = {
shell(_window, _options, next) {
attempts += 1;
next(new Error("Channel open failure: administratively prohibited"));
},
};
await assert.rejects(
openBoundedSshShell(client, {}, {}, {
rateLimitRetryTimeoutMs: 10,
rateLimitBackoffMs: 5,
sleepFn: async (ms) => { delays.push(ms); },
}),
/administratively prohibited/,
);
assert.equal(attempts, 1);
assert.deepEqual(delays, []);
});