openclaw-vainplex/src/telegram/network-errors.test.ts
Christian Klotz 99b4f2a24e
fix(telegram): handle Grammy HttpError network failures (#3815) (#7195)
* fix(telegram): handle Grammy HttpError network failures (#3815)

Grammy wraps fetch errors in an .error property (not .cause). Added .error
traversal to collectErrorCandidates in network-errors.ts.

Registered scoped unhandled rejection handler in monitorTelegramProvider
to catch network errors that escape the polling loop (e.g., from setMyCommands
during bot setup). Handler is unregistered when the provider stops.

* fix(telegram): address review feedback for Grammy HttpError handling

- Gate .error traversal on HttpError name to avoid widening search graph
- Use runtime logger instead of console.warn for consistency
- Add isGrammyHttpError check to scope unhandled rejection handler
- Consolidate isNetworkRelatedError into isRecoverableTelegramNetworkError
- Add 'timeout' to recoverable message snippets for full coverage
2026-02-02 15:25:41 +00:00

81 lines
3.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { isRecoverableTelegramNetworkError } from "./network-errors.js";
describe("isRecoverableTelegramNetworkError", () => {
it("detects recoverable error codes", () => {
const err = Object.assign(new Error("timeout"), { code: "ETIMEDOUT" });
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
});
it("detects additional recoverable error codes", () => {
const aborted = Object.assign(new Error("aborted"), { code: "ECONNABORTED" });
const network = Object.assign(new Error("network"), { code: "ERR_NETWORK" });
expect(isRecoverableTelegramNetworkError(aborted)).toBe(true);
expect(isRecoverableTelegramNetworkError(network)).toBe(true);
});
it("detects AbortError names", () => {
const err = Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
});
it("detects nested causes", () => {
const cause = Object.assign(new Error("socket hang up"), { code: "ECONNRESET" });
const err = Object.assign(new TypeError("fetch failed"), { cause });
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
});
it("detects expanded message patterns", () => {
expect(isRecoverableTelegramNetworkError(new Error("TypeError: fetch failed"))).toBe(true);
expect(isRecoverableTelegramNetworkError(new Error("Undici: socket failure"))).toBe(true);
});
it("skips message matches for send context", () => {
const err = new TypeError("fetch failed");
expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(false);
expect(isRecoverableTelegramNetworkError(err, { context: "polling" })).toBe(true);
});
it("returns false for unrelated errors", () => {
expect(isRecoverableTelegramNetworkError(new Error("invalid token"))).toBe(false);
});
// Grammy HttpError tests (issue #3815)
// Grammy wraps fetch errors in .error property, not .cause
describe("Grammy HttpError", () => {
class MockHttpError extends Error {
constructor(
message: string,
public readonly error: unknown,
) {
super(message);
this.name = "HttpError";
}
}
it("detects network error wrapped in HttpError", () => {
const fetchError = new TypeError("fetch failed");
const httpError = new MockHttpError(
"Network request for 'setMyCommands' failed!",
fetchError,
);
expect(isRecoverableTelegramNetworkError(httpError)).toBe(true);
});
it("detects network error with cause wrapped in HttpError", () => {
const cause = Object.assign(new Error("socket hang up"), { code: "ECONNRESET" });
const fetchError = Object.assign(new TypeError("fetch failed"), { cause });
const httpError = new MockHttpError("Network request for 'getUpdates' failed!", fetchError);
expect(isRecoverableTelegramNetworkError(httpError)).toBe(true);
});
it("returns false for non-network errors wrapped in HttpError", () => {
const authError = new Error("Unauthorized: bot token is invalid");
const httpError = new MockHttpError("Bad Request: invalid token", authError);
expect(isRecoverableTelegramNetworkError(httpError)).toBe(false);
});
});
});