mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(telegram): add retry logic to health probe (openclaw#7405) thanks @mcinteerj
Verified: - CI=true pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test Co-authored-by: mcinteerj <3613653+mcinteerj@users.noreply.github.com>
This commit is contained in:
143
src/telegram/probe.test.ts
Normal file
143
src/telegram/probe.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { type Mock, describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { probeTelegram } from "./probe.js";
|
||||
|
||||
describe("probeTelegram retry logic", () => {
|
||||
const token = "test-token";
|
||||
const timeoutMs = 5000;
|
||||
let fetchMock: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
fetchMock = vi.fn();
|
||||
global.fetch = fetchMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should succeed if the first attempt succeeds", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
result: { id: 123, username: "test_bot" },
|
||||
}),
|
||||
};
|
||||
fetchMock.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
// Mock getWebhookInfo which is also called
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ ok: true, result: { url: "" } }),
|
||||
});
|
||||
|
||||
const result = await probeTelegram(token, timeoutMs);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2); // getMe + getWebhookInfo
|
||||
expect(result.bot?.username).toBe("test_bot");
|
||||
});
|
||||
|
||||
it("should retry and succeed if first attempt fails but second succeeds", async () => {
|
||||
// 1st attempt: Network error
|
||||
fetchMock.mockRejectedValueOnce(new Error("Network timeout"));
|
||||
|
||||
// 2nd attempt: Success
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
result: { id: 123, username: "test_bot" },
|
||||
}),
|
||||
});
|
||||
|
||||
// getWebhookInfo
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ ok: true, result: { url: "" } }),
|
||||
});
|
||||
|
||||
const probePromise = probeTelegram(token, timeoutMs);
|
||||
|
||||
// Fast-forward 1 second for the retry delay
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
const result = await probePromise;
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3); // fail getMe, success getMe, getWebhookInfo
|
||||
expect(result.bot?.username).toBe("test_bot");
|
||||
});
|
||||
|
||||
it("should retry twice and succeed on the third attempt", async () => {
|
||||
// 1st attempt: Network error
|
||||
fetchMock.mockRejectedValueOnce(new Error("Network error 1"));
|
||||
// 2nd attempt: Network error
|
||||
fetchMock.mockRejectedValueOnce(new Error("Network error 2"));
|
||||
|
||||
// 3rd attempt: Success
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
result: { id: 123, username: "test_bot" },
|
||||
}),
|
||||
});
|
||||
|
||||
// getWebhookInfo
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ ok: true, result: { url: "" } }),
|
||||
});
|
||||
|
||||
const probePromise = probeTelegram(token, timeoutMs);
|
||||
|
||||
// Fast-forward for two retries
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
const result = await probePromise;
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(4); // fail, fail, success, webhook
|
||||
expect(result.bot?.username).toBe("test_bot");
|
||||
});
|
||||
|
||||
it("should fail after 3 unsuccessful attempts", async () => {
|
||||
const errorMsg = "Final network error";
|
||||
fetchMock.mockRejectedValue(new Error(errorMsg));
|
||||
|
||||
const probePromise = probeTelegram(token, timeoutMs);
|
||||
|
||||
// Fast-forward for all retries
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
const result = await probePromise;
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBe(errorMsg);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3); // 3 attempts at getMe
|
||||
});
|
||||
|
||||
it("should NOT retry if getMe returns a 401 Unauthorized", async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
description: "Unauthorized",
|
||||
}),
|
||||
};
|
||||
fetchMock.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await probeTelegram(token, timeoutMs);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.status).toBe(401);
|
||||
expect(result.error).toBe("Unauthorized");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1); // Should not retry
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,26 @@ export async function probeTelegram(
|
||||
};
|
||||
|
||||
try {
|
||||
const meRes = await fetchWithTimeout(`${base}/getMe`, {}, timeoutMs, fetcher);
|
||||
let meRes: Response | null = null;
|
||||
let fetchError: unknown = null;
|
||||
|
||||
// Retry loop for initial connection (handles network/DNS startup races)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
meRes = await fetchWithTimeout(`${base}/getMe`, {}, timeoutMs, fetcher);
|
||||
break;
|
||||
} catch (err) {
|
||||
fetchError = err;
|
||||
if (i < 2) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!meRes) {
|
||||
throw fetchError;
|
||||
}
|
||||
|
||||
const meJson = (await meRes.json()) as {
|
||||
ok?: boolean;
|
||||
description?: string;
|
||||
|
||||
Reference in New Issue
Block a user