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

275 lines
8.0 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const net = require("node:net");
const iconv = require("iconv-lite");
const terminalBridge = require("./terminalBridge.cjs");
function listen(server) {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
server.off("error", reject);
resolve(server.address().port);
});
});
}
function waitFor(predicate, timeoutMs = 1000) {
const startedAt = Date.now();
return new Promise((resolve, reject) => {
const tick = () => {
if (predicate()) {
resolve();
return;
}
if (Date.now() - startedAt > timeoutMs) {
reject(new Error("Timed out waiting for telnet input bytes"));
return;
}
setTimeout(tick, 10);
};
tick();
});
}
// These tests drive the real terminalBridge.writeToSession path over a raw TCP
// "device" that never speaks the Telnet protocol (no IAC bytes), so the bytes
// captured server-side are exactly what the input path serialized — proving
// the keystroke encoding without IAC-escaping noise. They guard issue #1216:
// input must use the SAME charset the output decoder uses.
function initBridge(sessions) {
terminalBridge.init({
sessions,
electronModule: {
webContents: {
fromId: () => ({ send() {} }),
},
},
});
}
test("Telnet input is encoded with the session's GB18030 charset", async () => {
const chunks = [];
const sockets = new Set();
let serverSocket = null;
const server = net.createServer((socket) => {
serverSocket = socket;
sockets.add(socket);
socket.on("error", () => {});
socket.on("close", () => sockets.delete(socket));
socket.on("data", (buf) => chunks.push(buf));
});
const port = await listen(server);
const sessions = new Map();
initBridge(sessions);
try {
await terminalBridge.startTelnetSession(
{ sender: { id: 1 } },
{
sessionId: "telnet-gb18030-input",
hostname: "127.0.0.1",
port,
// No saved credentials → auto-login stays idle and does not inject bytes.
charset: "GB18030",
},
);
await waitFor(() => serverSocket);
terminalBridge.writeToSession(
{},
{ sessionId: "telnet-gb18030-input", data: "你好\r" },
);
await waitFor(() => Buffer.concat(chunks).length >= 6);
const received = Buffer.concat(chunks);
assert.deepEqual([...received], [...iconv.encode("你好\r\n", "gb18030")]);
// It must NOT be the UTF-8 serialization that the old code always sent.
assert.notDeepEqual([...received], [...Buffer.from("你好\r", "utf8")]);
} finally {
terminalBridge.cleanupAllSessions();
for (const socket of sockets) socket.destroy();
await new Promise((resolve) => server.close(resolve));
}
});
test("Telnet input stays UTF-8 when no charset is configured", async () => {
const chunks = [];
const sockets = new Set();
let serverSocket = null;
const server = net.createServer((socket) => {
serverSocket = socket;
sockets.add(socket);
socket.on("error", () => {});
socket.on("close", () => sockets.delete(socket));
socket.on("data", (buf) => chunks.push(buf));
});
const port = await listen(server);
const sessions = new Map();
initBridge(sessions);
try {
await terminalBridge.startTelnetSession(
{ sender: { id: 1 } },
{
sessionId: "telnet-utf8-input",
hostname: "127.0.0.1",
port,
},
);
await waitFor(() => serverSocket);
terminalBridge.writeToSession(
{},
{ sessionId: "telnet-utf8-input", data: "你好\r" },
);
await waitFor(() => Buffer.concat(chunks).length >= 8);
const received = Buffer.concat(chunks);
assert.deepEqual([...received], [...Buffer.from("你好\r\n", "utf8")]);
} finally {
terminalBridge.cleanupAllSessions();
for (const socket of sockets) socket.destroy();
await new Promise((resolve) => server.close(resolve));
}
});
test("Telnet Enter is sent as CRLF so RT-Thread shells submit the command", async () => {
const chunks = [];
const sockets = new Set();
let serverSocket = null;
const server = net.createServer((socket) => {
serverSocket = socket;
sockets.add(socket);
socket.on("error", () => {});
socket.on("close", () => sockets.delete(socket));
socket.on("data", (buf) => chunks.push(buf));
});
const port = await listen(server);
const sessions = new Map();
initBridge(sessions);
try {
await terminalBridge.startTelnetSession(
{ sender: { id: 1 } },
{
sessionId: "telnet-rtthread-enter",
hostname: "127.0.0.1",
port,
},
);
await waitFor(() => serverSocket);
terminalBridge.writeToSession(
{},
{ sessionId: "telnet-rtthread-enter", data: "ps\r" },
);
await waitFor(() => Buffer.concat(chunks).length >= 3);
const received = Buffer.concat(chunks);
assert.deepEqual([...received], [...Buffer.from("ps\r\n", "utf8")]);
} finally {
terminalBridge.cleanupAllSessions();
for (const socket of sockets) socket.destroy();
await new Promise((resolve) => server.close(resolve));
}
});
test("Telnet input preserves existing CRLF and CR NUL while normalizing bare LF", async () => {
const chunks = [];
const sockets = new Set();
let serverSocket = null;
const server = net.createServer((socket) => {
serverSocket = socket;
sockets.add(socket);
socket.on("error", () => {});
socket.on("close", () => sockets.delete(socket));
socket.on("data", (buf) => chunks.push(buf));
});
const port = await listen(server);
const sessions = new Map();
initBridge(sessions);
try {
await terminalBridge.startTelnetSession(
{ sender: { id: 1 } },
{
sessionId: "telnet-rfc-newlines",
hostname: "127.0.0.1",
port,
},
);
await waitFor(() => serverSocket);
terminalBridge.writeToSession(
{},
{ sessionId: "telnet-rfc-newlines", data: "one\r\ntwo\nthree\r\0" },
);
await waitFor(() => Buffer.concat(chunks).length >= 17);
const received = Buffer.concat(chunks);
assert.deepEqual([...received], [...Buffer.from("one\r\ntwo\r\nthree\r\0", "utf8")]);
} finally {
terminalBridge.cleanupAllSessions();
for (const socket of sockets) socket.destroy();
await new Promise((resolve) => server.close(resolve));
}
});
test("setSessionEncoding switches the Telnet input charset at runtime", async () => {
const chunks = [];
const sockets = new Set();
let serverSocket = null;
const server = net.createServer((socket) => {
serverSocket = socket;
sockets.add(socket);
socket.on("error", () => {});
socket.on("close", () => sockets.delete(socket));
socket.on("data", (buf) => chunks.push(buf));
});
const port = await listen(server);
const sessions = new Map();
initBridge(sessions);
try {
await terminalBridge.startTelnetSession(
{ sender: { id: 1 } },
{
sessionId: "telnet-switch-input",
hostname: "127.0.0.1",
port,
},
);
await waitFor(() => serverSocket);
const switchResult = terminalBridge.setSessionEncoding(
{},
{ sessionId: "telnet-switch-input", encoding: "gbk" },
);
// "gbk" normalizes onto the gb18030 superset and is mirrored to
// session.encoding so the input path picks it up immediately.
assert.deepEqual(switchResult, { ok: true, encoding: "gb18030" });
assert.equal(sessions.get("telnet-switch-input").encoding, "gb18030");
terminalBridge.writeToSession(
{},
{ sessionId: "telnet-switch-input", data: "测试\r" },
);
await waitFor(() => Buffer.concat(chunks).length >= 6);
const received = Buffer.concat(chunks);
assert.deepEqual([...received], [...iconv.encode("测试\r\n", "gb18030")]);
} finally {
terminalBridge.cleanupAllSessions();
for (const socket of sockets) socket.destroy();
await new Promise((resolve) => server.close(resolve));
}
});