Files
NetMesh/electron/terminalWorker/runtime.test.cjs

1468 lines
44 KiB
JavaScript
Raw Normal View History

const assert = require("node:assert/strict");
const test = require("node:test");
const { createTerminalWorkerRuntime } = require("./runtime.cjs");
class FakePort {
constructor() {
this.messages = [];
this.closed = false;
this.started = false;
this.listeners = new Map();
}
postMessage(message) {
this.messages.push(message);
}
start() {
this.started = true;
}
close() {
this.closed = true;
}
on(channel, callback) {
this.listeners.set(channel, callback);
}
emitMessage(message) {
const callback = this.listeners.get("message");
if (callback) {
callback({ data: message });
return;
}
this.onmessage?.({ data: message });
}
}
function createParentPort() {
const messages = [];
const listeners = new Map();
return {
messages,
on(channel, cb) {
listeners.set(channel, cb);
},
postMessage(message) {
messages.push(message);
},
emitMessage(message) {
listeners.get("message")?.(message);
},
};
}
test("runtime invokes registered request handlers and posts responses", async () => {
const parentPort = createParentPort();
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:test", async (_event, payload) => ({ ok: true, payload }));
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:test",
payload: { value: 1 },
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages, [
{
kind: "response",
requestId: "req-1",
result: { ok: true, payload: { value: 1 } },
},
]);
});
test("runtime serializes overlapping same-id starts and closes the superseded session first", async () => {
const parentPort = createParentPort();
const order = [];
let active = null;
let releaseFirst;
const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:local:reconnect", async (_event, payload) => {
order.push(`start:${payload.attempt}`);
if (payload.attempt === "first") await firstGate;
active = payload.attempt;
order.push(`publish:${payload.attempt}`);
return { sessionId: payload.sessionId };
});
ipcMain.handle("netcatty:close:await", (_event, payload) => {
order.push(`close:${active}:${payload.sessionId}`);
active = null;
});
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:local:reconnect",
payload: { sessionId: "session-1", attempt: "first" },
webContentsId: 7,
});
parentPort.emitMessage({
kind: "request",
requestId: "req-2",
channel: "netcatty:local:reconnect",
payload: { sessionId: "session-1", attempt: "second" },
webContentsId: 8,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(order, ["start:first"]);
releaseFirst();
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(order, [
"start:first",
"publish:first",
"close:first:session-1",
"start:second",
"publish:second",
]);
assert.equal(active, "second");
assert.deepEqual(
parentPort.messages.filter((message) => message.kind === "response").map((message) => message.requestId),
["req-1", "req-2"],
);
});
test("runtime closes a completed same-id start before a later request begins", async () => {
const parentPort = createParentPort();
const order = [];
let active = null;
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:local:reconnect", async (_event, payload) => {
order.push(`start:${payload.attempt}`);
active = payload.attempt;
order.push(`publish:${payload.attempt}`);
return { sessionId: payload.sessionId };
});
ipcMain.handle("netcatty:close:await", (_event, payload) => {
order.push(`close:${active}:${payload.sessionId}`);
active = null;
});
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:local:reconnect",
payload: { sessionId: "session-1", attempt: "first" },
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
parentPort.emitMessage({
kind: "request",
requestId: "req-2",
channel: "netcatty:local:reconnect",
payload: { sessionId: "session-1", attempt: "second" },
webContentsId: 8,
});
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(order, [
"start:first",
"publish:first",
"close:first:session-1",
"start:second",
"publish:second",
]);
assert.equal(active, "second");
assert.deepEqual(
parentPort.messages.filter((message) => message.kind === "session-superseding"),
[{
kind: "session-superseding",
sessionId: "session-1",
sessionGeneration: 0,
replacementRequestId: "req-2",
}],
);
});
test("runtime records a generated session id so a later same-id start closes it first", async () => {
const parentPort = createParentPort();
const order = [];
let active = null;
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:local:start", async (_event, payload) => {
order.push(`start:${payload.attempt}`);
active = payload.attempt;
return { sessionId: "generated-1" };
});
ipcMain.handle("netcatty:close:await", (_event, payload) => {
order.push(`close:${active}:${payload.sessionId}`);
active = null;
});
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:local:start",
payload: { attempt: "first" },
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
parentPort.emitMessage({
kind: "request",
requestId: "req-2",
channel: "netcatty:local:start",
payload: { sessionId: "generated-1", attempt: "second" },
webContentsId: 8,
});
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(order, [
"start:first",
"close:first:generated-1",
"start:second",
]);
assert.equal(active, "second");
assert.deepEqual(
parentPort.messages
.filter((message) => message.kind === "response")
.map((message) => message.sessionGeneration),
[0, 1],
);
});
test("runtime close waits for a pending start and removes the session it publishes", async () => {
const parentPort = createParentPort();
const order = [];
let active = null;
let releaseStart;
const startGate = new Promise((resolve) => { releaseStart = resolve; });
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:local:reconnect", async (_event, payload) => {
order.push("start");
await startGate;
active = payload.sessionId;
order.push("publish");
return { sessionId: payload.sessionId };
});
ipcMain.on("netcatty:close", (_event, payload) => {
order.push(`close:${payload.sessionId}`);
active = null;
});
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:local:reconnect",
payload: { sessionId: "session-1" },
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(order, ["start"]);
parentPort.emitMessage({
kind: "send",
channel: "netcatty:close",
payload: { sessionId: "session-1" },
webContentsId: 7,
});
releaseStart();
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(order, ["start", "publish", "close:session-1"]);
assert.equal(active, null);
});
test("runtime close cancels every same-id start that was queued before it", async () => {
const parentPort = createParentPort();
const starts = [];
let active = null;
let releaseFirst;
const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:local:reconnect", async (_event, payload) => {
starts.push(payload.attempt);
if (payload.attempt === "first") await firstGate;
active = payload.attempt;
return { sessionId: payload.sessionId };
});
ipcMain.on("netcatty:close", () => { active = null; });
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:local:reconnect",
payload: { sessionId: "session-1", attempt: "first" },
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
parentPort.emitMessage({
kind: "request",
requestId: "req-2",
channel: "netcatty:local:reconnect",
payload: { sessionId: "session-1", attempt: "second" },
webContentsId: 7,
});
parentPort.emitMessage({
kind: "send",
channel: "netcatty:close",
payload: { sessionId: "session-1" },
webContentsId: 7,
});
releaseFirst();
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(starts, ["first"]);
assert.equal(active, null);
assert.equal(parentPort.messages.some((message) => (
message.requestId === "req-2" && /cancelled by close/u.test(message.error)
)), true);
});
test("runtime tags a failing start exit so its queued replacement can distinguish it", async () => {
const parentPort = createParentPort();
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:local:reconnect", async (event, payload) => {
if (payload.attempt === "first") {
event.sender.send("netcatty:exit", {
sessionId: payload.sessionId,
reason: "failed",
});
throw new Error("first failed");
}
return { sessionId: payload.sessionId };
});
ipcMain.handle("netcatty:close:await", () => {});
},
});
runtime.start();
for (const [requestId, attempt] of [["req-1", "first"], ["req-2", "second"]]) {
parentPort.emitMessage({
kind: "request",
requestId,
channel: "netcatty:local:reconnect",
payload: { sessionId: "session-1", attempt },
webContentsId: 7,
});
}
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
const exit = parentPort.messages.find((message) => message.kind === "renderer-event");
assert.equal(exit.originRequestId, "req-1");
assert.equal(parentPort.messages.some((message) => (
message.requestId === "req-2" && message.result?.sessionId === "session-1"
)), true);
});
test("runtime prunes sessions that naturally exit before their start response completes", async () => {
const parentPort = createParentPort();
const runtime = createTerminalWorkerRuntime({
parentPort,
sessionLifecycleTombstoneTtlMs: 60_000,
maxSessionLifecycleTombstones: 1,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:local:start", async (event, payload) => {
const generation = event.sender.claimSessionGeneration(payload.sessionId);
event.sender.send("netcatty:exit", {
sessionId: payload.sessionId,
reason: "exited-during-start",
_terminalSessionGeneration: generation,
});
return { sessionId: payload.sessionId };
});
},
});
runtime.start();
for (let index = 0; index < 3; index += 1) {
parentPort.emitMessage({
kind: "request",
requestId: `start-exit-${index}`,
channel: "netcatty:local:start",
payload: { sessionId: `start-exit-${index}` },
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
}
assert.deepEqual(runtime._getSessionLifecycleStateCountsForTests(), {
outputGenerations: 1,
closeEpochs: 0,
});
});
test("runtime routes interceptor ports to the worker-owned data pipeline", () => {
const parentPort = createParentPort();
const attached = [];
const detached = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
attach(message, port) { attached.push({ message, port }); },
detach(sessionId, direction) { detached.push({ sessionId, direction }); },
},
registerBridges() {},
});
runtime.start();
const port = new FakePort();
parentPort.emitMessage({
data: { kind: "terminal-interceptor-port", sessionId: "session-1", direction: "output" },
ports: [port],
});
assert.equal(attached[0].port, port);
parentPort.emitMessage({ kind: "terminal-interceptor-detach", sessionId: "session-1", direction: "output" });
assert.deepEqual(detached, [{ sessionId: "session-1", direction: "output" }]);
});
test("fresh sender lookups preserve an explicitly bound backend session generation", () => {
const parentPort = createParentPort();
const detached = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
detach(sessionId, direction, reason) { detached.push({ sessionId, direction, reason }); },
},
registerBridges() {},
});
runtime.start();
const oldGeneration = runtime.createSender(7).claimSessionGeneration("session-1");
runtime.createSender(7).send("netcatty:exit", {
sessionId: "session-1",
reason: "error",
_terminalSessionGeneration: oldGeneration,
});
const newGeneration = runtime.createSender(8).claimSessionGeneration("session-1");
runtime.createSender(7).send("netcatty:exit", {
sessionId: "session-1",
reason: "close",
_terminalSessionGeneration: oldGeneration,
});
runtime.createSender(8).send("netcatty:exit", {
sessionId: "session-1",
reason: "exited",
_terminalSessionGeneration: newGeneration,
});
assert.deepEqual(
parentPort.messages.map((message) => message.sessionGeneration),
[0, 0, 1],
);
assert.equal(parentPort.messages.some((message) => (
Object.hasOwn(message.payload, "_terminalSessionGeneration")
)), false);
assert.deepEqual(detached, [
{ sessionId: "session-1", direction: undefined, reason: "session-closed" },
{ sessionId: "session-1", direction: undefined, reason: "session-closed" },
]);
});
test("stale backend output is rejected before taps, observation, or interception", async () => {
const parentPort = createParentPort();
const observed = [];
const intercepted = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return 3; },
observeOutput(sessionId, data) { observed.push({ sessionId, data }); return false; },
async interceptOutput(sessionId, data) {
intercepted.push({ sessionId, data });
return data;
},
detach() {},
},
registerBridges() {},
});
runtime.start();
const oldSender = runtime.createSender(7);
const oldGeneration = oldSender.claimSessionGeneration("session-1");
oldSender.send("netcatty:exit", {
sessionId: "session-1",
reason: "error",
_terminalSessionGeneration: oldGeneration,
});
const newSender = runtime.createSender(8);
const newGeneration = newSender.claimSessionGeneration("session-1");
parentPort.messages.length = 0;
runtime.createSender(7).send("netcatty:data", {
sessionId: "session-1",
data: "old-secret",
_terminalSessionGeneration: oldGeneration,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages, []);
assert.deepEqual(observed, []);
assert.deepEqual(intercepted, []);
runtime.createSender(8).send("netcatty:data", {
sessionId: "session-1",
data: "new-output",
_terminalSessionGeneration: newGeneration,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(observed, [{ sessionId: "session-1", data: "new-output" }]);
assert.deepEqual(intercepted, [{ sessionId: "session-1", data: "new-output" }]);
assert.deepEqual(parentPort.messages.map((message) => message.kind), ["output-tap", "output"]);
});
test("runtime bounds closed lifecycle generations and safely reuses a recent session id", async () => {
const parentPort = createParentPort();
let now = 0;
const runtime = createTerminalWorkerRuntime({
parentPort,
now: () => now,
sessionLifecycleTombstoneTtlMs: 100,
maxSessionLifecycleTombstones: 8,
registerBridges(ipcMain) {
ipcMain.on("netcatty:close", () => {});
},
});
runtime.start();
const recentSenders = new Map();
for (let index = 0; index < 24; index += 1) {
const sessionId = `closed-${index}`;
const sender = runtime.createSender(7);
recentSenders.set(sessionId, {
sender,
generation: sender.claimSessionGeneration(sessionId),
});
parentPort.emitMessage({
kind: "send",
channel: "netcatty:close",
payload: { sessionId },
webContentsId: 7,
});
}
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(runtime._getSessionLifecycleStateCountsForTests(), {
outputGenerations: 8,
closeEpochs: 8,
});
const sessionId = "closed-23";
const recent = recentSenders.get(sessionId);
parentPort.messages.length = 0;
now = 99;
recent.sender.send("netcatty:data", {
sessionId,
data: "stale",
_terminalSessionGeneration: recent.generation,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages, [], "old sender stays fenced during the safety window");
const replacement = runtime.createSender(8);
const replacementGeneration = replacement.claimSessionGeneration(sessionId);
replacement.send("netcatty:data", {
sessionId,
data: "fresh",
_terminalSessionGeneration: replacementGeneration,
});
await new Promise((resolve) => setImmediate(resolve));
assert.ok(replacementGeneration > recent.generation);
assert.deepEqual(parentPort.messages.map((message) => message.kind), ["output-tap", "output"]);
});
test("runtime bounds natural-exit lifecycle generations and allows same-id restart", async () => {
const parentPort = createParentPort();
let now = 0;
let starts = 0;
const senders = new Map();
const generations = new Map();
const runtime = createTerminalWorkerRuntime({
parentPort,
now: () => now,
sessionLifecycleTombstoneTtlMs: 100,
maxSessionLifecycleTombstones: 8,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:local:start", async (event, payload) => {
starts += 1;
senders.set(payload.sessionId, event.sender);
generations.set(payload.sessionId, event.sender.claimSessionGeneration(payload.sessionId));
return { sessionId: payload.sessionId };
});
},
});
runtime.start();
for (let index = 0; index < 24; index += 1) {
const sessionId = `natural-exit-${index}`;
parentPort.emitMessage({
kind: "request",
requestId: `start-${index}`,
channel: "netcatty:local:start",
payload: { sessionId },
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
senders.get(sessionId).send("netcatty:exit", {
sessionId,
reason: "exited",
_terminalSessionGeneration: generations.get(sessionId),
});
}
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(runtime._getSessionLifecycleStateCountsForTests(), {
outputGenerations: 8,
closeEpochs: 0,
});
const sessionId = "natural-exit-23";
const exitedSender = senders.get(sessionId);
const exitedGeneration = generations.get(sessionId);
parentPort.messages.length = 0;
now = 99;
exitedSender.send("netcatty:data", {
sessionId,
data: "late-output",
_terminalSessionGeneration: exitedGeneration,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages, [], "natural exit keeps late output fenced");
parentPort.emitMessage({
kind: "request",
requestId: "restart-natural-exit",
channel: "netcatty:local:start",
payload: { sessionId },
webContentsId: 8,
});
await new Promise((resolve) => setImmediate(resolve));
const replacementSender = senders.get(sessionId);
const replacementGeneration = generations.get(sessionId);
assert.ok(replacementGeneration > exitedGeneration);
assert.equal(starts, 25);
parentPort.messages.length = 0;
replacementSender.send("netcatty:data", {
sessionId,
data: "fresh-output",
_terminalSessionGeneration: replacementGeneration,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages.map((message) => message.kind), ["output-tap", "output"]);
});
test("runtime invokes fire-and-forget listeners", () => {
const parentPort = createParentPort();
const calls = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.on("netcatty:write", (_event, payload) => calls.push(payload));
},
});
runtime.start();
parentPort.emitMessage({
kind: "send",
channel: "netcatty:write",
payload: { sessionId: "s1", data: "x" },
webContentsId: 7,
});
assert.deepEqual(calls, [{ sessionId: "s1", data: "x" }]);
});
test("runtime sends output drain markers through the session output port", () => {
const parentPort = createParentPort();
const outputPort = new FakePort();
const runtime = createTerminalWorkerRuntime({ parentPort, registerBridges() {} });
runtime.start();
parentPort.emitMessage({
data: { kind: "output-port", sessionId: "s1" },
ports: [outputPort],
});
parentPort.emitMessage({ kind: "output-drain", sessionId: "s1", requestId: "drain-1" });
assert.deepEqual(outputPort.messages, [
{ kind: "drain", requestId: "drain-1", sessionId: "s1" },
]);
});
test("runtime closes an urgent input port when its renderer is destroyed", () => {
const parentPort = createParentPort();
const urgentPort = new FakePort();
const runtime = createTerminalWorkerRuntime({ parentPort, registerBridges() {} });
runtime.start();
parentPort.emitMessage({
data: { kind: "urgent-input-port", webContentsId: 7 },
ports: [urgentPort],
});
parentPort.emitMessage({ kind: "close-urgent-input-port", webContentsId: 7 });
assert.equal(urgentPort.closed, true);
});
test("runtime routes urgent input port interrupts to the interrupt listener", () => {
const parentPort = createParentPort();
const urgentPort = new FakePort();
const calls = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.on("netcatty:interrupt", (event, payload) => {
calls.push({ senderId: event.sender.id, payload });
});
},
});
runtime.start();
parentPort.emitMessage({
data: {
kind: "urgent-input-port",
webContentsId: 7,
},
ports: [urgentPort],
});
urgentPort.emitMessage({
kind: "interrupt",
sessionId: "s1",
trace: { traceId: "trace-1" },
});
assert.equal(urgentPort.started, true);
assert.deepEqual(calls, [
{
senderId: 7,
payload: {
sessionId: "s1",
trace: { traceId: "trace-1" },
urgentInputPort: true,
},
},
]);
});
test("runtime clears host-sensitive input state before dispatching an interrupt", () => {
const parentPort = createParentPort();
const order = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
clearSensitiveInput(sessionId) { order.push(`clear:${sessionId}`); },
},
registerBridges(ipcMain) {
ipcMain.on("netcatty:interrupt", (_event, payload) => order.push(`write:${payload.sessionId}`));
},
});
runtime.start();
parentPort.emitMessage({
kind: "send",
channel: "netcatty:interrupt",
payload: { sessionId: "s1" },
webContentsId: 7,
});
assert.deepEqual(order, ["clear:s1", "write:s1"]);
});
test("runtime routes terminal data over output messages", async () => {
const parentPort = createParentPort();
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:test", (event) => {
event.sender.send("netcatty:data", { sessionId: "s1", data: "hello" });
return { done: true };
});
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:test",
payload: {},
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages[0], {
kind: "output-tap",
sessionId: "s1",
data: "hello",
sessionGeneration: 0,
});
assert.deepEqual(parentPort.messages[1], {
kind: "output",
sessionId: "s1",
data: "hello",
tapped: true,
sessionGeneration: 0,
});
});
test("runtime keeps the no-interceptor output path synchronous and allocation-free", async () => {
const parentPort = createParentPort();
let interceptCalls = 0;
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return 0; },
async interceptOutput() { interceptCalls += 1; return "changed"; },
},
registerBridges(ipcMain) {
ipcMain.handle("netcatty:test", (event) => {
event.sender.send("netcatty:data", { sessionId: "s1", data: "hello" });
assert.equal(parentPort.messages[1]?.data, "hello");
return null;
});
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-no-plugin",
channel: "netcatty:test",
payload: {},
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(interceptCalls, 0);
});
test("runtime sends transformed output to the renderer while host taps retain original data", async () => {
const parentPort = createParentPort();
const observed = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return 3; },
observeOutput(sessionId, data) { observed.push({ sessionId, data }); return true; },
async interceptOutput(_sessionId, data) { return String(data).toUpperCase(); },
},
registerBridges(ipcMain) {
ipcMain.handle("netcatty:test", (event) => {
event.sender.send("netcatty:data", { sessionId: "s1", data: "hello" });
return null;
});
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-plugin",
channel: "netcatty:test",
payload: {},
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages[0], {
kind: "output-tap",
sessionId: "s1",
data: "hello",
sessionGeneration: 0,
});
assert.deepEqual(parentPort.messages[1], {
kind: "output",
sessionId: "s1",
data: "HELLO",
tapped: true,
sessionGeneration: 0,
meta: {
pluginPipelineIngressBytes: 5,
pluginPipelineProcessed: true,
pluginPipelineSensitiveInput: true,
},
});
assert.deepEqual(observed, [{ sessionId: "s1", data: "hello" }]);
});
test("runtime classifies original prompts when only an output interceptor is active", async () => {
const parentPort = createParentPort();
const observed = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return 2; },
observeOutput(sessionId, data) { observed.push({ sessionId, data }); return true; },
async interceptOutput() { return "masked> "; },
},
registerBridges() {},
});
runtime.start();
runtime.createSender(7).send("netcatty:data", { sessionId: "s1", data: "Password: " });
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(observed, [{ sessionId: "s1", data: "Password: " }]);
assert.deepEqual(parentPort.messages.at(-1), {
kind: "output",
sessionId: "s1",
data: "masked> ",
tapped: true,
sessionGeneration: 0,
meta: {
pluginPipelineIngressBytes: 10,
pluginPipelineProcessed: true,
pluginPipelineSensitiveInput: true,
},
});
});
test("runtime publishes an explicit sensitive-state clear from original output", async () => {
const parentPort = createParentPort();
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return 3; },
observeOutput() { return false; },
async interceptOutput() { return "renamed prompt> "; },
},
registerBridges() {},
});
runtime.start();
runtime.createSender(7).send("netcatty:data", {
sessionId: "s1",
data: "\r\nAccess denied\r\n$ ",
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(
parentPort.messages.at(-1).meta.pluginPipelineSensitiveInput,
false,
);
});
test("runtime delivers pending intercepted output before closing the session", async () => {
const parentPort = createParentPort();
let releaseOutput;
const detached = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return 2; },
observeOutput() { return false; },
interceptOutput() {
return new Promise((resolve) => { releaseOutput = resolve; });
},
detach(sessionId, direction, reason) { detached.push({ sessionId, direction, reason }); },
},
registerBridges() {},
});
runtime.start();
runtime.createSender(7).send("netcatty:data", { sessionId: "s1", data: "final" });
runtime.createSender(7).send("netcatty:exit", { sessionId: "s1", reason: "closed" });
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages.map((message) => message.kind), ["output-tap"]);
releaseOutput("FINAL");
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages.map((message) => message.kind), [
"output-tap",
"output",
"renderer-event",
]);
assert.equal(parentPort.messages[1].data, "FINAL");
assert.equal(parentPort.messages[2].channel, "netcatty:exit");
assert.deepEqual(detached, [{ sessionId: "s1", direction: undefined, reason: "session-closed" }]);
});
test("runtime keeps direct output ordered behind a pending chunk after interceptor disable", async () => {
const parentPort = createParentPort();
let mode = 2;
let releaseOutput;
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return mode; },
observeOutput() { return false; },
interceptOutput() {
mode = 0;
return new Promise((resolve) => { releaseOutput = resolve; });
},
},
registerBridges() {},
});
runtime.start();
runtime.createSender(7).send("netcatty:data", { sessionId: "s1", data: "first" });
runtime.createSender(7).send("netcatty:data", { sessionId: "s1", data: "second" });
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages.map((message) => message.kind), ["output-tap", "output-tap"]);
releaseOutput("FIRST");
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(
parentPort.messages.filter((message) => message.kind === "output").map((message) => message.data),
["FIRST", "second"],
);
});
test("runtime drops a pending intercepted chunk after the session output route is reused", async () => {
const parentPort = createParentPort();
const oldPort = new FakePort();
const newPort = new FakePort();
const detached = [];
let releaseOutput;
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return 2; },
observeOutput() { return false; },
interceptOutput() {
return new Promise((resolve) => { releaseOutput = resolve; });
},
detach(sessionId, direction, reason) { detached.push({ sessionId, direction, reason }); },
},
registerBridges() {},
});
runtime.start();
parentPort.emitMessage({
data: { kind: "output-port", sessionId: "s1", bufferedOutput: [] },
ports: [oldPort],
});
runtime.createSender(7).send("netcatty:data", { sessionId: "s1", data: "old" });
await new Promise((resolve) => setImmediate(resolve));
parentPort.emitMessage({ kind: "close-output-port", sessionId: "s1" });
parentPort.emitMessage({
data: { kind: "output-port", sessionId: "s1", bufferedOutput: [] },
ports: [newPort],
});
releaseOutput("STALE");
await new Promise((resolve) => setImmediate(resolve));
assert.equal(oldPort.closed, true);
assert.deepEqual(newPort.messages, []);
assert.deepEqual(detached, [{
sessionId: "s1",
direction: undefined,
reason: "session-closed",
}]);
});
test("runtime submits queued output to the bounded pipeline before earlier transforms finish", async () => {
const parentPort = createParentPort();
const releases = [];
const calls = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode() { return 2; },
observeOutput() { return false; },
interceptOutput(_sessionId, data) {
calls.push(data);
return new Promise((resolve) => releases.push(() => resolve(data.toUpperCase())));
},
},
registerBridges() {},
});
runtime.start();
runtime.createSender(7).send("netcatty:data", { sessionId: "s1", data: "first" });
runtime.createSender(7).send("netcatty:data", { sessionId: "s1", data: "second" });
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(calls, ["first", "second"]);
releases[0]();
releases[1]();
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(
parentPort.messages.filter((message) => message.kind === "output").map((message) => message.data),
["FIRST", "SECOND"],
);
});
test("runtime routes terminal data over a transferred output port", async () => {
const parentPort = createParentPort();
const outputPort = new FakePort();
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:test", (event) => {
event.sender.send("netcatty:data", { sessionId: "s1", data: "hello" });
return { done: true };
});
},
});
runtime.start();
parentPort.emitMessage({
data: {
kind: "output-port",
sessionId: "s1",
bufferedOutput: ["early"],
},
ports: [outputPort],
});
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:test",
payload: {},
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
assert.equal(outputPort.started, true);
assert.deepEqual(outputPort.messages, [
{ sessionId: "s1", data: "early" },
{ sessionId: "s1", data: "hello" },
]);
assert.equal(parentPort.messages[0].kind, "output-port-ready");
assert.deepEqual(parentPort.messages[1], {
kind: "output-tap",
sessionId: "s1",
data: "hello",
sessionGeneration: 0,
});
assert.equal(parentPort.messages.some((message) => message.kind === "output"), false);
});
test("runtime routes buffered startup output through the selected interceptor", async () => {
const parentPort = createParentPort();
const outputPort = new FakePort();
const intercepted = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode: () => 2,
observeOutput: () => false,
interceptOutput(sessionId, data) {
intercepted.push({ sessionId, data });
return Promise.resolve(data.toUpperCase());
},
},
registerBridges() {},
});
runtime.start();
parentPort.emitMessage({
data: {
kind: "output-port",
sessionId: "s1",
bufferedOutput: [{ data: "early", meta: { source: "startup" } }],
},
ports: [outputPort],
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(intercepted, [{ sessionId: "s1", data: "early" }]);
assert.deepEqual(outputPort.messages, [{
sessionId: "s1",
data: "EARLY",
meta: {
source: "startup",
pluginPipelineIngressBytes: 5,
pluginPipelineProcessed: true,
pluginPipelineSensitiveInput: false,
},
}]);
assert.deepEqual(parentPort.messages, [{ kind: "output-port-ready", sessionId: "s1" }]);
});
test("runtime does not re-intercept already transformed buffered output", async () => {
const parentPort = createParentPort();
const outputPort = new FakePort();
const intercepted = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode: () => 2,
observeOutput: () => false,
interceptOutput(sessionId, data) {
intercepted.push({ sessionId, data });
return Promise.resolve(`again:${data}`);
},
},
registerBridges() {},
});
runtime.start();
parentPort.emitMessage({
data: {
kind: "output-port",
sessionId: "s1",
bufferedOutput: [{
data: "EARLY",
meta: {
pluginPipelineIngressBytes: 5,
pluginPipelineProcessed: true,
pluginPipelineSensitiveInput: true,
},
}],
},
ports: [outputPort],
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(intercepted, []);
assert.deepEqual(outputPort.messages, [{
sessionId: "s1",
data: "EARLY",
meta: {
pluginPipelineIngressBytes: 5,
pluginPipelineProcessed: true,
pluginPipelineSensitiveInput: true,
},
}]);
assert.deepEqual(parentPort.messages, [{ kind: "output-port-ready", sessionId: "s1" }]);
});
test("runtime re-intercepts raw buffered output that only inherited ingress accounting", async () => {
const parentPort = createParentPort();
const outputPort = new FakePort();
const intercepted = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode: () => 2,
observeOutput: () => false,
interceptOutput(sessionId, data) {
intercepted.push({ sessionId, data });
return Promise.resolve(`processed:${data}`);
},
},
registerBridges() {},
});
runtime.start();
parentPort.emitMessage({
data: {
kind: "output-port",
sessionId: "s1",
bufferedOutput: [{
data: "raw",
meta: { pluginPipelineIngressBytes: 9 },
}],
},
ports: [outputPort],
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(intercepted, [{ sessionId: "s1", data: "raw" }]);
assert.deepEqual(outputPort.messages, [{
sessionId: "s1",
data: "processed:raw",
meta: {
pluginPipelineIngressBytes: 12,
pluginPipelineProcessed: true,
pluginPipelineSensitiveInput: false,
},
}]);
assert.deepEqual(parentPort.messages, [{ kind: "output-port-ready", sessionId: "s1" }]);
});
test("runtime adds retained raw bytes after manager partial-trim accounting", async () => {
const parentPort = createParentPort();
const outputPort = new FakePort();
const intercepted = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode: () => 2,
observeOutput: () => false,
interceptOutput(sessionId, data) {
intercepted.push({ sessionId, data });
return Promise.resolve(`processed:${data}`);
},
},
registerBridges() {},
});
runtime.start();
parentPort.emitMessage({
data: {
kind: "output-port",
sessionId: "s1",
bufferedOutput: [{
data: "bcde",
meta: { pluginPipelineIngressBytes: 11 },
}],
},
ports: [outputPort],
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(intercepted, [{ sessionId: "s1", data: "bcde" }]);
assert.deepEqual(outputPort.messages, [{
sessionId: "s1",
data: "processed:bcde",
meta: {
pluginPipelineIngressBytes: 15,
pluginPipelineProcessed: true,
pluginPipelineSensitiveInput: false,
},
}]);
});
test("runtime counts retained raw bytes when replay continues without an interceptor", async () => {
const parentPort = createParentPort();
const outputPort = new FakePort();
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: {
getOutputMode: () => 0,
observeOutput: () => false,
},
registerBridges() {},
});
runtime.start();
parentPort.emitMessage({
data: {
kind: "output-port",
sessionId: "s1",
bufferedOutput: [{
data: "raw",
meta: { pluginPipelineIngressBytes: 9 },
}],
},
ports: [outputPort],
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(outputPort.messages, [{
sessionId: "s1",
data: "raw",
meta: { pluginPipelineIngressBytes: 12 },
}]);
});
test("runtime.createSender uses the transferred output port", () => {
const parentPort = createParentPort();
const outputPort = new FakePort();
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges() {},
});
runtime.start();
parentPort.emitMessage({
data: { kind: "output-port", sessionId: "s1" },
ports: [outputPort],
});
runtime.createSender(7).send("netcatty:data", { sessionId: "s1", data: "hello" });
assert.deepEqual(outputPort.messages, [{ sessionId: "s1", data: "hello" }]);
assert.deepEqual(parentPort.messages.at(-1), {
kind: "output-tap",
sessionId: "s1",
data: "hello",
sessionGeneration: 0,
});
});
test("runtime forwards non-output renderer events to the parent", async () => {
const parentPort = createParentPort();
const runtime = createTerminalWorkerRuntime({
parentPort,
registerBridges(ipcMain) {
ipcMain.handle("netcatty:test", (event) => {
event.sender.send("netcatty:exit", { sessionId: "s1", reason: "closed" });
return { done: true };
});
},
});
runtime.start();
parentPort.emitMessage({
kind: "request",
requestId: "req-1",
channel: "netcatty:test",
payload: {},
webContentsId: 7,
});
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(parentPort.messages[0], {
kind: "renderer-event",
webContentsId: 7,
channel: "netcatty:exit",
payload: { sessionId: "s1", reason: "closed" },
sessionGeneration: 0,
});
});
test("runtime contains throws from fire-and-forget send listeners", () => {
const parentPort = createParentPort();
const seenPayloads = [];
const runtime = createTerminalWorkerRuntime({
parentPort,
terminalDataPipeline: null,
registerBridges(ipcMain) {
ipcMain.on("netcatty:write", (_event, payload) => {
seenPayloads.push(payload);
throw new Error("listener blew up synchronously");
});
},
});
runtime.start();
assert.doesNotThrow(() => parentPort.emitMessage({
kind: "send",
channel: "netcatty:write",
payload: { sessionId: "s1", data: "ls\r" },
webContentsId: 7,
}));
assert.deepEqual(seenPayloads, [{ sessionId: "s1", data: "ls\r" }]);
// A following message must still dispatch normally after the swallowed throw.
assert.doesNotThrow(() => parentPort.emitMessage({
kind: "send",
channel: "netcatty:write",
payload: { sessionId: "s1", data: "pwd\r" },
webContentsId: 7,
}));
assert.deepEqual(seenPayloads, [
{ sessionId: "s1", data: "ls\r" },
{ sessionId: "s1", data: "pwd\r" },
]);
});