import { withFetchPreconnect } from
"openclaw/plugin-sdk/testing" ;
import { afterEach, describe, expect, it, vi, type Mock } from
"vitest" ;
import { probeTelegram, resetTelegramProbeFetcherCacheForTests } from
"./probe.js" ;
const resolveTelegramFetch = vi.hoisted(() => vi.fn());
const makeProxyFetch = vi.hoisted(() => vi.fn());
vi.mock(
"./fetch.js" , () => ({
resolveTelegramFetch,
resolveTelegramApiBase: (apiRoot?: string) =>
apiRoot?.trim()?.replace(/\/+$/,
"" ) ||
"https://api.telegram.org ",
}));
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 ?? fe
tch);
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" ,
},
apiRoot: undefined,
});
});
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 );
});
});
Messung V0.5 in Prozent C=97 H=96 G=96
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.14Bemerkung:
(vorverarbeitet am 2026-06-13)
¤
*© Formatika GbR, Deutschland