Files
openclaw/src/infra/errors.test.ts
Chinar Amrutkar 3f67581e50 fix: retry safe wrapped Telegram send failures (#51895) (thanks @chinar-amrutkar)
* fix(telegram): traverse error .cause chain in formatErrorMessage and match grammY HttpError

grammY wraps network failures in HttpError with message
'Network request for ... failed!' and the original error in .cause.
formatErrorMessage only checked err.message, so shouldRetry never
fired for the most common transient failure class.

Changes:
- formatErrorMessage now traverses .cause chain, appending nested
  error messages (with cycle protection)
- Added 'Network request' to TELEGRAM_RETRY_RE as belt-and-suspenders
- Added tests for .cause traversal, circular references, and grammY
  HttpError retry behavior

Fixes #51525

* style: fix oxfmt formatting in retry-policy.ts

* fix: add braces to satisfy oxlint requirement

* fix(telegram): keep send retries strict

* test(telegram): cover wrapped retry paths

* fix(telegram): retry rate-limited sends safely

* fix: retry safe wrapped Telegram send failures (#51895) (thanks @chinar-amrutkar)

* fix: preserve wrapped Telegram rate-limit retries (#51895) (thanks @chinar-amrutkar)

---------

Co-authored-by: chinar-amrutkar <chinar-amrutkar@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-01 15:54:29 +05:30

111 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
collectErrorGraphCandidates,
extractErrorCode,
formatErrorMessage,
formatUncaughtError,
hasErrnoCode,
isErrno,
readErrorName,
} from "./errors.js";
function createCircularObject() {
const circular: { self?: unknown } = {};
circular.self = circular;
return circular;
}
describe("error helpers", () => {
it.each([
{ value: { code: "EADDRINUSE" }, expected: "EADDRINUSE" },
{ value: { code: 429 }, expected: "429" },
{ value: { code: false }, expected: undefined },
{ value: "boom", expected: undefined },
])("extracts error codes from %j", ({ value, expected }) => {
expect(extractErrorCode(value)).toBe(expected);
});
it.each([
{ value: { name: "AbortError" }, expected: "AbortError" },
{ value: { name: 42 }, expected: "" },
{ value: null, expected: "" },
])("reads error names from %j", ({ value, expected }) => {
expect(readErrorName(value)).toBe(expected);
});
it("walks nested error graphs once in breadth-first order", () => {
const leaf = { name: "leaf" };
const child = { name: "child" } as {
name: string;
cause?: unknown;
errors?: unknown[];
};
const root = { name: "root", cause: child, errors: [leaf, child] };
child.cause = root;
expect(
collectErrorGraphCandidates(root, (current) => [
current.cause,
...((current as { errors?: unknown[] }).errors ?? []),
]),
).toEqual([root, child, leaf]);
expect(collectErrorGraphCandidates(null)).toEqual([]);
});
it("matches errno-shaped errors by code", () => {
const err = Object.assign(new Error("busy"), { code: "EADDRINUSE" });
expect(isErrno(err)).toBe(true);
expect(hasErrnoCode(err, "EADDRINUSE")).toBe(true);
expect(hasErrnoCode(err, "ENOENT")).toBe(false);
expect(isErrno("busy")).toBe(false);
});
it.each([
{ value: 123n, expected: "123" },
{ value: false, expected: "false" },
{ value: createCircularObject(), expected: "[object Object]" },
])("formats error messages for case %#", ({ value, expected }) => {
expect(formatErrorMessage(value)).toBe(expected);
});
it("traverses .cause chain to include nested error messages", () => {
const rootCause = new Error("ECONNRESET");
const httpError = Object.assign(new Error("Network request for 'sendMessage' failed!"), {
cause: rootCause,
});
const formatted = formatErrorMessage(httpError);
expect(formatted).toContain("Network request for 'sendMessage' failed!");
expect(formatted).toContain("ECONNRESET");
});
it("handles circular .cause references without infinite loop", () => {
const a: Error & { cause?: unknown } = new Error("error A");
const b: Error & { cause?: unknown } = new Error("error B");
a.cause = b;
b.cause = a;
const formatted = formatErrorMessage(a);
expect(formatted).toBe("error A | error B");
});
it("redacts sensitive tokens from formatted error messages", () => {
const token = "sk-abcdefghijklmnopqrstuv";
const formatted = formatErrorMessage(new Error(`Authorization: Bearer ${token}`));
expect(formatted).toContain("Authorization: Bearer");
expect(formatted).not.toContain(token);
});
it("uses message-only formatting for INVALID_CONFIG and stack formatting otherwise", () => {
const invalidConfig = Object.assign(new Error("TOKEN=sk-abcdefghijklmnopqrstuv"), {
code: "INVALID_CONFIG",
stack: "Error: TOKEN=sk-abcdefghijklmnopqrstuv\n at ignored",
});
expect(formatUncaughtError(invalidConfig)).not.toContain("at ignored");
const uncaught = new Error("boom");
uncaught.stack = "Error: Authorization: Bearer sk-abcdefghijklmnopqrstuv\n at runTask";
const formatted = formatUncaughtError(uncaught);
expect(formatted).toContain("at runTask");
expect(formatted).not.toContain("sk-abcdefghijklmnopqrstuv");
});
});