mirror of https://github.com/openclaw/openclaw.git
222 lines
8.5 KiB
TypeScript
222 lines
8.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
getTelegramNetworkErrorOrigin,
|
|
isRecoverableTelegramNetworkError,
|
|
isSafeToRetrySendError,
|
|
isTelegramClientRejection,
|
|
isTelegramPollingNetworkError,
|
|
isTelegramServerError,
|
|
tagTelegramNetworkError,
|
|
} from "./network-errors.js";
|
|
|
|
const errorWithCode = (message: string, code: string) =>
|
|
Object.assign(new Error(message), { code });
|
|
const errorWithTelegramCode = (message: string, error_code: number) =>
|
|
Object.assign(new Error(message), { error_code });
|
|
|
|
describe("isRecoverableTelegramNetworkError", () => {
|
|
it("tracks Telegram polling origin separately from generic network matching", () => {
|
|
const slackDnsError = Object.assign(
|
|
new Error("A request error occurred: getaddrinfo ENOTFOUND slack.com"),
|
|
{
|
|
code: "ENOTFOUND",
|
|
hostname: "slack.com",
|
|
},
|
|
);
|
|
expect(isRecoverableTelegramNetworkError(slackDnsError)).toBe(true);
|
|
expect(isTelegramPollingNetworkError(slackDnsError)).toBe(false);
|
|
|
|
tagTelegramNetworkError(slackDnsError, {
|
|
method: "getUpdates",
|
|
url: "https://api.telegram.org/bot123456:ABC/getUpdates",
|
|
});
|
|
expect(getTelegramNetworkErrorOrigin(slackDnsError)).toEqual({
|
|
method: "getupdates",
|
|
url: "https://api.telegram.org/bot123456:ABC/getUpdates",
|
|
});
|
|
expect(isTelegramPollingNetworkError(slackDnsError)).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
["ETIMEDOUT", "timeout"],
|
|
["ECONNABORTED", "aborted"],
|
|
["ERR_NETWORK", "network"],
|
|
])("detects recoverable error code %s", (code, message) => {
|
|
expect(isRecoverableTelegramNetworkError(errorWithCode(message, code))).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("treats undici fetch failed errors as recoverable in send context", () => {
|
|
const err = new TypeError("fetch failed");
|
|
expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(true);
|
|
expect(
|
|
isRecoverableTelegramNetworkError(new Error("TypeError: fetch failed"), { context: "send" }),
|
|
).toBe(true);
|
|
expect(isRecoverableTelegramNetworkError(err, { context: "polling" })).toBe(true);
|
|
});
|
|
|
|
it("honors allowMessageMatch=false for broad snippet matches", () => {
|
|
expect(
|
|
isRecoverableTelegramNetworkError(new Error("Undici: socket failure"), {
|
|
allowMessageMatch: false,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isRecoverableTelegramNetworkError(new Error("TypeError: fetch failed"), {
|
|
allowMessageMatch: false,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("skips broad message matches for send context", () => {
|
|
const networkRequestErr = new Error("Network request for 'sendMessage' failed!");
|
|
expect(isRecoverableTelegramNetworkError(networkRequestErr, { context: "send" })).toBe(false);
|
|
expect(isRecoverableTelegramNetworkError(networkRequestErr, { context: "polling" })).toBe(true);
|
|
|
|
const undiciSnippetErr = new Error("Undici: socket failure");
|
|
expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "send" })).toBe(false);
|
|
expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "polling" })).toBe(true);
|
|
});
|
|
|
|
it("treats grammY failed-after envelope errors as recoverable in send context", () => {
|
|
expect(
|
|
isRecoverableTelegramNetworkError(
|
|
new Error("Network request for 'sendMessage' failed after 2 attempts."),
|
|
{ context: "send" },
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false for unrelated errors", () => {
|
|
expect(isRecoverableTelegramNetworkError(new Error("invalid token"))).toBe(false);
|
|
});
|
|
|
|
it("detects grammY 'timed out' long-poll errors (#7239)", () => {
|
|
const err = new Error("Request to 'getUpdates' timed out after 500 seconds");
|
|
expect(isRecoverableTelegramNetworkError(err)).toBe(true);
|
|
});
|
|
|
|
it("normalizes blank tagged origins to null and finds nested tags", () => {
|
|
const inner = new Error("inner");
|
|
tagTelegramNetworkError(inner, { method: " ", url: " " });
|
|
const outer = Object.assign(new Error("outer"), { cause: inner });
|
|
expect(getTelegramNetworkErrorOrigin(outer)).toEqual({ method: null, url: null });
|
|
expect(isTelegramPollingNetworkError(outer)).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);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("isSafeToRetrySendError", () => {
|
|
it.each([
|
|
["ECONNREFUSED", "connect ECONNREFUSED", true],
|
|
["ENOTFOUND", "getaddrinfo ENOTFOUND", true],
|
|
["EAI_AGAIN", "getaddrinfo EAI_AGAIN", true],
|
|
["ENETUNREACH", "connect ENETUNREACH", true],
|
|
["EHOSTUNREACH", "connect EHOSTUNREACH", true],
|
|
["ECONNRESET", "read ECONNRESET", false],
|
|
["ETIMEDOUT", "connect ETIMEDOUT", false],
|
|
["EPIPE", "write EPIPE", false],
|
|
["UND_ERR_CONNECT_TIMEOUT", "connect timeout", false],
|
|
])("returns %s => %s", (code, message, expected) => {
|
|
expect(isSafeToRetrySendError(errorWithCode(message, code))).toBe(expected);
|
|
});
|
|
|
|
it("does NOT allow retry for non-network errors", () => {
|
|
expect(isSafeToRetrySendError(new Error("400: Bad Request"))).toBe(false);
|
|
expect(isSafeToRetrySendError(null)).toBe(false);
|
|
});
|
|
|
|
it("detects pre-connect error nested in cause chain", () => {
|
|
const root = Object.assign(new Error("ECONNREFUSED"), { code: "ECONNREFUSED" });
|
|
const wrapped = Object.assign(new Error("fetch failed"), { cause: root });
|
|
expect(isSafeToRetrySendError(wrapped)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("isTelegramServerError", () => {
|
|
it.each([
|
|
["Internal Server Error", 500, true],
|
|
["Bad Gateway", 502, true],
|
|
["Forbidden", 403, false],
|
|
])("returns %s for error_code %s", (message, errorCode, expected) => {
|
|
expect(isTelegramServerError(errorWithTelegramCode(message, errorCode))).toBe(expected);
|
|
});
|
|
|
|
it("returns false for plain Error", () => {
|
|
expect(isTelegramServerError(new Error("500: Internal Server Error"))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isTelegramClientRejection", () => {
|
|
it.each([
|
|
["Bad Request", 400, true],
|
|
["Forbidden", 403, true],
|
|
["Bad Gateway", 502, false],
|
|
])("returns %s for error_code %s", (message, errorCode, expected) => {
|
|
expect(isTelegramClientRejection(errorWithTelegramCode(message, errorCode))).toBe(expected);
|
|
});
|
|
|
|
it("returns false for plain Error", () => {
|
|
expect(isTelegramClientRejection(new Error("400: Bad Request"))).toBe(false);
|
|
});
|
|
|
|
it("detects error_code in nested cause", () => {
|
|
const inner = Object.assign(new Error("Forbidden"), { error_code: 403 });
|
|
const outer = Object.assign(new Error("wrapped"), { cause: inner });
|
|
expect(isTelegramClientRejection(outer)).toBe(true);
|
|
});
|
|
});
|