2026-07-24 04:52:23 -05:00
|
|
|
// SPDX-License-Identifier: 0BSD
|
|
|
|
|
|
|
|
|
|
import { createRequire } from "module";
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
|
const {
|
|
|
|
|
isBrokenPipeError,
|
|
|
|
|
installBrokenPipeGuards,
|
|
|
|
|
safeConsoleLog,
|
2026-07-24 04:58:09 -05:00
|
|
|
shouldMirrorStdout,
|
|
|
|
|
createMainProcessLogger,
|
|
|
|
|
ELECTRON_LOG_PREFIX,
|
2026-07-24 04:52:23 -05:00
|
|
|
} = require("../../electron/safeConsole");
|
|
|
|
|
|
|
|
|
|
describe("safeConsole", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("detects EPIPE by code and message", () => {
|
|
|
|
|
expect(isBrokenPipeError({ code: "EPIPE" })).toBe(true);
|
|
|
|
|
expect(isBrokenPipeError({ errno: "EPIPE" })).toBe(true);
|
|
|
|
|
expect(isBrokenPipeError(new Error("write EPIPE"))).toBe(true);
|
|
|
|
|
expect(isBrokenPipeError(new Error("other"))).toBe(false);
|
|
|
|
|
expect(isBrokenPipeError(null)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("installBrokenPipeGuards ignores EPIPE stream errors", () => {
|
|
|
|
|
const handlers = [];
|
|
|
|
|
const stream = {
|
|
|
|
|
on(event, handler) {
|
|
|
|
|
handlers.push([event, handler]);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
installBrokenPipeGuards({ stdout: stream, stderr: null });
|
|
|
|
|
expect(handlers).toHaveLength(1);
|
|
|
|
|
expect(handlers[0][0]).toBe("error");
|
|
|
|
|
expect(() => handlers[0][1]({ code: "EPIPE" })).not.toThrow();
|
|
|
|
|
installBrokenPipeGuards({ stdout: stream, stderr: null });
|
|
|
|
|
expect(handlers).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("safeConsoleLog swallows EPIPE from console.log", () => {
|
|
|
|
|
const spy = vi.spyOn(console, "log").mockImplementation(() => {
|
|
|
|
|
const err = new Error("write EPIPE");
|
|
|
|
|
err.code = "EPIPE";
|
|
|
|
|
throw err;
|
|
|
|
|
});
|
|
|
|
|
expect(() => safeConsoleLog("backend line")).not.toThrow();
|
|
|
|
|
expect(spy).toHaveBeenCalledWith("backend line");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("safeConsoleLog rethrows non-EPIPE errors", () => {
|
|
|
|
|
vi.spyOn(console, "log").mockImplementation(() => {
|
|
|
|
|
throw new Error("boom");
|
|
|
|
|
});
|
|
|
|
|
expect(() => safeConsoleLog("x")).toThrow("boom");
|
|
|
|
|
});
|
2026-07-24 04:58:09 -05:00
|
|
|
|
|
|
|
|
it("shouldMirrorStdout is false without a TTY unless forced", () => {
|
|
|
|
|
expect(shouldMirrorStdout({ stdout: { isTTY: false } }, {})).toBe(false);
|
|
|
|
|
expect(shouldMirrorStdout({ stdout: { isTTY: true } }, {})).toBe(true);
|
2026-07-24 05:12:19 -05:00
|
|
|
expect(shouldMirrorStdout({ stdout: { isTTY: false } }, { MESHCHAT_FORCE_STDOUT_LOG: "1" })).toBe(true);
|
|
|
|
|
expect(shouldMirrorStdout({ stdout: { isTTY: true } }, { MESHCHAT_DISABLE_STDOUT_LOG: "1" })).toBe(false);
|
2026-07-24 04:58:09 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("createMainProcessLogger appends durable lines and skips non-TTY stdout", () => {
|
|
|
|
|
const writes = [];
|
|
|
|
|
const mkdirCalls = [];
|
|
|
|
|
const logger = createMainProcessLogger({
|
|
|
|
|
getLogsDir: () => "/tmp/meshchat-logs",
|
|
|
|
|
mkdirSync: (dir, opts) => {
|
|
|
|
|
mkdirCalls.push([dir, opts]);
|
|
|
|
|
},
|
|
|
|
|
appendFileSync: (file, data) => {
|
|
|
|
|
writes.push([file, data]);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
|
|
|
|
|
|
|
|
logger.write("backend ready", {
|
|
|
|
|
proc: { stdout: { isTTY: false } },
|
|
|
|
|
env: {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(consoleSpy).not.toHaveBeenCalled();
|
|
|
|
|
expect(mkdirCalls.length).toBeGreaterThan(0);
|
|
|
|
|
expect(writes).toHaveLength(1);
|
|
|
|
|
expect(writes[0][0]).toBe("/tmp/meshchat-logs/meshchatx.log");
|
|
|
|
|
expect(writes[0][1]).toContain(ELECTRON_LOG_PREFIX);
|
|
|
|
|
expect(writes[0][1]).toContain("backend ready");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("createMainProcessLogger mirrors to stdout when TTY is present", () => {
|
|
|
|
|
const logger = createMainProcessLogger({
|
|
|
|
|
getLogsDir: () => "/tmp/meshchat-logs",
|
|
|
|
|
mkdirSync: () => {},
|
|
|
|
|
appendFileSync: () => {},
|
|
|
|
|
});
|
|
|
|
|
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
|
|
|
logger.write("tty line", {
|
|
|
|
|
proc: { stdout: { isTTY: true } },
|
|
|
|
|
env: {},
|
|
|
|
|
});
|
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith("tty line");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("createMainProcessLogger buffers until logs dir is available", () => {
|
|
|
|
|
let logsDir = null;
|
|
|
|
|
const writes = [];
|
|
|
|
|
const logger = createMainProcessLogger({
|
|
|
|
|
getLogsDir: () => logsDir,
|
|
|
|
|
mkdirSync: () => {},
|
|
|
|
|
appendFileSync: (file, data) => {
|
|
|
|
|
writes.push([file, data]);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
logger.write("early", { mirrorStdout: false });
|
|
|
|
|
expect(writes).toHaveLength(0);
|
|
|
|
|
expect(logger._pendingForTests.length).toBe(1);
|
|
|
|
|
|
|
|
|
|
logsDir = "/tmp/meshchat-logs";
|
|
|
|
|
logger.write("later", { mirrorStdout: false });
|
|
|
|
|
expect(writes.length).toBeGreaterThanOrEqual(1);
|
|
|
|
|
const joined = writes.map((entry) => entry[1]).join("");
|
|
|
|
|
expect(joined).toContain("early");
|
|
|
|
|
expect(joined).toContain("later");
|
|
|
|
|
expect(logger._pendingForTests.length).toBe(0);
|
|
|
|
|
});
|
2026-07-24 04:52:23 -05:00
|
|
|
});
|