mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 16:40:24 +00:00
refactor(channels): dedupe transport and gateway test scaffolds
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
55
src/telegram/bot-message-context.test-harness.ts
Normal file
55
src/telegram/bot-message-context.test-harness.ts
Normal 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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user