[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:
274
electron/bridges/terminalBridge.inputEncoding.test.cjs
Normal file
274
electron/bridges/terminalBridge.inputEncoding.test.cjs
Normal file
@@ -0,0 +1,274 @@
|
||||
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));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user