mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Merged via squash.
Prepared head SHA: a4456d48b4
Co-authored-by: sircrumpet <4436535+sircrumpet@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
276 lines
8.0 KiB
TypeScript
276 lines
8.0 KiB
TypeScript
import { afterEach, type Mock, describe, expect, it, vi } from "vitest";
|
|
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
|
import { probeTelegram, resetTelegramProbeFetcherCacheForTests } from "./probe.js";
|
|
|
|
const resolveTelegramFetch = vi.hoisted(() => vi.fn());
|
|
const makeProxyFetch = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("./fetch.js", () => ({
|
|
resolveTelegramFetch,
|
|
}));
|
|
|
|
vi.mock("./proxy.js", () => ({
|
|
makeProxyFetch,
|
|
}));
|
|
|
|
describe("probeTelegram retry logic", () => {
|
|
const token = "test-token";
|
|
const timeoutMs = 5000;
|
|
const originalFetch = global.fetch;
|
|
|
|
const installFetchMock = (): Mock => {
|
|
const fetchMock = vi.fn();
|
|
global.fetch = withFetchPreconnect(fetchMock);
|
|
resolveTelegramFetch.mockImplementation((proxyFetch?: typeof fetch) => proxyFetch ?? fetch);
|
|
makeProxyFetch.mockImplementation(() => fetchMock as unknown as typeof fetch);
|
|
return fetchMock;
|
|
};
|
|
|
|
function mockGetMeSuccess(fetchMock: Mock) {
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
result: { id: 123, username: "test_bot" },
|
|
}),
|
|
});
|
|
}
|
|
|
|
function mockGetWebhookInfoSuccess(fetchMock: Mock) {
|
|
fetchMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: vi.fn().mockResolvedValue({ ok: true, result: { url: "" } }),
|
|
});
|
|
}
|
|
|
|
async function expectSuccessfulProbe(fetchMock: Mock, expectedCalls: number, retryCount = 0) {
|
|
const probePromise = probeTelegram(token, timeoutMs);
|
|
if (retryCount > 0) {
|
|
await vi.advanceTimersByTimeAsync(retryCount * 1000);
|
|
}
|
|
|
|
const result = await probePromise;
|
|
expect(result.ok).toBe(true);
|
|
expect(fetchMock).toHaveBeenCalledTimes(expectedCalls);
|
|
expect(result.bot?.username).toBe("test_bot");
|
|
}
|
|
|
|
afterEach(() => {
|
|
resetTelegramProbeFetcherCacheForTests();
|
|
resolveTelegramFetch.mockReset();
|
|
makeProxyFetch.mockReset();
|
|
vi.unstubAllEnvs();
|
|
vi.clearAllMocks();
|
|
if (originalFetch) {
|
|
global.fetch = originalFetch;
|
|
} else {
|
|
delete (globalThis as { fetch?: typeof fetch }).fetch;
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
errors: [],
|
|
expectedCalls: 2,
|
|
retryCount: 0,
|
|
},
|
|
{
|
|
errors: ["Network timeout"],
|
|
expectedCalls: 3,
|
|
retryCount: 1,
|
|
},
|
|
{
|
|
errors: ["Network error 1", "Network error 2"],
|
|
expectedCalls: 4,
|
|
retryCount: 2,
|
|
},
|
|
])("succeeds after retry pattern %#", async ({ errors, expectedCalls, retryCount }) => {
|
|
const fetchMock = installFetchMock();
|
|
vi.useFakeTimers();
|
|
try {
|
|
for (const message of errors) {
|
|
fetchMock.mockRejectedValueOnce(new Error(message));
|
|
}
|
|
|
|
mockGetMeSuccess(fetchMock);
|
|
mockGetWebhookInfoSuccess(fetchMock);
|
|
await expectSuccessfulProbe(fetchMock, expectedCalls, retryCount);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("should fail after 3 unsuccessful attempts", async () => {
|
|
const fetchMock = installFetchMock();
|
|
vi.useFakeTimers();
|
|
const errorMsg = "Final network error";
|
|
try {
|
|
fetchMock.mockRejectedValue(new Error(errorMsg));
|
|
|
|
const probePromise = probeTelegram(token, timeoutMs);
|
|
|
|
// Fast-forward for all retries
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
|
|
const result = await probePromise;
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toBe(errorMsg);
|
|
expect(fetchMock).toHaveBeenCalledTimes(3); // 3 attempts at getMe
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("respects timeout budget across retries", async () => {
|
|
const fetchMock = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
|
|
return new Promise<Response>((_resolve, reject) => {
|
|
const signal = init?.signal;
|
|
if (signal?.aborted) {
|
|
reject(new Error("Request aborted"));
|
|
return;
|
|
}
|
|
signal?.addEventListener("abort", () => reject(new Error("Request aborted")), {
|
|
once: true,
|
|
});
|
|
});
|
|
});
|
|
global.fetch = withFetchPreconnect(fetchMock as unknown as typeof fetch);
|
|
resolveTelegramFetch.mockImplementation((proxyFetch?: typeof fetch) => proxyFetch ?? fetch);
|
|
makeProxyFetch.mockImplementation(() => fetchMock as unknown as typeof fetch);
|
|
vi.useFakeTimers();
|
|
try {
|
|
const probePromise = probeTelegram(`${token}-budget`, 500);
|
|
await vi.advanceTimersByTimeAsync(600);
|
|
const result = await probePromise;
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("should NOT retry if getMe returns a 401 Unauthorized", async () => {
|
|
const fetchMock = installFetchMock();
|
|
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
|
|
});
|
|
|
|
it("uses resolver-scoped Telegram fetch with probe network options", async () => {
|
|
const fetchMock = installFetchMock();
|
|
mockGetMeSuccess(fetchMock);
|
|
mockGetWebhookInfoSuccess(fetchMock);
|
|
|
|
await probeTelegram(token, timeoutMs, {
|
|
proxyUrl: "http://127.0.0.1:8888",
|
|
network: {
|
|
autoSelectFamily: false,
|
|
dnsResultOrder: "ipv4first",
|
|
},
|
|
});
|
|
|
|
expect(makeProxyFetch).toHaveBeenCalledWith("http://127.0.0.1:8888");
|
|
expect(resolveTelegramFetch).toHaveBeenCalledWith(fetchMock, {
|
|
network: {
|
|
autoSelectFamily: false,
|
|
dnsResultOrder: "ipv4first",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("reuses probe fetcher across repeated probes for the same account transport settings", async () => {
|
|
const fetchMock = installFetchMock();
|
|
vi.stubEnv("VITEST", "");
|
|
vi.stubEnv("NODE_ENV", "production");
|
|
|
|
mockGetMeSuccess(fetchMock);
|
|
mockGetWebhookInfoSuccess(fetchMock);
|
|
await probeTelegram(`${token}-cache`, timeoutMs, {
|
|
network: {
|
|
autoSelectFamily: true,
|
|
dnsResultOrder: "ipv4first",
|
|
},
|
|
});
|
|
|
|
mockGetMeSuccess(fetchMock);
|
|
mockGetWebhookInfoSuccess(fetchMock);
|
|
await probeTelegram(`${token}-cache`, timeoutMs, {
|
|
network: {
|
|
autoSelectFamily: true,
|
|
dnsResultOrder: "ipv4first",
|
|
},
|
|
});
|
|
|
|
expect(resolveTelegramFetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not reuse probe fetcher cache when network settings differ", async () => {
|
|
const fetchMock = installFetchMock();
|
|
vi.stubEnv("VITEST", "");
|
|
vi.stubEnv("NODE_ENV", "production");
|
|
|
|
mockGetMeSuccess(fetchMock);
|
|
mockGetWebhookInfoSuccess(fetchMock);
|
|
await probeTelegram(`${token}-cache-variant`, timeoutMs, {
|
|
network: {
|
|
autoSelectFamily: true,
|
|
dnsResultOrder: "ipv4first",
|
|
},
|
|
});
|
|
|
|
mockGetMeSuccess(fetchMock);
|
|
mockGetWebhookInfoSuccess(fetchMock);
|
|
await probeTelegram(`${token}-cache-variant`, timeoutMs, {
|
|
network: {
|
|
autoSelectFamily: false,
|
|
dnsResultOrder: "ipv4first",
|
|
},
|
|
});
|
|
|
|
expect(resolveTelegramFetch).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("reuses probe fetcher cache across token rotation when accountId is stable", async () => {
|
|
const fetchMock = installFetchMock();
|
|
vi.stubEnv("VITEST", "");
|
|
vi.stubEnv("NODE_ENV", "production");
|
|
|
|
mockGetMeSuccess(fetchMock);
|
|
mockGetWebhookInfoSuccess(fetchMock);
|
|
await probeTelegram(`${token}-old`, timeoutMs, {
|
|
accountId: "main",
|
|
network: {
|
|
autoSelectFamily: true,
|
|
dnsResultOrder: "ipv4first",
|
|
},
|
|
});
|
|
|
|
mockGetMeSuccess(fetchMock);
|
|
mockGetWebhookInfoSuccess(fetchMock);
|
|
await probeTelegram(`${token}-new`, timeoutMs, {
|
|
accountId: "main",
|
|
network: {
|
|
autoSelectFamily: true,
|
|
dnsResultOrder: "ipv4first",
|
|
},
|
|
});
|
|
|
|
expect(resolveTelegramFetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|