refactor(channels): dedupe transport and gateway test scaffolds

This commit is contained in:
Peter Steinberger
2026-02-16 14:52:15 +00:00
parent f717a13039
commit 93ca0ed54f
95 changed files with 4068 additions and 5221 deletions

View File

@@ -1,43 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import { buildTelegramMessageContext } from "./bot-message-context.js";
import { describe, expect, it } from "vitest";
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
describe("buildTelegramMessageContext dm thread sessions", () => {
const baseConfig = {
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
channels: { telegram: {} },
messages: { groupChat: { mentionPatterns: [] } },
} as never;
const buildContext = async (message: Record<string, unknown>) =>
await buildTelegramMessageContext({
primaryCtx: {
message,
me: { id: 7, username: "bot" },
} as never,
allMedia: [],
storeAllowFrom: [],
options: {},
bot: {
api: {
sendChatAction: vi.fn(),
setMessageReaction: vi.fn(),
},
} as never,
cfg: baseConfig,
account: { accountId: "default" } as never,
historyLimit: 0,
groupHistories: new Map(),
dmPolicy: "open",
allowFrom: [],
groupAllowFrom: [],
ackReactionScope: "off",
logger: { info: vi.fn() },
resolveGroupActivation: () => undefined,
resolveGroupRequireMention: () => false,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: false },
topicConfig: undefined,
}),
await buildTelegramMessageContextForTest({
message,
});
it("uses thread session key for dm topics", async () => {
@@ -71,42 +38,11 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
});
describe("buildTelegramMessageContext group sessions without forum", () => {
const baseConfig = {
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
channels: { telegram: {} },
messages: { groupChat: { mentionPatterns: [] } },
} as never;
const buildContext = async (message: Record<string, unknown>) =>
await buildTelegramMessageContext({
primaryCtx: {
message,
me: { id: 7, username: "bot" },
} as never,
allMedia: [],
storeAllowFrom: [],
await buildTelegramMessageContextForTest({
message,
options: { forceWasMentioned: true },
bot: {
api: {
sendChatAction: vi.fn(),
setMessageReaction: vi.fn(),
},
} as never,
cfg: baseConfig,
account: { accountId: "default" } as never,
historyLimit: 0,
groupHistories: new Map(),
dmPolicy: "open",
allowFrom: [],
groupAllowFrom: [],
ackReactionScope: "off",
logger: { info: vi.fn() },
resolveGroupActivation: () => true,
resolveGroupRequireMention: () => false,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: false },
topicConfig: undefined,
}),
});
it("ignores message_thread_id for regular groups (not forums)", async () => {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { buildTelegramMessageContext } from "./bot-message-context.js";
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
// Mock recordInboundSession to capture updateLastRoute parameter
const recordInboundSessionMock = vi.fn().mockResolvedValue(undefined);
@@ -8,52 +8,15 @@ vi.mock("../channels/session.js", () => ({
}));
describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#8891)", () => {
const baseConfig = {
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
channels: { telegram: {} },
messages: { groupChat: { mentionPatterns: [] } },
} as never;
async function buildCtx(params: {
message: Record<string, unknown>;
options?: Record<string, unknown>;
resolveGroupActivation?: () => unknown;
}): Promise<Awaited<ReturnType<typeof buildTelegramMessageContext>>> {
return await buildTelegramMessageContext({
primaryCtx: {
message: {
message_id: 1,
date: 1700000000,
text: "hello",
from: { id: 42, first_name: "Alice" },
...params.message,
},
me: { id: 7, username: "bot" },
} as never,
allMedia: [],
storeAllowFrom: [],
options: params.options ?? {},
bot: {
api: {
sendChatAction: vi.fn(),
setMessageReaction: vi.fn(),
},
} as never,
cfg: baseConfig,
account: { accountId: "default" } as never,
historyLimit: 0,
groupHistories: new Map(),
dmPolicy: "open",
allowFrom: [],
groupAllowFrom: [],
ackReactionScope: "off",
logger: { info: vi.fn() },
resolveGroupActivation: params.resolveGroupActivation ?? (() => undefined),
resolveGroupRequireMention: () => false,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: false },
topicConfig: undefined,
}),
resolveGroupActivation?: () => boolean | undefined;
}) {
return await buildTelegramMessageContextForTest({
message: params.message,
options: params.options,
resolveGroupActivation: params.resolveGroupActivation,
});
}

View File

@@ -0,0 +1,55 @@
import { vi } from "vitest";
import { buildTelegramMessageContext } from "./bot-message-context.js";
export const baseTelegramMessageContextConfig = {
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
channels: { telegram: {} },
messages: { groupChat: { mentionPatterns: [] } },
} as never;
type BuildTelegramMessageContextForTestParams = {
message: Record<string, unknown>;
options?: Record<string, unknown>;
resolveGroupActivation?: () => boolean | undefined;
};
export async function buildTelegramMessageContextForTest(
params: BuildTelegramMessageContextForTestParams,
): Promise<Awaited<ReturnType<typeof buildTelegramMessageContext>>> {
return await buildTelegramMessageContext({
primaryCtx: {
message: {
message_id: 1,
date: 1_700_000_000,
text: "hello",
from: { id: 42, first_name: "Alice" },
...params.message,
},
me: { id: 7, username: "bot" },
} as never,
allMedia: [],
storeAllowFrom: [],
options: params.options ?? {},
bot: {
api: {
sendChatAction: vi.fn(),
setMessageReaction: vi.fn(),
},
} as never,
cfg: baseTelegramMessageContextConfig,
account: { accountId: "default" } as never,
historyLimit: 0,
groupHistories: new Map(),
dmPolicy: "open",
allowFrom: [],
groupAllowFrom: [],
ackReactionScope: "off",
logger: { info: vi.fn() },
resolveGroupActivation: params.resolveGroupActivation ?? (() => undefined),
resolveGroupRequireMention: () => false,
resolveTelegramGroupConfig: () => ({
groupConfig: { requireMention: false },
topicConfig: undefined,
}),
});
}

View File

@@ -18,9 +18,13 @@ const deliveryMocks = vi.hoisted(() => ({
deliverReplies: vi.fn(async () => ({ delivered: true })),
}));
vi.mock("../auto-reply/skill-commands.js", () => ({
listSkillCommandsForAgents,
}));
vi.mock("../auto-reply/skill-commands.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../auto-reply/skill-commands.js")>();
return {
...actual,
listSkillCommandsForAgents,
};
});
vi.mock("../plugins/commands.js", () => ({
getPluginCommandSpecs: pluginCommandMocks.getPluginCommandSpecs,
matchPluginCommand: pluginCommandMocks.matchPluginCommand,

View File

@@ -21,6 +21,18 @@ async function createBotHandler(): Promise<{
handler: (ctx: Record<string, unknown>) => Promise<void>;
replySpy: ReturnType<typeof vi.fn>;
runtimeError: ReturnType<typeof vi.fn>;
}> {
return createBotHandlerWithOptions({});
}
async function createBotHandlerWithOptions(options: {
proxyFetch?: typeof fetch;
runtimeLog?: ReturnType<typeof vi.fn>;
runtimeError?: ReturnType<typeof vi.fn>;
}): Promise<{
handler: (ctx: Record<string, unknown>) => Promise<void>;
replySpy: ReturnType<typeof vi.fn>;
runtimeError: ReturnType<typeof vi.fn>;
}> {
const { createTelegramBot } = await import("./bot.js");
const replyModule = await import("../auto-reply/reply.js");
@@ -30,12 +42,14 @@ async function createBotHandler(): Promise<{
replySpy.mockReset();
sendChatActionSpy.mockReset();
const runtimeError = vi.fn();
const runtimeError = options.runtimeError ?? vi.fn();
const runtimeLog = options.runtimeLog ?? vi.fn();
createTelegramBot({
token: "tok",
testTimings: TELEGRAM_TEST_TIMINGS,
...(options.proxyFetch ? { proxyFetch: options.proxyFetch } : {}),
runtime: {
log: vi.fn(),
log: runtimeLog,
error: runtimeError,
exit: () => {
throw new Error("exit");
@@ -46,7 +60,6 @@ async function createBotHandler(): Promise<{
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
return { handler, replySpy, runtimeError };
}
@@ -63,6 +76,16 @@ function mockTelegramFileDownload(params: {
} as Response);
}
function mockTelegramPngDownload(): ReturnType<typeof vi.spyOn> {
return vi.spyOn(globalThis, "fetch" as never).mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
headers: { get: () => "image/png" },
arrayBuffer: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer,
} as Response);
}
beforeEach(() => {
vi.useRealTimers();
lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
@@ -122,10 +145,6 @@ describe("telegram inbound media", () => {
);
it("prefers proxyFetch over global fetch", async () => {
const { createTelegramBot } = await import("./bot.js");
onSpy.mockReset();
const runtimeLog = vi.fn();
const runtimeError = vi.fn();
const globalFetchSpy = vi.spyOn(globalThis, "fetch" as never).mockImplementation(() => {
@@ -139,22 +158,11 @@ describe("telegram inbound media", () => {
arrayBuffer: async () => new Uint8Array([0xff, 0xd8, 0xff]).buffer,
} as Response);
createTelegramBot({
token: "tok",
testTimings: TELEGRAM_TEST_TIMINGS,
const { handler } = await createBotHandlerWithOptions({
proxyFetch: proxyFetch as unknown as typeof fetch,
runtime: {
log: runtimeLog,
error: runtimeError,
exit: () => {
throw new Error("exit");
},
},
runtimeLog,
runtimeError,
});
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
await handler({
message: {
@@ -176,32 +184,13 @@ describe("telegram inbound media", () => {
});
it("logs a handler error when getFile returns no file_path", async () => {
const { createTelegramBot } = await import("./bot.js");
const replyModule = await import("../auto-reply/reply.js");
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
onSpy.mockReset();
replySpy.mockReset();
const runtimeLog = vi.fn();
const runtimeError = vi.fn();
const fetchSpy = vi.spyOn(globalThis, "fetch" as never);
createTelegramBot({
token: "tok",
testTimings: TELEGRAM_TEST_TIMINGS,
runtime: {
log: runtimeLog,
error: runtimeError,
exit: () => {
throw new Error("exit");
},
},
const { handler, replySpy } = await createBotHandlerWithOptions({
runtimeLog,
runtimeError,
});
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
const fetchSpy = vi.spyOn(globalThis, "fetch" as never);
await handler({
message: {
@@ -235,37 +224,9 @@ describe("telegram media groups", () => {
it(
"buffers messages with same media_group_id and processes them together",
async () => {
const { createTelegramBot } = await import("./bot.js");
const replyModule = await import("../auto-reply/reply.js");
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
onSpy.mockReset();
replySpy.mockReset();
const runtimeError = vi.fn();
const fetchSpy = vi.spyOn(globalThis, "fetch" as never).mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
headers: { get: () => "image/png" },
arrayBuffer: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer,
} as Response);
createTelegramBot({
token: "tok",
testTimings: TELEGRAM_TEST_TIMINGS,
runtime: {
log: vi.fn(),
error: runtimeError,
exit: () => {
throw new Error("exit");
},
},
});
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
const { handler, replySpy } = await createBotHandlerWithOptions({ runtimeError });
const fetchSpy = mockTelegramPngDownload();
const first = handler({
message: {
@@ -312,26 +273,8 @@ describe("telegram media groups", () => {
it(
"processes separate media groups independently",
async () => {
const { createTelegramBot } = await import("./bot.js");
const replyModule = await import("../auto-reply/reply.js");
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
onSpy.mockReset();
replySpy.mockReset();
const fetchSpy = vi.spyOn(globalThis, "fetch" as never).mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
headers: { get: () => "image/png" },
arrayBuffer: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer,
} as Response);
createTelegramBot({ token: "tok", testTimings: TELEGRAM_TEST_TIMINGS });
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
const { handler, replySpy } = await createBotHandler();
const fetchSpy = mockTelegramPngDownload();
const first = handler({
message: {
@@ -431,13 +374,7 @@ describe("telegram stickers", () => {
it(
"refreshes cached sticker metadata on cache hit",
async () => {
const { createTelegramBot } = await import("./bot.js");
const replyModule = await import("../auto-reply/reply.js");
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
onSpy.mockReset();
replySpy.mockReset();
sendChatActionSpy.mockReset();
const { handler, replySpy, runtimeError } = await createBotHandler();
getCachedStickerSpy.mockReturnValue({
fileId: "old_file_id",
@@ -448,23 +385,6 @@ describe("telegram stickers", () => {
cachedAt: "2026-01-20T10:00:00.000Z",
});
const runtimeError = vi.fn();
createTelegramBot({
token: "tok",
testTimings: TELEGRAM_TEST_TIMINGS,
runtime: {
log: vi.fn(),
error: runtimeError,
exit: () => {
throw new Error("exit");
},
},
});
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
const fetchSpy = vi.spyOn(globalThis, "fetch" as never).mockResolvedValueOnce({
ok: true,
status: 200,

View File

@@ -1,23 +1,28 @@
import { describe, expect, it, vi } from "vitest";
import { onSpy } from "./bot.media.e2e-harness.js";
async function createMessageHandlerAndReplySpy() {
const { createTelegramBot } = await import("./bot.js");
const replyModule = await import("../auto-reply/reply.js");
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
onSpy.mockReset();
replySpy.mockReset();
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
return { handler, replySpy };
}
describe("telegram inbound media", () => {
const _INBOUND_MEDIA_TEST_TIMEOUT_MS = process.platform === "win32" ? 30_000 : 20_000;
it(
"includes location text and ctx fields for pins",
async () => {
const { createTelegramBot } = await import("./bot.js");
const replyModule = await import("../auto-reply/reply.js");
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
onSpy.mockReset();
replySpy.mockReset();
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
const { handler, replySpy } = await createMessageHandlerAndReplySpy();
await handler({
message: {
@@ -50,18 +55,7 @@ describe("telegram inbound media", () => {
it(
"captures venue fields for named places",
async () => {
const { createTelegramBot } = await import("./bot.js");
const replyModule = await import("../auto-reply/reply.js");
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
onSpy.mockReset();
replySpy.mockReset();
createTelegramBot({ token: "tok" });
const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
ctx: Record<string, unknown>,
) => Promise<void>;
expect(handler).toBeDefined();
const { handler, replySpy } = await createMessageHandlerAndReplySpy();
await handler({
message: {

View File

@@ -1,44 +1,54 @@
import { describe, expect, it, vi } from "vitest";
import { createTelegramDraftStream } from "./draft-stream.js";
function createMockDraftApi(sendMessageImpl?: () => Promise<{ message_id: number }>) {
return {
sendMessage: vi.fn(sendMessageImpl ?? (async () => ({ message_id: 17 }))),
editMessageText: vi.fn().mockResolvedValue(true),
deleteMessage: vi.fn().mockResolvedValue(true),
};
}
function createForumDraftStream(api: ReturnType<typeof createMockDraftApi>) {
return createThreadedDraftStream(api, { id: 99, scope: "forum" });
}
function createThreadedDraftStream(
api: ReturnType<typeof createMockDraftApi>,
thread: { id: number; scope: "forum" | "dm" },
) {
return createTelegramDraftStream({
// oxlint-disable-next-line typescript/no-explicit-any
api: api as any,
chatId: 123,
thread,
});
}
async function expectInitialForumSend(
api: ReturnType<typeof createMockDraftApi>,
text = "Hello",
): Promise<void> {
await vi.waitFor(() =>
expect(api.sendMessage).toHaveBeenCalledWith(123, text, { message_thread_id: 99 }),
);
}
describe("createTelegramDraftStream", () => {
it("sends stream preview message with message_thread_id when provided", async () => {
const api = {
sendMessage: vi.fn().mockResolvedValue({ message_id: 17 }),
editMessageText: vi.fn().mockResolvedValue(true),
deleteMessage: vi.fn().mockResolvedValue(true),
};
const stream = createTelegramDraftStream({
// oxlint-disable-next-line typescript/no-explicit-any
api: api as any,
chatId: 123,
thread: { id: 99, scope: "forum" },
});
const api = createMockDraftApi();
const stream = createForumDraftStream(api);
stream.update("Hello");
await vi.waitFor(() =>
expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 99 }),
);
await expectInitialForumSend(api);
});
it("edits existing stream preview message on subsequent updates", async () => {
const api = {
sendMessage: vi.fn().mockResolvedValue({ message_id: 17 }),
editMessageText: vi.fn().mockResolvedValue(true),
deleteMessage: vi.fn().mockResolvedValue(true),
};
const stream = createTelegramDraftStream({
// oxlint-disable-next-line typescript/no-explicit-any
api: api as any,
chatId: 123,
thread: { id: 99, scope: "forum" },
});
const api = createMockDraftApi();
const stream = createForumDraftStream(api);
stream.update("Hello");
await vi.waitFor(() =>
expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 99 }),
);
await expectInitialForumSend(api);
await (api.sendMessage.mock.results[0]?.value as Promise<unknown>);
stream.update("Hello again");
@@ -52,17 +62,8 @@ describe("createTelegramDraftStream", () => {
const firstSend = new Promise<{ message_id: number }>((resolve) => {
resolveSend = resolve;
});
const api = {
sendMessage: vi.fn().mockReturnValue(firstSend),
editMessageText: vi.fn().mockResolvedValue(true),
deleteMessage: vi.fn().mockResolvedValue(true),
};
const stream = createTelegramDraftStream({
// oxlint-disable-next-line typescript/no-explicit-any
api: api as any,
chatId: 123,
thread: { id: 99, scope: "forum" },
});
const api = createMockDraftApi(() => firstSend);
const stream = createForumDraftStream(api);
stream.update("Hello");
await vi.waitFor(() => expect(api.sendMessage).toHaveBeenCalledTimes(1));
@@ -77,17 +78,8 @@ describe("createTelegramDraftStream", () => {
});
it("omits message_thread_id for general topic id", async () => {
const api = {
sendMessage: vi.fn().mockResolvedValue({ message_id: 17 }),
editMessageText: vi.fn().mockResolvedValue(true),
deleteMessage: vi.fn().mockResolvedValue(true),
};
const stream = createTelegramDraftStream({
// oxlint-disable-next-line typescript/no-explicit-any
api: api as any,
chatId: 123,
thread: { id: 1, scope: "forum" },
});
const api = createMockDraftApi();
const stream = createThreadedDraftStream(api, { id: 1, scope: "forum" });
stream.update("Hello");
@@ -95,17 +87,8 @@ describe("createTelegramDraftStream", () => {
});
it("omits message_thread_id for dm threads and clears preview on cleanup", async () => {
const api = {
sendMessage: vi.fn().mockResolvedValue({ message_id: 17 }),
editMessageText: vi.fn().mockResolvedValue(true),
deleteMessage: vi.fn().mockResolvedValue(true),
};
const stream = createTelegramDraftStream({
// oxlint-disable-next-line typescript/no-explicit-any
api: api as any,
chatId: 123,
thread: { id: 1, scope: "dm" },
});
const api = createMockDraftApi();
const stream = createThreadedDraftStream(api, { id: 1, scope: "dm" });
stream.update("Hello");
await vi.waitFor(() => expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", undefined));

View File

@@ -6,6 +6,35 @@ describe("probeTelegram retry logic", () => {
const timeoutMs = 5000;
let fetchMock: Mock;
function mockGetMeSuccess() {
fetchMock.mockResolvedValueOnce({
ok: true,
json: vi.fn().mockResolvedValue({
ok: true,
result: { id: 123, username: "test_bot" },
}),
});
}
function mockGetWebhookInfoSuccess() {
fetchMock.mockResolvedValueOnce({
ok: true,
json: vi.fn().mockResolvedValue({ ok: true, result: { url: "" } }),
});
}
async function expectSuccessfulProbe(expectedCalls: number, retryCount = 0) {
const probePromise = probeTelegram(token, timeoutMs);
for (let i = 0; i < retryCount; i += 1) {
await vi.advanceTimersByTimeAsync(1000);
}
const result = await probePromise;
expect(result.ok).toBe(true);
expect(fetchMock).toHaveBeenCalledTimes(expectedCalls);
expect(result.bot?.username).toBe("test_bot");
}
beforeEach(() => {
vi.useFakeTimers();
fetchMock = vi.fn();
@@ -18,57 +47,18 @@ describe("probeTelegram retry logic", () => {
});
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");
mockGetMeSuccess();
mockGetWebhookInfoSuccess();
await expectSuccessfulProbe(2);
});
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");
mockGetMeSuccess();
mockGetWebhookInfoSuccess();
await expectSuccessfulProbe(3, 1);
});
it("should retry twice and succeed on the third attempt", async () => {
@@ -77,32 +67,9 @@ describe("probeTelegram retry logic", () => {
// 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");
mockGetMeSuccess();
mockGetWebhookInfoSuccess();
await expectSuccessfulProbe(4, 2);
});
it("should fail after 3 unsuccessful attempts", async () => {