diff --git a/src/infra/warning-filter.test.ts b/src/infra/warning-filter.test.ts index 8220037d95..bd3ff1247c 100644 --- a/src/infra/warning-filter.test.ts +++ b/src/infra/warning-filter.test.ts @@ -2,12 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { installProcessWarningFilter, shouldIgnoreWarning } from "./warning-filter.js"; const warningFilterKey = Symbol.for("openclaw.warning-filter"); +const baseEmitWarning = process.emitWarning.bind(process) as typeof process.emitWarning; function resetWarningFilterInstallState(): void { const globalState = globalThis as typeof globalThis & { [warningFilterKey]?: { installed: boolean }; }; delete globalState[warningFilterKey]; + process.emitWarning = baseEmitWarning; } describe("warning filter", () => { @@ -53,37 +55,29 @@ describe("warning filter", () => { ).toBe(false); }); - it("installs once and only writes unsuppressed warnings", () => { - let warningHandler: ((warning: Error & { code?: string; message?: string }) => void) | null = - null; - const onSpy = vi.spyOn(process, "on").mockImplementation(((event, handler) => { - if (event === "warning") { - warningHandler = handler as (warning: Error & { code?: string; message?: string }) => void; - } - return process; - }) as typeof process.on); + it("installs once and suppresses known warnings at emit time", async () => { const writeSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); installProcessWarningFilter(); installProcessWarningFilter(); + installProcessWarningFilter(); + const emitWarning = (...args: unknown[]) => + (process.emitWarning as unknown as (...warningArgs: unknown[]) => void)(...args); - expect(onSpy).toHaveBeenCalledTimes(1); - expect(warningHandler).not.toBeNull(); - - warningHandler?.({ - name: "DeprecationWarning", + emitWarning( + "The `util._extend` API is deprecated. Please use Object.assign() instead.", + "DeprecationWarning", + "DEP0060", + ); + emitWarning("The `util._extend` API is deprecated. Please use Object.assign() instead.", { + type: "DeprecationWarning", code: "DEP0060", - message: "The `util._extend` API is deprecated.", - toString: () => "suppressed", - } as Error & { code?: string; message?: string }); + }); + await new Promise((resolve) => setImmediate(resolve)); expect(writeSpy).not.toHaveBeenCalled(); - warningHandler?.({ - name: "Warning", - message: "Visible warning", - stack: "Warning: visible", - toString: () => "visible", - } as Error & { code?: string; message?: string }); - expect(writeSpy).toHaveBeenCalledWith("Warning: visible\n"); + emitWarning("Visible warning", { type: "Warning", code: "OPENCLAW_TEST_WARNING" }); + await new Promise((resolve) => setImmediate(resolve)); + expect(writeSpy).toHaveBeenCalled(); }); }); diff --git a/src/infra/warning-filter.ts b/src/infra/warning-filter.ts index 68af7ad5d3..4086322288 100644 --- a/src/infra/warning-filter.ts +++ b/src/infra/warning-filter.ts @@ -1,11 +1,15 @@ const warningFilterKey = Symbol.for("openclaw.warning-filter"); -export type ProcessWarning = Error & { +export type ProcessWarning = { code?: string; name?: string; message?: string; }; +type ProcessWarningInstallState = { + installed: boolean; +}; + export function shouldIgnoreWarning(warning: ProcessWarning): boolean { if (warning.code === "DEP0040" && warning.message?.includes("punycode")) { return true; @@ -22,19 +26,60 @@ export function shouldIgnoreWarning(warning: ProcessWarning): boolean { return false; } +function normalizeWarningArgs(args: unknown[]): ProcessWarning { + const warningArg = args[0]; + const secondArg = args[1]; + const thirdArg = args[2]; + let name: string | undefined; + let code: string | undefined; + let message: string | undefined; + + if (warningArg instanceof Error) { + name = warningArg.name; + message = warningArg.message; + code = (warningArg as Error & { code?: string }).code; + } else if (typeof warningArg === "string") { + message = warningArg; + } + + if (secondArg && typeof secondArg === "object" && !Array.isArray(secondArg)) { + const options = secondArg as { type?: unknown; code?: unknown }; + if (typeof options.type === "string") { + name = options.type; + } + if (typeof options.code === "string") { + code = options.code; + } + } else { + if (typeof secondArg === "string") { + name = secondArg; + } + if (typeof thirdArg === "string") { + code = thirdArg; + } + } + + return { name, code, message }; +} + export function installProcessWarningFilter(): void { const globalState = globalThis as typeof globalThis & { - [warningFilterKey]?: { installed: boolean }; + [warningFilterKey]?: ProcessWarningInstallState; }; if (globalState[warningFilterKey]?.installed) { return; } - globalState[warningFilterKey] = { installed: true }; - process.on("warning", (warning: ProcessWarning) => { - if (shouldIgnoreWarning(warning)) { + const originalEmitWarning = process.emitWarning.bind(process); + const wrappedEmitWarning: typeof process.emitWarning = ((...args: unknown[]) => { + if (shouldIgnoreWarning(normalizeWarningArgs(args))) { return; } - process.stderr.write(`${warning.stack ?? warning.toString()}\n`); - }); + return Reflect.apply(originalEmitWarning, process, args); + }) as typeof process.emitWarning; + + process.emitWarning = wrappedEmitWarning; + globalState[warningFilterKey] = { + installed: true, + }; }