[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:
363
electron/bridges/terminalBridge.outputFlood.test.cjs
Normal file
363
electron/bridges/terminalBridge.outputFlood.test.cjs
Normal file
@@ -0,0 +1,363 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const Module = require("node:module");
|
||||
const { FLOW_HIGH_WATER_MARK } = require("./terminalFlowAck.cjs");
|
||||
|
||||
class FakePty {
|
||||
constructor() {
|
||||
this.pid = 4242;
|
||||
this.dataHandlers = [];
|
||||
this.exitHandlers = [];
|
||||
this.paused = false;
|
||||
}
|
||||
|
||||
onData(handler) {
|
||||
this.dataHandlers.push(handler);
|
||||
}
|
||||
|
||||
onExit(handler) {
|
||||
this.exitHandlers.push(handler);
|
||||
}
|
||||
|
||||
write() {}
|
||||
|
||||
resize() {}
|
||||
|
||||
kill() {}
|
||||
|
||||
pause() {
|
||||
this.paused = true;
|
||||
}
|
||||
|
||||
resume() {
|
||||
this.paused = false;
|
||||
}
|
||||
|
||||
emitData(data) {
|
||||
for (const handler of this.dataHandlers) handler(data);
|
||||
}
|
||||
|
||||
emitExit(evt = { exitCode: 0, signal: 0 }) {
|
||||
for (const handler of this.exitHandlers) handler(evt);
|
||||
}
|
||||
}
|
||||
|
||||
function loadBridgeWithFakes(spawns, sentries) {
|
||||
const bridgePath = require.resolve("./terminalBridge.cjs");
|
||||
delete require.cache[bridgePath];
|
||||
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function patchedLoad(request, parent, isMain) {
|
||||
if (request === "node-pty") {
|
||||
return {
|
||||
spawn(...spawnArgs) {
|
||||
const pty = new FakePty();
|
||||
pty.spawnArgs = spawnArgs;
|
||||
spawns.push(pty);
|
||||
return pty;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (request === "serialport") {
|
||||
return { SerialPort: class { static async list() { return []; } } };
|
||||
}
|
||||
|
||||
if (request === "./nodePtySpawnHelperPermissions.cjs") {
|
||||
return { ensureNodePtySpawnHelperExecutable() {} };
|
||||
}
|
||||
|
||||
if (request === "./zmodemHelper.cjs") {
|
||||
return {
|
||||
createZmodemSentry(options) {
|
||||
const sentry = {
|
||||
active: false,
|
||||
consumeCalls: [],
|
||||
consume(data) {
|
||||
this.consumeCalls.push(data);
|
||||
const raw = Buffer.isBuffer(data) ? data : Buffer.from(String(data));
|
||||
options.onData(raw);
|
||||
},
|
||||
isActive() {
|
||||
return this.active;
|
||||
},
|
||||
cancel() {},
|
||||
};
|
||||
sentries.push(sentry);
|
||||
return sentry;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
|
||||
try {
|
||||
return require("./terminalBridge.cjs");
|
||||
} finally {
|
||||
Module._load = originalLoad;
|
||||
}
|
||||
}
|
||||
|
||||
test("Windows local terminals enable the bundled ConPTY implementation required for clear", () => {
|
||||
const spawns = [];
|
||||
const sentries = [];
|
||||
const sessions = new Map();
|
||||
const bridge = loadBridgeWithFakes(spawns, sentries);
|
||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
|
||||
try {
|
||||
Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" });
|
||||
bridge.init({
|
||||
sessions,
|
||||
electronModule: {
|
||||
webContents: {
|
||||
fromId: () => ({ send() {} }),
|
||||
},
|
||||
},
|
||||
});
|
||||
bridge.startLocalSession(
|
||||
{ sender: { id: 7 } },
|
||||
{ sessionId: "windows-clear", shell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" },
|
||||
);
|
||||
|
||||
assert.equal(spawns.length, 1);
|
||||
assert.equal(spawns[0].spawnArgs[2].useConptyDll, true);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", platformDescriptor);
|
||||
}
|
||||
});
|
||||
|
||||
test("local terminal buffers incoming flood while renderer flow is paused", () => {
|
||||
const spawns = [];
|
||||
const sentries = [];
|
||||
const sent = [];
|
||||
const sessions = new Map();
|
||||
const bridge = loadBridgeWithFakes(spawns, sentries);
|
||||
|
||||
bridge.init({
|
||||
sessions,
|
||||
electronModule: {
|
||||
webContents: {
|
||||
fromId() {
|
||||
return { send: (channel, payload) => sent.push({ channel, payload }) };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridge.startLocalSession(
|
||||
{ sender: { id: 7 } },
|
||||
{ sessionId: "local-flood", shell: "/bin/sh", cols: 80, rows: 24 },
|
||||
);
|
||||
|
||||
bridge.setSessionFlowPaused(
|
||||
{ sender: {} },
|
||||
{ sessionId: "local-flood", paused: true },
|
||||
);
|
||||
spawns[0].emitData(Buffer.from("ordinary flood"));
|
||||
|
||||
assert.equal(sentries[0].consumeCalls.length, 1);
|
||||
assert.deepEqual(sent, []);
|
||||
bridge.setSessionFlowPaused(
|
||||
{ sender: {} },
|
||||
{ sessionId: "local-flood", paused: false },
|
||||
);
|
||||
assert.deepEqual(sent.map((item) => item.payload.data), ["ordinary flood"]);
|
||||
|
||||
sentries[0].active = true;
|
||||
spawns[0].emitData(Buffer.from("transfer bytes"));
|
||||
|
||||
assert.equal(sentries[0].consumeCalls.length, 2);
|
||||
});
|
||||
|
||||
test("local terminal keeps source paused while paced backlog absorbs fresh flood", () => {
|
||||
const spawns = [];
|
||||
const sentries = [];
|
||||
const sent = [];
|
||||
const sessions = new Map();
|
||||
const bridge = loadBridgeWithFakes(spawns, sentries);
|
||||
|
||||
bridge.init({
|
||||
sessions,
|
||||
electronModule: {
|
||||
webContents: {
|
||||
fromId() {
|
||||
return { send: (channel, payload) => sent.push({ channel, payload }) };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridge.startLocalSession(
|
||||
{ sender: { id: 7 } },
|
||||
{ sessionId: "local-flood-paced-fresh", shell: "/bin/sh", cols: 80, rows: 24 },
|
||||
);
|
||||
|
||||
bridge.setSessionFlowPaused(
|
||||
{ sender: {} },
|
||||
{ sessionId: "local-flood-paced-fresh", paused: true },
|
||||
);
|
||||
spawns[0].emitData(Buffer.from("a".repeat(FLOW_HIGH_WATER_MARK + 10)));
|
||||
|
||||
const session = sessions.get("local-flood-paced-fresh");
|
||||
assert.equal(spawns[0].paused, true);
|
||||
assert.equal(session.flowState.bufferedBytes, FLOW_HIGH_WATER_MARK + 10);
|
||||
assert.deepEqual(sent, []);
|
||||
|
||||
bridge.setSessionFlowPaused(
|
||||
{ sender: {} },
|
||||
{ sessionId: "local-flood-paced-fresh", paused: false },
|
||||
);
|
||||
|
||||
assert.equal(sent.length, 1);
|
||||
assert.equal(spawns[0].paused, true);
|
||||
|
||||
spawns[0].emitData(Buffer.from("b".repeat(FLOW_HIGH_WATER_MARK)));
|
||||
|
||||
assert.equal(sent.length, 1);
|
||||
assert.equal(spawns[0].paused, true);
|
||||
assert.ok(session.flowState.bufferedBytes >= FLOW_HIGH_WATER_MARK);
|
||||
});
|
||||
|
||||
test("closing a local terminal discards buffered output instead of flushing it", () => {
|
||||
const spawns = [];
|
||||
const sentries = [];
|
||||
const sent = [];
|
||||
const sessions = new Map();
|
||||
const bridge = loadBridgeWithFakes(spawns, sentries);
|
||||
|
||||
bridge.init({
|
||||
sessions,
|
||||
electronModule: {
|
||||
webContents: {
|
||||
fromId() {
|
||||
return { send: (channel, payload) => sent.push({ channel, payload }) };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridge.startLocalSession(
|
||||
{ sender: { id: 7 } },
|
||||
{ sessionId: "local-flood-close", shell: "/bin/sh", cols: 80, rows: 24 },
|
||||
);
|
||||
|
||||
spawns[0].emitData(Buffer.from("pending tail"));
|
||||
bridge.closeSession({ sender: {} }, { sessionId: "local-flood-close" });
|
||||
|
||||
assert.deepEqual(sent, [{
|
||||
channel: "netcatty:exit",
|
||||
payload: { sessionId: "local-flood-close", exitCode: 0, reason: "closed" },
|
||||
}]);
|
||||
});
|
||||
|
||||
test("app cleanup discards buffered output instead of flushing it", () => {
|
||||
const spawns = [];
|
||||
const sentries = [];
|
||||
const sent = [];
|
||||
const sessions = new Map();
|
||||
const bridge = loadBridgeWithFakes(spawns, sentries);
|
||||
|
||||
bridge.init({
|
||||
sessions,
|
||||
electronModule: {
|
||||
webContents: {
|
||||
fromId() {
|
||||
return { send: (channel, payload) => sent.push({ channel, payload }) };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridge.startLocalSession(
|
||||
{ sender: { id: 7 } },
|
||||
{ sessionId: "local-flood-cleanup", shell: "/bin/sh", cols: 80, rows: 24 },
|
||||
);
|
||||
|
||||
spawns[0].emitData(Buffer.from("pending tail"));
|
||||
bridge.cleanupAllSessions();
|
||||
|
||||
assert.deepEqual(sent, []);
|
||||
assert.equal(sessions.size, 0);
|
||||
});
|
||||
|
||||
test("local terminal exit waits for paced buffered output drain", async () => {
|
||||
const spawns = [];
|
||||
const sentries = [];
|
||||
const sent = [];
|
||||
const sessions = new Map();
|
||||
const bridge = loadBridgeWithFakes(spawns, sentries);
|
||||
|
||||
bridge.init({
|
||||
sessions,
|
||||
electronModule: {
|
||||
webContents: {
|
||||
fromId() {
|
||||
return { send: (channel, payload) => sent.push({ channel, payload }) };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridge.startLocalSession(
|
||||
{ sender: { id: 7 } },
|
||||
{ sessionId: "local-flood-exit", shell: "/bin/sh", cols: 80, rows: 24 },
|
||||
);
|
||||
|
||||
const output = "x".repeat(2_400_000);
|
||||
spawns[0].emitData(Buffer.from(output));
|
||||
spawns[0].emitExit({ exitCode: 0, signal: 0 });
|
||||
|
||||
assert.equal(sent.some((item) => item.channel === "netcatty:exit"), false);
|
||||
|
||||
bridge.ackSessionFlow(
|
||||
{ sender: {} },
|
||||
{ sessionId: "local-flood-exit", bytes: output.length },
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
const data = sent
|
||||
.filter((item) => item.channel === "netcatty:data")
|
||||
.map((item) => item.payload.data)
|
||||
.join("");
|
||||
assert.equal(data, output);
|
||||
assert.equal(sent.some((item) => item.channel === "netcatty:exit"), true);
|
||||
assert.equal(sessions.has("local-flood-exit"), false);
|
||||
});
|
||||
|
||||
test("local terminal exit completes while renderer flow is paused", () => {
|
||||
const spawns = [];
|
||||
const sentries = [];
|
||||
const sent = [];
|
||||
const sessions = new Map();
|
||||
const bridge = loadBridgeWithFakes(spawns, sentries);
|
||||
|
||||
bridge.init({
|
||||
sessions,
|
||||
electronModule: {
|
||||
webContents: {
|
||||
fromId() {
|
||||
return { send: (channel, payload) => sent.push({ channel, payload }) };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridge.startLocalSession(
|
||||
{ sender: { id: 7 } },
|
||||
{ sessionId: "local-paused-exit", shell: "/bin/sh", cols: 80, rows: 24 },
|
||||
);
|
||||
|
||||
bridge.setSessionFlowPaused(
|
||||
{ sender: {} },
|
||||
{ sessionId: "local-paused-exit", paused: true },
|
||||
);
|
||||
spawns[0].emitData(Buffer.from("pending output"));
|
||||
spawns[0].emitExit({ exitCode: 0, signal: 0 });
|
||||
|
||||
assert.equal(sent.some((item) => item.channel === "netcatty:data"), false);
|
||||
assert.equal(sent.some((item) => item.channel === "netcatty:exit"), true);
|
||||
assert.equal(sessions.has("local-paused-exit"), false);
|
||||
});
|
||||
Reference in New Issue
Block a user