Logging: tolerate EIO console writes

This commit is contained in:
George Pickett
2026-01-14 17:55:06 -08:00
committed by Peter Steinberger
parent ed68f378d7
commit 8f797f213e
3 changed files with 88 additions and 1 deletions

View File

@@ -13,6 +13,7 @@
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
- Logging: tolerate `EIO` from console writes to avoid gateway crashes. (#925, fixes #878) — thanks @grp06.
- Sandbox: restore `docker.binds` config validation for custom bind mounts. (#873) — thanks @akonyer.
- Sandbox: preserve configured PATH for `docker exec` so custom tools remain available. (#873) — thanks @akonyer.
- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr.

View File

@@ -0,0 +1,85 @@
import crypto from "node:crypto";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
enableConsoleCapture,
resetLogger,
routeLogsToStderr,
setLoggerOverride,
} from "../logging.js";
import { loggingState } from "./state.js";
type ConsoleSnapshot = {
log: typeof console.log;
info: typeof console.info;
warn: typeof console.warn;
error: typeof console.error;
debug: typeof console.debug;
trace: typeof console.trace;
};
let snapshot: ConsoleSnapshot;
beforeEach(() => {
snapshot = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug,
trace: console.trace,
};
loggingState.consolePatched = false;
loggingState.forceConsoleToStderr = false;
loggingState.rawConsole = null;
resetLogger();
});
afterEach(() => {
console.log = snapshot.log;
console.info = snapshot.info;
console.warn = snapshot.warn;
console.error = snapshot.error;
console.debug = snapshot.debug;
console.trace = snapshot.trace;
loggingState.consolePatched = false;
loggingState.forceConsoleToStderr = false;
loggingState.rawConsole = null;
resetLogger();
setLoggerOverride(null);
vi.restoreAllMocks();
});
describe("enableConsoleCapture", () => {
it("swallows EIO from stderr writes", () => {
setLoggerOverride({ level: "info", file: tempLogPath() });
vi.spyOn(process.stderr, "write").mockImplementation(() => {
throw eioError();
});
routeLogsToStderr();
enableConsoleCapture();
expect(() => console.log("hello")).not.toThrow();
});
it("swallows EIO from original console writes", () => {
setLoggerOverride({ level: "info", file: tempLogPath() });
console.log = () => {
throw eioError();
};
enableConsoleCapture();
expect(() => console.log("hello")).not.toThrow();
});
});
function tempLogPath() {
return path.join(os.tmpdir(), `clawdbot-log-${crypto.randomUUID()}.log`);
}
function eioError() {
const err = new Error("EIO") as NodeJS.ErrnoException;
err.code = "EIO";
return err;
}

View File

@@ -89,7 +89,8 @@ function shouldSuppressConsoleMessage(message: string): boolean {
}
function isEpipeError(err: unknown): boolean {
return Boolean((err as { code?: string })?.code === "EPIPE");
const code = (err as { code?: string })?.code;
return code === "EPIPE" || code === "EIO";
}
/**