[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:
253
electron/bridges/mainProcessErrorGuards.test.cjs
Normal file
253
electron/bridges/mainProcessErrorGuards.test.cjs
Normal file
@@ -0,0 +1,253 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { EventEmitter } = require("node:events");
|
||||
const {
|
||||
classifyProcessError,
|
||||
createProcessErrorController,
|
||||
installProcessErrorHandlers,
|
||||
isNonFatalNetworkError,
|
||||
} = require("./processErrorGuards.cjs");
|
||||
|
||||
test("treats Chromium ERR_NETWORK_CHANGED as non-fatal", () => {
|
||||
assert.equal(
|
||||
isNonFatalNetworkError(new Error("net::ERR_NETWORK_CHANGED")),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("treats other Chromium net::ERR_* failures as non-fatal network errors", () => {
|
||||
assert.equal(
|
||||
isNonFatalNetworkError(new Error("net::ERR_INTERNET_DISCONNECTED")),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isNonFatalNetworkError(new Error("net::ERR_NAME_NOT_RESOLVED")),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("treats Node socket error codes as non-fatal network errors", () => {
|
||||
const err = new Error("socket reset");
|
||||
err.code = "ECONNRESET";
|
||||
assert.equal(isNonFatalNetworkError(err), true);
|
||||
|
||||
const dnsErr = new Error("dns failed");
|
||||
dnsErr.code = "ENOTFOUND";
|
||||
assert.equal(isNonFatalNetworkError(dnsErr), true);
|
||||
});
|
||||
|
||||
test("keeps non-network errors fatal", () => {
|
||||
assert.equal(
|
||||
isNonFatalNetworkError(new Error("Something else broke")),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("generic startup exceptions stay fatal before the app is up", () => {
|
||||
const result = classifyProcessError(new Error("boom"), {
|
||||
runtimeStarted: false,
|
||||
});
|
||||
|
||||
assert.equal(result.action, "fatal");
|
||||
});
|
||||
|
||||
test("generic runtime exceptions are suppressed after startup", () => {
|
||||
const result = classifyProcessError(new Error("boom"), {
|
||||
runtimeStarted: true,
|
||||
});
|
||||
|
||||
assert.equal(result.action, "suppress");
|
||||
assert.match(result.reason, /runtime/i);
|
||||
});
|
||||
|
||||
test("generic runtime promise rejections are also suppressed after startup", () => {
|
||||
const result = classifyProcessError(new Error("promise boom"), {
|
||||
runtimeStarted: true,
|
||||
origin: "unhandledRejection",
|
||||
});
|
||||
|
||||
assert.equal(result.action, "suppress");
|
||||
assert.match(result.reason, /runtime/i);
|
||||
});
|
||||
|
||||
test("controller keeps startup strict until the main window is actually shown", () => {
|
||||
const controller = createProcessErrorController();
|
||||
|
||||
controller.beginMainWindowStartup();
|
||||
assert.equal(controller.isRuntimeProtectionActive(), false);
|
||||
|
||||
controller.completeMainWindowStartup({ windowShown: true });
|
||||
assert.equal(controller.isRuntimeProtectionActive(), true);
|
||||
});
|
||||
|
||||
test("controller becomes strict again while recreating a missing main window", () => {
|
||||
const controller = createProcessErrorController();
|
||||
|
||||
controller.beginMainWindowStartup();
|
||||
controller.completeMainWindowStartup({ windowShown: true });
|
||||
assert.equal(controller.isRuntimeProtectionActive(), true);
|
||||
|
||||
controller.beginMainWindowStartup();
|
||||
assert.equal(controller.isRuntimeProtectionActive(), false);
|
||||
|
||||
controller.completeMainWindowStartup({ windowShown: false });
|
||||
assert.equal(controller.isRuntimeProtectionActive(), true);
|
||||
});
|
||||
|
||||
test("startup-period errors stay fatal while recreating the main window", () => {
|
||||
const fakeProcess = new EventEmitter();
|
||||
const fatals = [];
|
||||
const controller = createProcessErrorController({
|
||||
captureError() {},
|
||||
onFatalError(err) {
|
||||
fatals.push(err.message);
|
||||
throw err;
|
||||
},
|
||||
logError() {},
|
||||
logWarn() {},
|
||||
});
|
||||
|
||||
installProcessErrorHandlers(fakeProcess, controller);
|
||||
controller.completeMainWindowStartup({ windowShown: true });
|
||||
controller.beginMainWindowStartup();
|
||||
|
||||
assert.throws(() => {
|
||||
fakeProcess.emit("uncaughtException", new Error("recreate boom"));
|
||||
}, /recreate boom/);
|
||||
assert.deepEqual(fatals, ["recreate boom"]);
|
||||
});
|
||||
|
||||
test("fatal startup failures uninstall listeners and keep throwing", () => {
|
||||
const fakeProcess = new EventEmitter();
|
||||
const captured = [];
|
||||
const fatals = [];
|
||||
let uninstall = null;
|
||||
const controller = createProcessErrorController({
|
||||
captureError(source, err) {
|
||||
captured.push([source, err.message]);
|
||||
},
|
||||
onFatalError(err) {
|
||||
fatals.push(err.message);
|
||||
uninstall?.();
|
||||
throw err;
|
||||
},
|
||||
logError() {},
|
||||
logWarn() {},
|
||||
});
|
||||
|
||||
uninstall = installProcessErrorHandlers(fakeProcess, controller);
|
||||
|
||||
assert.throws(() => {
|
||||
fakeProcess.emit("uncaughtException", new Error("startup boom"));
|
||||
}, /startup boom/);
|
||||
assert.deepEqual(fatals, ["startup boom"]);
|
||||
assert.deepEqual(captured, [["uncaughtException", "startup boom"]]);
|
||||
assert.equal(fakeProcess.listenerCount("uncaughtException"), 0);
|
||||
assert.equal(fakeProcess.listenerCount("unhandledRejection"), 0);
|
||||
});
|
||||
|
||||
test("installed handlers suppress runtime failures after startup", () => {
|
||||
const fakeProcess = new EventEmitter();
|
||||
const captured = [];
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const controller = createProcessErrorController({
|
||||
captureError(source, err) {
|
||||
captured.push([source, err.message]);
|
||||
},
|
||||
onFatalError(err) {
|
||||
throw err;
|
||||
},
|
||||
logError(...args) {
|
||||
errors.push(args.map(String).join(" "));
|
||||
},
|
||||
logWarn(...args) {
|
||||
warnings.push(args.map(String).join(" "));
|
||||
},
|
||||
});
|
||||
|
||||
installProcessErrorHandlers(fakeProcess, controller);
|
||||
|
||||
controller.beginMainWindowStartup();
|
||||
controller.completeMainWindowStartup({ windowShown: true });
|
||||
|
||||
fakeProcess.emit("uncaughtException", new Error("runtime boom"));
|
||||
fakeProcess.emit("unhandledRejection", new Error("runtime rejection"));
|
||||
assert.deepEqual(captured, [
|
||||
["uncaughtException", "runtime boom"],
|
||||
["unhandledRejection", "runtime rejection"],
|
||||
]);
|
||||
assert.equal(errors.some((line) => line.includes("runtime error after startup")), true);
|
||||
assert.equal(warnings.length, 0);
|
||||
});
|
||||
|
||||
test("unhandled rejection marks the forwarded error so uncaught follow-up is not double-captured", () => {
|
||||
const captured = [];
|
||||
const fatals = [];
|
||||
const controller = createProcessErrorController({
|
||||
captureError(source, err) {
|
||||
captured.push([source, err.message]);
|
||||
},
|
||||
onFatalError(err) {
|
||||
fatals.push(err);
|
||||
},
|
||||
logError() {},
|
||||
logWarn() {},
|
||||
});
|
||||
|
||||
controller.handleUnhandledRejection(new Error("startup rejection"));
|
||||
assert.equal(fatals.length, 1);
|
||||
assert.equal(fatals[0].__fromUnhandledRejection, true);
|
||||
assert.deepEqual(captured, [["unhandledRejection", "startup rejection"]]);
|
||||
|
||||
controller.handleUncaughtException(fatals[0]);
|
||||
assert.deepEqual(captured, [["unhandledRejection", "startup rejection"]]);
|
||||
});
|
||||
|
||||
test("benign stream teardown errors are ignored by the installed handlers", () => {
|
||||
const fakeProcess = new EventEmitter();
|
||||
let captureCount = 0;
|
||||
let fatalCount = 0;
|
||||
const controller = createProcessErrorController({
|
||||
captureError() {
|
||||
captureCount += 1;
|
||||
},
|
||||
onFatalError() {
|
||||
fatalCount += 1;
|
||||
},
|
||||
logError() {},
|
||||
logWarn() {},
|
||||
});
|
||||
|
||||
installProcessErrorHandlers(fakeProcess, controller);
|
||||
const err = new Error("broken pipe");
|
||||
err.code = "EPIPE";
|
||||
fakeProcess.emit("uncaughtException", err);
|
||||
|
||||
assert.equal(captureCount, 0);
|
||||
assert.equal(fatalCount, 0);
|
||||
});
|
||||
|
||||
test("controller suppresses wrapped network errors from err.cause", () => {
|
||||
const err = new Error("request failed");
|
||||
err.cause = new Error("net::ERR_NETWORK_CHANGED");
|
||||
|
||||
const result = classifyProcessError(err, {
|
||||
runtimeStarted: false,
|
||||
});
|
||||
|
||||
assert.equal(isNonFatalNetworkError(err), true);
|
||||
assert.equal(result.action, "suppress");
|
||||
});
|
||||
|
||||
test("controller suppresses ssh-style errors with a level property", () => {
|
||||
const err = new Error("connection lost before handshake");
|
||||
err.level = "client-socket";
|
||||
|
||||
const result = classifyProcessError(err, {
|
||||
runtimeStarted: false,
|
||||
});
|
||||
|
||||
assert.equal(isNonFatalNetworkError(err), true);
|
||||
assert.equal(result.action, "suppress");
|
||||
});
|
||||
Reference in New Issue
Block a user