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

577 lines
13 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const terminalBridge = require("./terminalBridge.cjs");
const {
FLOW_HIGH_WATER_MARK,
} = require("../../infrastructure/config/terminalFlowConstants.cjs");
function initBridge(sessions, sent = []) {
terminalBridge.init({
sessions,
electronModule: {
webContents: {
fromId: () => ({
send(channel, payload) {
sent.push([channel, payload]);
},
}),
},
},
});
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
test("SSH Ctrl+C writes the original byte without sending a channel signal", () => {
const calls = [];
const sessions = new Map();
sessions.set("ssh-1", {
stream: {
signal(signalName) {
calls.push(["signal", signalName]);
},
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.writeToSession({ sender: {} }, { sessionId: "ssh-1", data: "\x03" });
assert.deepEqual(calls, [
["write", "\x03"],
]);
});
test("interruptSession sends SSH Ctrl+C without discarding pending output", () => {
const calls = [];
const sessions = new Map();
sessions.set("ssh-1", {
discardPendingData() {
calls.push(["discard"]);
},
stream: {
signal(signalName) {
calls.push(["signal", signalName]);
},
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "ssh-1" });
assert.deepEqual(calls, [
["write", "\x03"],
]);
});
test("interruptSession sends SSH Ctrl+C before resuming a paused output flood", async () => {
const calls = [];
const sessions = new Map();
sessions.set("ssh-1", {
discardPendingData() {
calls.push(["discard"]);
},
stream: {
pause() {
calls.push(["pause"]);
},
resume() {
calls.push(["resume"]);
},
signal(signalName) {
calls.push(["signal", signalName]);
},
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.setSessionFlowPaused({ sender: {} }, { sessionId: "ssh-1", paused: true });
terminalBridge.interruptSession({ sender: {} }, { sessionId: "ssh-1" });
assert.deepEqual(calls, [
["pause"],
["write", "\x03"],
]);
await delay(0);
assert.deepEqual(calls, [
["pause"],
["write", "\x03"],
["resume"],
]);
});
test("interruptSession sends SSH Ctrl+C without fastpath under low output pressure", async () => {
const calls = [];
const sessions = new Map();
sessions.set("ssh-1", {
takePendingData() {
calls.push(["take-pending"]);
return "Type :qa and press <Enter> to exit Vim";
},
discardPendingData() {
calls.push(["discard"]);
},
flowState: {
rendererPaused: false,
unackedBytes: 119,
appliedPause: false,
},
stream: {
pause() {
calls.push(["pause"]);
},
resume() {
calls.push(["resume"]);
},
signal(signalName) {
calls.push(["signal", signalName]);
},
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "ssh-1" });
assert.deepEqual(calls, [
["write", "\x03"],
]);
await delay(0);
assert.deepEqual(calls, [
["write", "\x03"],
]);
});
test("interruptSession filters pending SSH output under high output pressure", () => {
const calls = [];
const sent = [];
const sessions = new Map();
sessions.set("ssh-1", {
cols: 80,
rows: 24,
webContentsId: 1,
takePendingData() {
calls.push(["take-pending"]);
return {
data: "stale frame\x1b[?1049l",
meta: { droppedOutputMayAffectTerminalState: true },
};
},
discardPendingData() {
calls.push(["discard"]);
},
flowState: {
rendererPaused: false,
unackedBytes: FLOW_HIGH_WATER_MARK + 1300,
appliedPause: false,
},
stream: {
pause() {
calls.push(["pause"]);
},
resume() {},
signal(signalName) {
calls.push(["signal", signalName]);
},
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions, sent);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "ssh-1" });
assert.deepEqual(calls, [
["pause"],
["take-pending"],
["pause"],
["write", "\x03"],
]);
assert.deepEqual(sent, [
["netcatty:data", {
sessionId: "ssh-1",
data: "\x1b[?1049l",
meta: { droppedOutputMayAffectTerminalState: true },
}],
]);
});
test("interruptSession preserves pending metadata when drained output is fully filtered", () => {
const calls = [];
const sent = [];
const sessions = new Map();
const session = {
cols: 80,
rows: 24,
webContentsId: 1,
takePendingData() {
calls.push(["take-pending"]);
return {
data: "stale frame without prompt",
meta: { droppedOutputMayAffectTerminalState: true },
};
},
flowState: {
rendererPaused: false,
unackedBytes: FLOW_HIGH_WATER_MARK + 1300,
appliedPause: false,
},
stream: {
pause() {
calls.push(["pause"]);
},
resume() {},
write(data) {
calls.push(["write", data]);
},
},
};
sessions.set("ssh-1", session);
initBridge(sessions, sent);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "ssh-1" });
assert.deepEqual(sent, []);
assert.deepEqual(session._pendingInterruptOutputMeta, {
droppedOutputMayAffectTerminalState: true,
});
terminalBridge.writeToSession({ sender: {} }, { sessionId: "ssh-1", data: "x" });
assert.deepEqual(session._pendingInterruptOutputMeta, {
droppedOutputMayAffectTerminalState: true,
});
});
test("interruptSession does not arm SSH output drain for tiny in-flight echo", () => {
const sessions = new Map();
const session = {
discardPendingData() {},
flowState: {
rendererPaused: false,
unackedBytes: 119,
appliedPause: false,
},
stream: {
pause() {},
resume() {},
signal() {},
write() {},
},
};
sessions.set("ssh-1", session);
initBridge(sessions);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "ssh-1" });
assert.notEqual(session._interruptOutputGate?.active, true);
});
test("interruptSession arms SSH output drain when interrupting a paused output flood", () => {
const sessions = new Map();
const session = {
discardPendingData() {},
flowState: {
rendererPaused: false,
unackedBytes: 34068,
appliedPause: true,
},
stream: {
resume() {},
signal() {},
write() {},
},
};
sessions.set("ssh-1", session);
initBridge(sessions);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "ssh-1" });
assert.equal(session._interruptOutputGate?.active, true);
});
test("ordinary SSH input still disarms a pending interrupt output drain", () => {
const sessions = new Map();
const session = {
discardPendingData() {},
flowState: {
rendererPaused: false,
unackedBytes: 119,
appliedPause: false,
},
stream: {
resume() {},
signal() {},
write() {},
},
};
sessions.set("ssh-1", session);
initBridge(sessions);
session._interruptOutputGate = { active: true };
assert.equal(session._interruptOutputGate?.active, true);
terminalBridge.writeToSession({ sender: {} }, { sessionId: "ssh-1", data: "l" });
assert.equal(session._interruptOutputGate?.active, false);
});
test("SSH Ctrl+C does not use channel signals even when the stream exposes them", () => {
const calls = [];
const sessions = new Map();
sessions.set("ssh-1", {
stream: {
signal(signalName) {
calls.push(["signal", signalName]);
throw new Error("signals unsupported");
},
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.writeToSession({ sender: {} }, { sessionId: "ssh-1", data: "\x03" });
assert.deepEqual(calls, [
["write", "\x03"],
]);
});
test("SSH ordinary input is written without sending INT", () => {
const calls = [];
const sessions = new Map();
sessions.set("ssh-1", {
stream: {
signal(signalName) {
calls.push(["signal", signalName]);
},
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.writeToSession({ sender: {} }, { sessionId: "ssh-1", data: "cat\r" });
assert.deepEqual(calls, [["write", "cat\r"]]);
});
test("local Ctrl+C behavior is unchanged", () => {
const calls = [];
const sessions = new Map();
sessions.set("local-1", {
type: "local",
proc: {
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.writeToSession({ sender: {} }, { sessionId: "local-1", data: "\x03" });
assert.deepEqual(calls, [["write", "\x03"]]);
});
test("local interruptSession writes Ctrl+C without fastpath", () => {
const calls = [];
const sessions = new Map();
sessions.set("local-1", {
type: "local",
takePendingData() {
calls.push(["take-pending"]);
return "pending";
},
proc: {
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "local-1" });
assert.deepEqual(calls, [["write", "\x03"]]);
});
test("telnet Ctrl+C behavior is unchanged", () => {
const calls = [];
const sessions = new Map();
sessions.set("telnet-1", {
type: "telnet-native",
socket: {
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.writeToSession({ sender: {} }, { sessionId: "telnet-1", data: "\x03" });
assert.deepEqual(calls, [["write", "\x03"]]);
});
test("telnet interruptSession writes Ctrl+C without fastpath", () => {
const calls = [];
const sessions = new Map();
sessions.set("telnet-1", {
type: "telnet-native",
takePendingData() {
calls.push(["take-pending"]);
return "pending";
},
socket: {
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "telnet-1" });
assert.deepEqual(calls, [["write", "\x03"]]);
});
test("serial Ctrl+C behavior is unchanged", () => {
const calls = [];
const sessions = new Map();
sessions.set("serial-1", {
type: "serial",
serialPort: {
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.writeToSession({ sender: {} }, { sessionId: "serial-1", data: "\x03" });
assert.deepEqual(calls, [["write", "\x03"]]);
});
test("serial interruptSession writes Ctrl+C without fastpath", () => {
const calls = [];
const sessions = new Map();
sessions.set("serial-1", {
type: "serial",
takePendingData() {
calls.push(["take-pending"]);
return "pending";
},
serialPort: {
write(data) {
calls.push(["write", data]);
},
},
});
initBridge(sessions);
terminalBridge.interruptSession({ sender: {} }, { sessionId: "serial-1" });
assert.deepEqual(calls, [["write", "\x03"]]);
});
test("automated multi-line input is written one line at a time", async () => {
const calls = [];
const sessions = new Map();
sessions.set("telnet-1", {
type: "telnet-native",
socket: {
write(data) {
calls.push(data);
},
},
});
initBridge(sessions);
terminalBridge.writeToSession(
{ sender: {} },
{
sessionId: "telnet-1",
data: "tthdf 0 2323\nadmin\ntest123\n\r",
automated: true,
lineDelayMs: 5,
},
);
assert.deepEqual(calls, ["tthdf 0 2323\r\n"]);
await delay(30);
assert.deepEqual(calls, ["tthdf 0 2323\r\n", "admin\r\n", "test123\r\n", "\r\n"]);
});
test("manual input cancels pending automated lines", async () => {
const calls = [];
const sessions = new Map();
sessions.set("telnet-1", {
type: "telnet-native",
socket: {
write(data) {
calls.push(data);
},
},
});
initBridge(sessions);
terminalBridge.writeToSession(
{ sender: {} },
{
sessionId: "telnet-1",
data: "first\nsecond\nthird\r",
automated: true,
lineDelayMs: 20,
},
);
terminalBridge.writeToSession({ sender: {} }, { sessionId: "telnet-1", data: "\x03" });
await delay(60);
assert.deepEqual(calls, ["first\r\n", "\x03"]);
});
test("closing a paused SSH session does not resume the output flood first", () => {
const calls = [];
const sessions = new Map();
sessions.set("ssh-1", {
stream: {
pause() {
calls.push("pause");
},
resume() {
calls.push("resume");
},
close() {
calls.push("close");
},
},
});
initBridge(sessions);
terminalBridge.setSessionFlowPaused({ sender: {} }, { sessionId: "ssh-1", paused: true });
terminalBridge.closeSession({ sender: {} }, { sessionId: "ssh-1" });
assert.deepEqual(calls, ["pause", "close"]);
});