[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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,669 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const Module = require("node:module");
function loadTerminalBridgeWithMocks() {
const bridgePath = require.resolve("./terminalBridge.cjs");
delete require.cache[bridgePath];
const opened = [];
const fakeChannel = {
openSession(sessionId, webContents) {
opened.push({ sessionId, webContentsId: webContents.id });
return true;
},
closeSession() {},
};
const originalRequire = Module.prototype.require;
Module.prototype.require = function patchedRequire(request) {
if (request === "./terminalOutputChannel.cjs" || request.endsWith("terminalOutputChannel.cjs")) {
return {
createTerminalOutputChannel: () => fakeChannel,
TERMINAL_OUTPUT_PORT_CHANNEL: "netcatty:terminal-output-port",
};
}
if (request === "./emitTerminalSessionData.cjs" || request.endsWith("emitTerminalSessionData.cjs")) {
return { configureTerminalSessionDataEmitter: () => {} };
}
if (request === "electron") {
return {};
}
return originalRequire.apply(this, arguments);
};
try {
const bridge = require("./terminalBridge.cjs");
return { bridge, opened, fakeChannel };
} finally {
Module.prototype.require = originalRequire;
}
}
test("rebindTerminalSessionOutput moves output and updates webContentsId", () => {
// Load bridge source helpers via init + direct IPC simulation is heavy;
// assert the implementation is registered and the openSession rebind contract
// used by the attach popup is present.
const source = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge.cjs"),
"utf8",
);
assert.match(source, /function rebindTerminalSessionOutput/);
assert.match(source, /function restoreTerminalSessionOutput/);
assert.match(source, /netcatty:terminal:rebindOutput/);
assert.match(source, /netcatty:terminal:restoreOutput/);
assert.match(source, /session\.webContentsId = sender\.id/);
});
test("rebind and restore handlers register even when terminal worker is enabled", () => {
const source = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge.cjs"),
"utf8",
);
// Must be registered before the worker early-return, otherwise production
// (worker-on) hits "No handler registered for rebindOutput" on first attach.
const rebindIdx = source.indexOf('ipcMain.handle("netcatty:terminal:rebindOutput"');
const restoreIdx = source.indexOf('ipcMain.handle("netcatty:terminal:restoreOutput"');
const snapshotIdx = source.indexOf('ipcMain.handle("netcatty:terminal:requestSnapshot"');
const workerReturnIdx = source.indexOf("].forEach((channel) => registerWorkerSend");
assert.ok(rebindIdx > 0, "rebind handler present");
assert.ok(restoreIdx > 0, "restore handler present");
assert.ok(snapshotIdx > 0, "snapshot handler present");
assert.ok(workerReturnIdx > 0, "worker send registration present");
assert.ok(rebindIdx < workerReturnIdx, "rebind registered before worker-only early return");
assert.ok(restoreIdx < workerReturnIdx, "restore registered before worker-only early return");
assert.ok(snapshotIdx < workerReturnIdx, "snapshot registered before worker-only early return");
assert.match(source, /terminalWorkerManager\.rebindOutputSession/);
assert.match(source, /function requestTerminalSessionSnapshot/);
});
test("main process arbitrates two renderer leases before forwarding to the worker", async () => {
const { bridge } = loadTerminalBridgeWithMocks();
const handles = new Map();
const listeners = new Map();
const sent = [];
const requested = [];
const ipcMain = {
handle(channel, handler) { handles.set(channel, handler); },
on(channel, handler) { listeners.set(channel, handler); },
};
const terminalWorkerManager = {
request: async (channel, payload) => {
requested.push({ channel, payload });
return { success: true };
},
send: (channel, payload) => sent.push({ channel, payload }),
};
bridge.registerHandlers(ipcMain, { terminalWorkerManager });
const sender = (id) => ({ id, once() {} });
const acquire = handles.get("netcatty:terminal:acquireFlowPauseLease");
const release = handles.get("netcatty:terminal:releaseFlowPauseLease");
const flow = listeners.get("netcatty:flow");
const home = await acquire({ sender: sender(10) }, { sessionId: "session-lease" });
const popup = await acquire({ sender: sender(20) }, { sessionId: "session-lease" });
flow({ sender: sender(10) }, { sessionId: "session-lease", paused: false });
await release(
{ sender: sender(10) },
{ sessionId: "session-lease", leaseId: home.leaseId },
);
await release(
{ sender: sender(20) },
{ sessionId: "session-lease", leaseId: popup.leaseId },
);
assert.deepEqual(requested.map(({ payload }) => payload.paused), [true, true]);
assert.deepEqual(sent.map(({ payload }) => payload.paused), [true, true, false]);
assert.ok([...requested, ...sent].every(({ payload }) => payload._flowArbitrated === true));
});
test("attach recovery respects a home renderer lease in worker mode", async () => {
const { bridge } = loadTerminalBridgeWithMocks();
const handles = new Map();
const sent = [];
const ipcMain = {
handle(channel, handler) { handles.set(channel, handler); },
on() {},
};
const terminalWorkerManager = {
request: async () => ({ success: true }),
send: (channel, payload) => sent.push({ channel, payload }),
restoreAttachHome: async () => ({ success: true, restored: true }),
};
bridge.registerHandlers(ipcMain, { terminalWorkerManager });
const sender = { id: 10, once() {} };
const acquire = handles.get("netcatty:terminal:acquireFlowPauseLease");
const release = handles.get("netcatty:terminal:releaseFlowPauseLease");
const home = await acquire({ sender }, { sessionId: "session-restore" });
const attachRestore = require("./terminalAttachRestore.cjs");
const restored = await attachRestore.restoreAttachedSessionOutput("session-restore");
assert.equal(restored.success, true);
assert.equal(sent.at(-1)?.payload?.paused, true);
await release(
{ sender },
{ sessionId: "session-restore", leaseId: home.leaseId },
);
assert.equal(sent.at(-1)?.payload?.paused, false);
});
test("duplicate popup crash restores share one in-flight operation", async () => {
const { bridge } = loadTerminalBridgeWithMocks();
const handles = new Map();
let restoreCalls = 0;
let finishRestore;
const restoreResult = new Promise((resolve) => { finishRestore = resolve; });
const ipcMain = {
handle(channel, handler) { handles.set(channel, handler); },
on() {},
};
const terminalWorkerManager = {
request: async () => ({ success: true }),
send() {},
restoreAttachHome: async () => {
restoreCalls += 1;
return restoreResult;
},
};
bridge.registerHandlers(ipcMain, { terminalWorkerManager });
const attachRestore = require("./terminalAttachRestore.cjs");
const renderGoneRestore = attachRestore.restoreAttachedSessionOutput("session-crash");
const closedRestore = attachRestore.restoreAttachedSessionOutput("session-crash");
await new Promise((resolve) => setImmediate(resolve));
assert.equal(restoreCalls, 1);
finishRestore({ success: true, restored: true });
assert.deepEqual(await Promise.all([renderGoneRestore, closedRestore]), [
{ success: true, restored: true },
{ success: true, restored: true },
]);
assert.equal(restoreCalls, 1);
});
test("failed worker lease acquisition compensates the backend pause", async () => {
const { bridge } = loadTerminalBridgeWithMocks();
const handles = new Map();
const sent = [];
const ipcMain = {
handle(channel, handler) { handles.set(channel, handler); },
on() {},
};
const terminalWorkerManager = {
request: async () => { throw new Error("worker unavailable"); },
send: (channel, payload) => sent.push({ channel, payload }),
};
bridge.registerHandlers(ipcMain, { terminalWorkerManager });
const acquire = handles.get("netcatty:terminal:acquireFlowPauseLease");
await assert.rejects(
acquire(
{ sender: { id: 10, once() {} } },
{ sessionId: "session-acquire-failure" },
),
/worker unavailable/,
);
assert.equal(sent.at(-1)?.payload?.paused, false);
assert.equal(sent.at(-1)?.payload?._flowArbitrated, true);
});
test("worker close and natural exit clear main-process pause ownership", async () => {
const { bridge } = loadTerminalBridgeWithMocks();
const handles = new Map();
const sends = new Map();
const ipcMain = {
handle(channel, handler) { handles.set(channel, handler); },
on(channel, handler) { sends.set(channel, handler); },
};
let notifySessionClosed = null;
const terminalWorkerManager = {
request: async (_channel, payload) => (
Number.isFinite(payload?.bootEpoch)
? { skipped: true }
: { success: true }
),
send() {},
onSessionClosed(listener) {
notifySessionClosed = listener;
return { dispose() {} };
},
};
bridge.registerHandlers(ipcMain, { terminalWorkerManager });
const sender = { id: 10, isDestroyed() {}, send() {} };
const acquire = handles.get("netcatty:terminal:acquireFlowPauseLease");
const release = handles.get("netcatty:terminal:releaseFlowPauseLease");
const closeAwait = handles.get("netcatty:close:await");
const closeFireAndForget = sends.get("netcatty:close");
const closingLease = await acquire({ sender }, { sessionId: "session-close" });
await closeAwait({ sender }, { sessionId: "session-close" });
assert.equal((await release(
{ sender },
{ sessionId: "session-close", leaseId: closingLease.leaseId },
)).success, false);
// Epoch-mismatch / skipped close must leave the replacement lease intact.
const skippedLease = await acquire({ sender }, { sessionId: "session-skipped" });
const skippedClose = await closeAwait(
{ sender },
{ sessionId: "session-skipped", bootEpoch: 1 },
);
assert.equal(skippedClose?.skipped, true);
assert.equal((await release(
{ sender },
{ sessionId: "session-skipped", leaseId: skippedLease.leaseId },
)).success, true);
// Fire-and-forget epoch-scoped close must also preserve leases (worker may
// no-op); unscoped close still clears eagerly.
const epochFireLease = await acquire({ sender }, { sessionId: "session-epoch-fire" });
closeFireAndForget({ sender }, { sessionId: "session-epoch-fire", bootEpoch: 2 });
assert.equal((await release(
{ sender },
{ sessionId: "session-epoch-fire", leaseId: epochFireLease.leaseId },
)).success, true);
const unscopedFireLease = await acquire({ sender }, { sessionId: "session-unscoped-fire" });
closeFireAndForget({ sender }, { sessionId: "session-unscoped-fire" });
assert.equal((await release(
{ sender },
{ sessionId: "session-unscoped-fire", leaseId: unscopedFireLease.leaseId },
)).success, false);
const exitLease = await acquire({ sender }, { sessionId: "session-exit" });
notifySessionClosed({ sessionId: "session-exit", reason: "exited" });
assert.equal((await release(
{ sender },
{ sessionId: "session-exit", leaseId: exitLease.leaseId },
)).success, false);
});
test("worker renderer-event forwarding prefers rebound webContentsId", () => {
const source = require("node:fs").readFileSync(
path.join(__dirname, "terminalWorkerManager.cjs"),
"utf8",
);
assert.match(source, /sessionWebContentsIds\.get\(sessionId\)/);
assert.match(source, /displayWebContentsId/);
assert.match(source, /attachHomeWebContentsIds/);
assert.match(source, /restoreAttachHome/);
// Exit cleanup must not run before we capture display/home targets.
const rendererEventIdx = source.indexOf('if (message.kind === "renderer-event")');
const captureIdx = source.indexOf("const displayWebContentsId =", rendererEventIdx);
const closeIdx = source.indexOf('if (message.channel === "netcatty:exit" && sessionId)', captureIdx);
assert.ok(captureIdx > 0 && closeIdx > captureIdx, "capture targets before closeOutputSession on exit");
assert.match(
source,
/message\.channel === "netcatty:exit" && homeWebContentsId != null/,
"only exit events fan out to the attach home",
);
});
test("in-process SSH and Telnet ZMODEM prompts follow the rebound display", () => {
const sshSource = require("node:fs").readFileSync(
path.join(__dirname, "sshBridge/startSession.cjs"),
"utf8",
);
const telnetSource = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge/telnetSession.cjs"),
"utf8",
);
const sshSentry = sshSource.slice(
sshSource.indexOf("const sshZmodemSentry"),
sshSource.indexOf("session.zmodemSentry = sshZmodemSentry"),
);
const telnetSentry = telnetSource.slice(
telnetSource.indexOf("const telnetZmodemSentry"),
telnetSource.indexOf("const attachTelnetSentry"),
);
assert.match(sshSentry, /getCurrentSessionWebContents/);
assert.match(sshSentry, /getCurrentSessionWebContentsId/);
assert.doesNotMatch(sshSentry, /event\.sender/);
assert.match(telnetSentry, /getCurrentTelnetWebContents/);
assert.match(telnetSentry, /getCurrentTelnetWebContentsId/);
assert.doesNotMatch(telnetSentry, /telnetWebContentsId/);
});
test("popup window closed lifecycle restores attach output", () => {
const source = require("node:fs").readFileSync(
path.join(__dirname, "windowManager/terminalPopupWindow.cjs"),
"utf8",
);
assert.match(source, /restoreAttachedSessionOutput\(attachSessionId\)/);
assert.match(source, /terminalAttachRestore/);
const crashStart = source.indexOf('"render-process-gone"');
const crashEnd = source.indexOf('"console-message"', crashStart);
assert.match(source.slice(crashStart, crashEnd), /win\.destroy\(\)/);
});
test("attach close restores the output route before resuming the backend", () => {
const bridgeSource = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge.cjs"),
"utf8",
);
const restoreStart = bridgeSource.indexOf("async function restoreAttachedSessionOutputOnce");
const restoreEnd = bridgeSource.indexOf("function restoreTerminalSessionOutput", restoreStart);
const restoreSource = bridgeSource.slice(restoreStart, restoreEnd);
const routeIdx = restoreSource.indexOf("restoreAttachHome");
const resumeIdx = restoreSource.lastIndexOf("setDirectPaused");
assert.ok(routeIdx > 0 && resumeIdx > routeIdx, "restore route before resuming output");
const terminalSource = require("node:fs").readFileSync(
path.join(__dirname, "../../components/Terminal.tsx"),
"utf8",
);
const cleanupStart = terminalSource.indexOf("Observe/attach popups must not kill");
const cleanupEnd = terminalSource.indexOf("const cleanupPromise", cleanupStart);
const cleanupSource = terminalSource.slice(cleanupStart, cleanupEnd);
const pauseIdx = cleanupSource.indexOf("acquireSessionFlowPauseLease(closingSessionId)");
const snapshotIdx = cleanupSource.indexOf("applySessionSnapshot");
const rendererRestoreIdx = cleanupSource.indexOf("restoreSessionOutput");
const disposeIdx = cleanupSource.indexOf("disposeSessionListeners()");
const releaseIdx = cleanupSource.indexOf("releaseTerminalFlowBeforeHibernate");
const leaseReleaseIdx = cleanupSource.indexOf("outputPauseLease.release()", releaseIdx);
assert.ok(pauseIdx > 0, "pause before final snapshot");
assert.ok(snapshotIdx > pauseIdx, "snapshot after pause");
assert.ok(rendererRestoreIdx > snapshotIdx, "restore route after snapshot");
assert.ok(disposeIdx > rendererRestoreIdx, "detach popup listener after route restore");
assert.ok(releaseIdx > disposeIdx, "resume only after popup listener detaches");
assert.ok(leaseReleaseIdx > releaseIdx, "release pause ownership after output handoff");
const mainRestoreStart = bridgeSource.indexOf("async function restoreAttachedSessionOutputOnce");
const mainRestoreEnd = bridgeSource.indexOf("function restoreTerminalSessionOutput", mainRestoreStart);
const mainRestoreSource = bridgeSource.slice(mainRestoreStart, mainRestoreEnd);
assert.match(
mainRestoreSource,
/if \(result\?\.success\) \{[\s\S]*setDirectPaused\([\s\S]*restorePauseOwner,[\s\S]*false/,
);
});
test("in-process explicit close sends an exit event before dropping the output route", () => {
const source = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge.cjs"),
"utf8",
);
const start = source.indexOf("function closeSession(event, payload)");
const end = source.indexOf("function setSessionEncoding", start);
const closeSource = source.slice(start, end);
assert.ok(
closeSource.indexOf("fanoutSessionLifecycleEvent") < closeSource.indexOf("closeTerminalOutputSession"),
"notify attached renderers before removing their output route",
);
assert.match(source, /if \(session\.closed\) return;/, "transport callbacks suppress duplicate explicit-close exits");
const sshSource = require("node:fs").readFileSync(
path.join(__dirname, "sshBridge/startSession.cjs"),
"utf8",
);
assert.match(sshSource, /if \(liveSession\?\.closed\)/, "SSH close callback suppresses duplicate explicit-close exits");
});
test("late backend exits cannot tear down a replacement with the same session id", () => {
const bridgeSource = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge.cjs"),
"utf8",
);
const sshSource = require("node:fs").readFileSync(
path.join(__dirname, "sshBridge/startSession.cjs"),
"utf8",
);
const telnetSource = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge/telnetSession.cjs"),
"utf8",
);
const moshSource = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge/moshSession.cjs"),
"utf8",
);
const etSource = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge/etSession.cjs"),
"utf8",
);
assert.match(sshSource, /if \(liveSession === session\)/);
assert.match(bridgeSource, /if \(sessions\.get\(sessionId\) !== session\) return;/);
assert.match(bridgeSource, /serialExitFinalized \|\| sessions\.get\(sessionId\) !== session/);
assert.match(telnetSource, /sessions\.get\(sessionId\) !== activeSession/);
assert.match(moshSource, /sessions\.get\(sessionId\) !== session/);
assert.match(etSource, /sessions\.get\(sessionId\) !== session/);
assert.match(sshSource, /establishedOwnerSession && current !== establishedOwnerSession/);
assert.match(sshSource, /establishedOwnerSession \? \{ session: establishedOwnerSession \} : \{\}/);
});
test("in-process explicit close notifies a rebound popup and its home renderer", async () => {
const { bridge, fakeChannel } = loadTerminalBridgeWithMocks();
const sent = [];
const contents = new Map([7, 9].map((id) => [id, {
id,
isDestroyed: () => false,
send: (channel, payload) => sent.push({ id, channel, payload }),
}]));
const sessions = new Map([[
"session-close",
{ webContentsId: 7, proc: { kill() {} } },
]]);
bridge.init({
sessions,
electronModule: { webContents: { fromId: (id) => contents.get(id) } },
terminalOutputChannel: fakeChannel,
});
const registry = require("./terminalAttachRestore.cjs");
registry.registerAttachPopupAuthorization("close-grant", "session-close", 9);
assert.equal(bridge.registerHandlers != null, true);
const ipcMain = {
handlers: new Map(),
listeners: new Map(),
handle(channel, handler) { this.handlers.set(channel, handler); },
on(channel, handler) { this.listeners.set(channel, handler); },
};
bridge.registerHandlers(ipcMain);
assert.equal(
(await ipcMain.handlers.get("netcatty:terminal:rebindOutput")(
{ sender: contents.get(9) },
{ sessionId: "session-close", authorization: "close-grant" },
)).success,
true,
);
bridge.closeSession({ sender: contents.get(7) }, { sessionId: "session-close" });
assert.deepEqual(sent, [
{ id: 9, channel: "netcatty:exit", payload: { sessionId: "session-close", exitCode: 0, reason: "closed" } },
{ id: 7, channel: "netcatty:exit", payload: { sessionId: "session-close", exitCode: 0, reason: "closed" } },
]);
registry.releaseAttachPopupAuthorization("close-grant");
});
test("snapshot apply acknowledgements are emitted only by the matching terminal", () => {
const preloadSource = require("node:fs").readFileSync(
path.join(__dirname, "../preload/api.cjs"),
"utf8",
);
const terminalSource = require("node:fs").readFileSync(
path.join(__dirname, "../../components/Terminal.tsx"),
"utf8",
);
const bridgeSource = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge.cjs"),
"utf8",
);
const effectsSource = require("node:fs").readFileSync(
path.join(__dirname, "../../components/terminal/useTerminalEffects.ts"),
"utf8",
);
assert.match(preloadSource, /if \(handled !== true\) return/);
assert.match(terminalSource, /typeof payload\.contextViewportSnapshot !== "string"/);
assert.doesNotMatch(terminalSource, /if \(snap\) \{\s*const applied = await terminalBackend\.applySessionSnapshot/);
assert.match(terminalSource, /const applied = await terminalBackend\.applySessionSnapshot/);
assert.match(bridgeSource, /const hasSnapshot = typeof payload\?\.snapshot === "string"/);
assert.match(bridgeSource, /const hasContextSnapshot = typeof payload\?\.contextSnapshot === "string"/);
assert.match(preloadSource, /contextSnapshot: typeof context\?\.contextSnapshot === "string"/);
assert.match(terminalSource, /finalContext = readTerminalHibernateContext\(snapshotTerm\)/);
assert.match(bridgeSource, /kittyKeyboardModeState: normalizeKittyKeyboardModeState/);
assert.match(preloadSource, /sanitizeKittyKeyboardModeState\(kittyKeyboardModeState\)/);
assert.match(terminalSource, /getKittyKeyboardModeState\(\)/);
assert.match(terminalSource, /restoreKittyKeyboardModeState/);
assert.match(effectsSource, /runtime\.restoreKittyKeyboardModeState\(snap\.kittyKeyboardModeState\)/);
assert.match(bridgeSource, /kittyKeyboardProtocolEnabled: typeof payload\?\.kittyKeyboardProtocolEnabled/);
assert.match(preloadSource, /typeof kittyKeyboardProtocolEnabled === "boolean"/);
assert.match(terminalSource, /getKittyKeyboardProtocolEnabled\(\)/);
assert.match(terminalSource, /\?\? kittyKeyboardProtocolEnabledForSession/);
assert.match(terminalSource, /setKittyKeyboardProtocolEnabled/);
assert.match(effectsSource, /runtime\.setKittyKeyboardProtocolEnabled\(snap\.kittyKeyboardProtocolEnabled\)/);
assert.match(terminalSource, /passwordPromptActive: passwordPromptActiveRef\.current/u);
assert.match(preloadSource, /typeof passwordPromptActive === "boolean"/u);
assert.match(bridgeSource, /passwordPromptActive: typeof payload\?\.passwordPromptActive/u);
assert.match(effectsSource, /passwordPromptActiveRef\.current = snap\.passwordPromptActive/u);
});
test("exit fanout preserves the original renderer before registry wiring", () => {
const attachRestore = require("./terminalAttachRestore.cjs");
attachRestore.setFanoutSessionExit(null);
const sent = [];
const contents = {
id: 42,
send(channel, payload) {
sent.push({ channel, payload });
},
};
const payload = { sessionId: "session-1", reason: "exited" };
assert.equal(attachRestore.fanoutSessionExit("session-1", contents, payload), true);
assert.deepEqual(sent, [{ channel: "netcatty:exit", payload }]);
});
test("attach authorization is bound to one session and renderer", () => {
const registry = require("./terminalAttachRestore.cjs");
registry.registerAttachPopupAuthorization("grant-1", "session-1", 42);
assert.equal(registry.validateAttachPopupAuthorization("grant-1", "session-1", 42), true);
assert.equal(registry.validateAttachPopupAuthorization("grant-1", "session-2", 42), false);
assert.equal(registry.validateAttachPopupAuthorization("grant-1", "session-1", 43), false);
assert.equal(registry.markAttachPopupClosePrepared("grant-1", "session-1", 42), true);
assert.equal(registry.isAttachPopupClosePrepared("grant-1"), true);
registry.releaseAttachPopupAuthorization("grant-1");
assert.equal(registry.validateAttachPopupAuthorization("grant-1", "session-1", 42), false);
});
test("failed attach restores retry when a main renderer becomes ready", async () => {
const registry = require("./terminalAttachRestore.cjs");
let available = false;
let calls = 0;
registry.setRestoreAttachedSessionOutput(() => {
calls += 1;
return available
? { success: true, restored: true }
: { success: false, restored: false, error: "Home renderer unavailable" };
});
assert.equal((await registry.restoreAttachedSessionOutput("session-retry")).success, false);
available = true;
await registry.retryPendingAttachedSessionOutputs();
await registry.retryPendingAttachedSessionOutputs();
assert.equal(calls, 2, "successful retry clears the pending restore");
});
test("closing a terminal clears failed restore and popup authorization state", async () => {
const registry = require("./terminalAttachRestore.cjs");
let calls = 0;
registry.setRestoreAttachedSessionOutput(() => {
calls += 1;
return { success: false, restored: false };
});
registry.registerAttachPopupAuthorization("closed-grant", "closed-session", 42);
await registry.restoreAttachedSessionOutput("closed-session");
registry.releaseAttachedSessionState("closed-session");
const result = await registry.retryPendingAttachedSessionOutput("closed-session", 42);
assert.equal(calls, 1);
assert.deepEqual(result, { success: true, restored: false });
assert.equal(
registry.validateAttachPopupAuthorization("closed-grant", "closed-session", 42),
false,
);
registry.setRestoreAttachedSessionOutput(null);
});
test("a ready replacement main renderer becomes the explicit restore target", async () => {
const registry = require("./terminalAttachRestore.cjs");
const targets = [];
registry.setRestoreAttachedSessionOutput((_sessionId, preferredHomeWebContentsId) => {
targets.push(preferredHomeWebContentsId);
return targets.length === 1
? { success: false, restored: false }
: { success: true, restored: true };
});
await registry.restoreAttachedSessionOutput("session-replacement");
await registry.retryPendingAttachedSessionOutput("session-replacement", 99);
assert.deepEqual(targets, [null, 99]);
});
test("attach IPC handlers validate popup authorization and flow pause has an awaitable barrier", () => {
const source = require("node:fs").readFileSync(
path.join(__dirname, "terminalBridge.cjs"),
"utf8",
);
assert.match(source, /function isAuthorizedAttachIpc/);
for (const functionName of [
"requestTerminalSessionSnapshot",
"applyTerminalSessionSnapshot",
"rebindTerminalSessionOutput",
"restoreTerminalSessionOutput",
]) {
const start = source.indexOf(`function ${functionName}`);
const end = source.indexOf("\nfunction ", start + 10);
assert.match(source.slice(start, end), /isAuthorizedAttachIpc/);
}
assert.match(source, /netcatty:terminal:setFlowPausedAndWait/);
assert.match(source, /terminalWorkerManager\.request\("netcatty:terminal:setFlowPausedAndWait"/);
});
test("terminal worker manager exposes rebindOutputSession", () => {
const source = require("node:fs").readFileSync(
path.join(__dirname, "terminalWorkerManager.cjs"),
"utf8",
);
assert.match(source, /function rebindOutputSession/);
assert.match(source, /rebindOutputSession,/);
assert.match(source, /getSessionWebContentsId\(sessionId\)/);
});
test("attach popup payload field is consumed by terminal popup window", () => {
const source = require("node:fs").readFileSync(
path.join(__dirname, "windowManager/terminalPopupWindow.cjs"),
"utf8",
);
assert.match(source, /attachSessionId/);
assert.match(source, /attachSessionPopups/);
assert.match(source, /reused: true/);
});
test("attach snapshots preserve cwd and title updates including explicit clears", () => {
const terminalSource = require("node:fs").readFileSync(
path.join(__dirname, "../../components/Terminal.tsx"),
"utf8",
);
const effectsSource = require("node:fs").readFileSync(
path.join(__dirname, "../../components/terminal/useTerminalEffects.ts"),
"utf8",
);
assert.match(
terminalSource,
/knownCwdRef\.current \?\? null,[\s\S]*?terminalTitleRef\.current \?\? null/,
);
assert.match(
effectsSource,
/snap\.cwd !== undefined[\s\S]*?setRendererCwd\(snap\.cwd,\s*"snapshot"\)[\s\S]*?snap\.title !== undefined/,
);
assert.match(
effectsSource,
/onTitleChange: \(title: string \| null\) => \{[\s\S]*?terminalTitleRef\.current = title \|\| undefined/,
);
assert.match(
terminalSource,
/payload\.cwd !== undefined[\s\S]*?setRendererCwd\(payload\.cwd,\s*"snapshot"\)[\s\S]*?payload\.title !== undefined/,
);
});