diff --git a/src/cli/context.ts b/src/cli/context.ts index 8c9dac4..3b6f924 100644 --- a/src/cli/context.ts +++ b/src/cli/context.ts @@ -42,8 +42,33 @@ function effectiveWorkspaceUrl(flag?: string): string | undefined { return flag?.trim() || process.env.SLACK_WORKSPACE_URL?.trim() || undefined; } -function errorMessage(err: unknown): string { - return err instanceof Error ? err.message : String(err); +export function errorMessage(err: unknown): string { + if (!(err instanceof Error)) { + return String(err); + } + const { message: rootMessage, cause: rootCause } = err; + let message = rootMessage; + let cause: unknown = rootCause; + const seenCauses = new Set([err]); + while (cause !== undefined && cause !== null) { + if (seenCauses.has(cause)) { + break; + } + seenCauses.add(cause); + if (cause instanceof AggregateError && cause.errors.length > 0) { + message += `: ${cause.errors.map((e) => (e instanceof Error ? e.message : String(e))).join("; ")}`; + break; + } + if (cause instanceof Error) { + const { message: causeMessage, cause: nextCause } = cause; + message += `: ${causeMessage}`; + cause = nextCause; + continue; + } + message += `: ${String(cause)}`; + break; + } + return message; } function parseContentType(value: unknown): "any" | "text" | "image" | "snippet" | "file" { diff --git a/test/error-message.test.ts b/test/error-message.test.ts new file mode 100644 index 0000000..8509efe --- /dev/null +++ b/test/error-message.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "bun:test"; +import { errorMessage } from "../src/cli/context.ts"; + +describe("errorMessage", () => { + test("returns the message for a plain error", () => { + expect(errorMessage(new Error("boom"))).toBe("boom"); + }); + + test("stringifies non-Error values", () => { + expect(errorMessage("boom")).toBe("boom"); + }); + + test("appends a single Error cause", () => { + const err = new Error("fetch failed", { cause: new Error("connect ECONNREFUSED") }); + expect(errorMessage(err)).toBe("fetch failed: connect ECONNREFUSED"); + }); + + test("appends AggregateError causes from a failed fetch through a dead proxy", () => { + const cause = new AggregateError( + [ + new Error("connect ECONNREFUSED 127.0.0.1:9090"), + new Error("connect ECONNREFUSED ::1:9090"), + ], + "ECONNREFUSED", + ); + const err = new Error("fetch failed", { cause }); + expect(errorMessage(err)).toBe( + "fetch failed: connect ECONNREFUSED 127.0.0.1:9090; connect ECONNREFUSED ::1:9090", + ); + }); + + test("walks multi-level cause chains", () => { + const root = new Error("connect ECONNREFUSED"); + const middle = new Error("request failed", { cause: root }); + const err = new Error("fetch failed", { cause: middle }); + expect(errorMessage(err)).toBe("fetch failed: request failed: connect ECONNREFUSED"); + }); + + test("terminates on a self-referential cause instead of looping forever", () => { + const err = new Error("boom"); + err.cause = err; + expect(errorMessage(err)).toBe("boom"); + }); + + test("terminates on a longer cause cycle", () => { + const a = new Error("a"); + const b = new Error("b", { cause: a }); + a.cause = b; + expect(errorMessage(a)).toBe("a: b"); + }); +});