From 63711330e42c19ea35e5cb259dc88d554d3c6efd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 02:33:56 +0000 Subject: [PATCH] perf(test): dedupe browser/telegram coverage and trim batch retry cost --- ....agent-contract-snapshot-endpoints.test.ts | 11 + ...fault-maxchars-explicitly-set-zero.test.ts | 276 --- src/memory/manager.batch.test.ts | 57 +- src/telegram/bot.test.ts | 1477 ----------------- 4 files changed, 43 insertions(+), 1778 deletions(-) delete mode 100644 src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts diff --git a/src/browser/server.agent-contract-snapshot-endpoints.test.ts b/src/browser/server.agent-contract-snapshot-endpoints.test.ts index 8c4530a91a..3d84311046 100644 --- a/src/browser/server.agent-contract-snapshot-endpoints.test.ts +++ b/src/browser/server.agent-contract-snapshot-endpoints.test.ts @@ -317,6 +317,17 @@ describe("browser control server", () => { targetId: "abcd1234", maxChars: DEFAULT_AI_SNAPSHOT_MAX_CHARS, }); + + const snapAiZero = (await realFetch(`${base}/snapshot?format=ai&maxChars=0`).then((r) => + r.json(), + )) as { ok: boolean; format?: string }; + expect(snapAiZero.ok).toBe(true); + expect(snapAiZero.format).toBe("ai"); + const [lastCall] = pwMocks.snapshotAiViaPlaywright.mock.calls.at(-1) ?? []; + expect(lastCall).toEqual({ + cdpUrl: cdpBaseUrl, + targetId: "abcd1234", + }); }); it("agent contract: navigation + common act commands", async () => { diff --git a/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts b/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts deleted file mode 100644 index 9d6810ed31..0000000000 --- a/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { type AddressInfo, createServer } from "node:net"; -import { fetch as realFetch } from "undici"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; - -let testPort = 0; -let cdpBaseUrl = ""; -let reachable = false; -let cfgAttachOnly = false; -let prevGatewayPort: string | undefined; - -const cdpMocks = vi.hoisted(() => ({ - createTargetViaCdp: vi.fn(async () => { - throw new Error("cdp disabled"); - }), - snapshotAria: vi.fn(async () => ({ - nodes: [{ ref: "1", role: "link", name: "x", depth: 0 }], - })), -})); - -const pwMocks = vi.hoisted(() => ({ - armDialogViaPlaywright: vi.fn(async () => {}), - armFileUploadViaPlaywright: vi.fn(async () => {}), - clickViaPlaywright: vi.fn(async () => {}), - closePageViaPlaywright: vi.fn(async () => {}), - closePlaywrightBrowserConnection: vi.fn(async () => {}), - downloadViaPlaywright: vi.fn(async () => ({ - url: "https://example.com/report.pdf", - suggestedFilename: "report.pdf", - path: "/tmp/report.pdf", - })), - dragViaPlaywright: vi.fn(async () => {}), - evaluateViaPlaywright: vi.fn(async () => "ok"), - fillFormViaPlaywright: vi.fn(async () => {}), - getConsoleMessagesViaPlaywright: vi.fn(async () => []), - hoverViaPlaywright: vi.fn(async () => {}), - scrollIntoViewViaPlaywright: vi.fn(async () => {}), - navigateViaPlaywright: vi.fn(async () => ({ url: "https://example.com" })), - pdfViaPlaywright: vi.fn(async () => ({ buffer: Buffer.from("pdf") })), - pressKeyViaPlaywright: vi.fn(async () => {}), - responseBodyViaPlaywright: vi.fn(async () => ({ - url: "https://example.com/api/data", - status: 200, - headers: { "content-type": "application/json" }, - body: '{"ok":true}', - })), - resizeViewportViaPlaywright: vi.fn(async () => {}), - selectOptionViaPlaywright: vi.fn(async () => {}), - setInputFilesViaPlaywright: vi.fn(async () => {}), - snapshotAiViaPlaywright: vi.fn(async () => ({ snapshot: "ok" })), - takeScreenshotViaPlaywright: vi.fn(async () => ({ - buffer: Buffer.from("png"), - })), - typeViaPlaywright: vi.fn(async () => {}), - waitForDownloadViaPlaywright: vi.fn(async () => ({ - url: "https://example.com/report.pdf", - suggestedFilename: "report.pdf", - path: "/tmp/report.pdf", - })), - waitForViaPlaywright: vi.fn(async () => {}), -})); - -function makeProc(pid = 123) { - const handlers = new Map void>>(); - return { - pid, - killed: false, - exitCode: null as number | null, - on: (event: string, cb: (...args: unknown[]) => void) => { - handlers.set(event, [...(handlers.get(event) ?? []), cb]); - return undefined; - }, - emitExit: () => { - for (const cb of handlers.get("exit") ?? []) { - cb(0); - } - }, - kill: () => { - return true; - }, - }; -} - -const proc = makeProc(); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => ({ - browser: { - enabled: true, - color: "#FF4500", - attachOnly: cfgAttachOnly, - headless: true, - defaultProfile: "openclaw", - profiles: { - openclaw: { cdpPort: testPort + 1, color: "#FF4500" }, - }, - }, - }), - writeConfigFile: vi.fn(async () => {}), - }; -}); - -const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>); -vi.mock("./chrome.js", () => ({ - isChromeCdpReady: vi.fn(async () => reachable), - isChromeReachable: vi.fn(async () => reachable), - launchOpenClawChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => { - launchCalls.push({ port: profile.cdpPort }); - reachable = true; - return { - pid: 123, - exe: { kind: "chrome", path: "/fake/chrome" }, - userDataDir: "/tmp/openclaw", - cdpPort: profile.cdpPort, - startedAt: Date.now(), - proc, - }; - }), - resolveOpenClawUserDataDir: vi.fn(() => "/tmp/openclaw"), - stopOpenClawChrome: vi.fn(async () => { - reachable = false; - }), -})); - -vi.mock("./cdp.js", () => ({ - createTargetViaCdp: cdpMocks.createTargetViaCdp, - normalizeCdpWsUrl: vi.fn((wsUrl: string) => wsUrl), - snapshotAria: cdpMocks.snapshotAria, - getHeadersWithAuth: vi.fn(() => ({})), - appendCdpPath: vi.fn((cdpUrl: string, path: string) => { - const base = cdpUrl.replace(/\/$/, ""); - const suffix = path.startsWith("/") ? path : `/${path}`; - return `${base}${suffix}`; - }), -})); - -vi.mock("./pw-ai.js", () => pwMocks); - -vi.mock("../media/store.js", () => ({ - ensureMediaDir: vi.fn(async () => {}), - saveMediaBuffer: vi.fn(async () => ({ path: "/tmp/fake.png" })), -})); - -vi.mock("./screenshot.js", () => ({ - DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES: 128, - DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE: 64, - normalizeBrowserScreenshot: vi.fn(async (buf: Buffer) => ({ - buffer: buf, - contentType: "image/png", - })), -})); - -async function getFreePort(): Promise { - while (true) { - const port = await new Promise((resolve, reject) => { - const s = createServer(); - s.once("error", reject); - s.listen(0, "127.0.0.1", () => { - const assigned = (s.address() as AddressInfo).port; - s.close((err) => (err ? reject(err) : resolve(assigned))); - }); - }); - if (port < 65535) { - return port; - } - } -} - -function makeResponse( - body: unknown, - init?: { ok?: boolean; status?: number; text?: string }, -): Response { - const ok = init?.ok ?? true; - const status = init?.status ?? 200; - const text = init?.text ?? ""; - return { - ok, - status, - json: async () => body, - text: async () => text, - } as unknown as Response; -} - -describe("browser control server", () => { - beforeAll(async () => { - reachable = false; - cfgAttachOnly = false; - launchCalls.length = 0; - - cdpMocks.createTargetViaCdp.mockImplementation(async () => { - throw new Error("cdp disabled"); - }); - - for (const fn of Object.values(pwMocks)) { - fn.mockClear(); - } - for (const fn of Object.values(cdpMocks)) { - fn.mockClear(); - } - - testPort = await getFreePort(); - cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; - prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; - process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2); - - // Minimal CDP JSON endpoints used by the server. - vi.stubGlobal( - "fetch", - vi.fn(async (url: string, init?: RequestInit) => { - const u = String(url); - if (u.includes("/json/list")) { - if (!reachable) { - return makeResponse([]); - } - return makeResponse([ - { - id: "abcd1234", - title: "Tab", - url: "https://example.com", - webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/abcd1234", - type: "page", - }, - { - id: "abce9999", - title: "Other", - url: "https://other", - webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/abce9999", - type: "page", - }, - ]); - } - void init; - return makeResponse({}, { ok: false, status: 500, text: "unexpected" }); - }), - ); - }); - - afterAll(async () => { - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - if (prevGatewayPort === undefined) { - delete process.env.OPENCLAW_GATEWAY_PORT; - } else { - process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; - } - const { stopBrowserControlServer } = await import("./server.js"); - await stopBrowserControlServer(); - }); - - it("keeps maxChars unset when snapshot explicitly passes zero", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); - const started = await startBrowserControlServerFromConfig(); - expect(started?.port).toBe(testPort); - - const base = `http://127.0.0.1:${testPort}`; - const startedPayload = (await realFetch(`${base}/start`, { method: "POST" }).then((r) => - r.json(), - )) as { ok: boolean; profile?: string }; - expect(startedPayload.ok).toBe(true); - expect(startedPayload.profile).toBe("openclaw"); - - const snapAi = (await realFetch(`${base}/snapshot?format=ai&maxChars=0`).then((r) => - r.json(), - )) as { ok: boolean; format?: string }; - expect(snapAi.ok).toBe(true); - expect(snapAi.format).toBe("ai"); - expect(launchCalls.length).toBeGreaterThan(0); - const [call] = pwMocks.snapshotAiViaPlaywright.mock.calls.at(-1) ?? []; - expect(call).toEqual({ - cdpUrl: cdpBaseUrl, - targetId: "abcd1234", - }); - }); -}); diff --git a/src/memory/manager.batch.test.ts b/src/memory/manager.batch.test.ts index 2cf1b30c05..4eed7a1f95 100644 --- a/src/memory/manager.batch.test.ts +++ b/src/memory/manager.batch.test.ts @@ -30,7 +30,22 @@ describe("memory indexing with OpenAI batches", () => { let workspaceDir: string; let indexPath: string; let manager: MemoryIndexManager | null = null; - let setTimeoutSpy: ReturnType; + + function useFastShortTimeouts() { + const realSetTimeout = setTimeout; + const spy = vi.spyOn(global, "setTimeout").mockImplementation((( + handler: TimerHandler, + timeout?: number, + ...args: unknown[] + ) => { + const delay = typeof timeout === "number" ? timeout : 0; + if (delay > 0 && delay <= 2000) { + return realSetTimeout(handler, 0, ...args); + } + return realSetTimeout(handler, delay, ...args); + }) as typeof setTimeout); + return () => spy.mockRestore(); + } beforeAll(async () => { fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-batch-")); @@ -46,18 +61,6 @@ describe("memory indexing with OpenAI batches", () => { embedBatch.mockImplementation(async (texts: string[]) => texts.map((_text, index) => [index + 1, 0, 0]), ); - const realSetTimeout = setTimeout; - setTimeoutSpy = vi.spyOn(global, "setTimeout").mockImplementation((( - handler: TimerHandler, - timeout?: number, - ...args: unknown[] - ) => { - const delay = typeof timeout === "number" ? timeout : 0; - if (delay > 0 && delay <= 2000) { - return realSetTimeout(handler, 0, ...args); - } - return realSetTimeout(handler, delay, ...args); - }) as typeof setTimeout); workspaceDir = path.join(fixtureRoot, `case-${++caseId}`); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true }); @@ -65,7 +68,6 @@ describe("memory indexing with OpenAI batches", () => { afterEach(async () => { vi.unstubAllGlobals(); - setTimeoutSpy.mockRestore(); if (manager) { await manager.close(); manager = null; @@ -180,6 +182,7 @@ describe("memory indexing with OpenAI batches", () => { }); it("retries OpenAI batch create on transient failures", async () => { + const restoreTimeouts = useFastShortTimeouts(); const content = ["retry", "the", "batch"].join("\n\n"); await fs.writeFile(path.join(workspaceDir, "memory", "2026-01-08.md"), content); @@ -268,17 +271,21 @@ describe("memory indexing with OpenAI batches", () => { }, }; - const result = await getMemorySearchManager({ cfg, agentId: "main" }); - expect(result.manager).not.toBeNull(); - if (!result.manager) { - throw new Error("manager missing"); - } - manager = result.manager; - await manager.sync({ force: true }); + try { + const result = await getMemorySearchManager({ cfg, agentId: "main" }); + expect(result.manager).not.toBeNull(); + if (!result.manager) { + throw new Error("manager missing"); + } + manager = result.manager; + await manager.sync({ force: true }); - const status = manager.status(); - expect(status.chunks).toBeGreaterThan(0); - expect(batchCreates).toBe(2); + const status = manager.status(); + expect(status.chunks).toBeGreaterThan(0); + expect(batchCreates).toBe(2); + } finally { + restoreTimeouts(); + } }); it("tracks batch failures, resets on success, and disables after repeated failures", async () => { @@ -319,7 +326,7 @@ describe("memory indexing with OpenAI batches", () => { } if (url.endsWith("/batches")) { if (mode === "fail") { - return new Response("batch failed", { status: 500 }); + return new Response("batch failed", { status: 400 }); } return new Response(JSON.stringify({ id: "batch_1", status: "in_progress" }), { status: 200, diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 1d4b7bd4b7..2e4ffa338e 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -222,12 +222,6 @@ describe("createTelegramBot", () => { tempDirs.length = 0; }); - it("installs grammY throttler", () => { - createTelegramBot({ token: "tok" }); - expect(throttlerSpy).toHaveBeenCalledTimes(1); - expect(useSpy).toHaveBeenCalledWith("throttler"); - }); - it("merges custom commands with native commands", () => { const config = { channels: { @@ -329,84 +323,6 @@ describe("createTelegramBot", () => { expect(registered.some((command) => reserved.has(command.command))).toBe(false); }); - it("uses wrapped fetch when global fetch is available", () => { - const originalFetch = globalThis.fetch; - const fetchSpy = vi.fn() as unknown as typeof fetch; - globalThis.fetch = fetchSpy; - try { - createTelegramBot({ token: "tok" }); - const fetchImpl = resolveTelegramFetch(); - expect(fetchImpl).toBeTypeOf("function"); - expect(fetchImpl).not.toBe(fetchSpy); - const clientFetch = (botCtorSpy.mock.calls[0]?.[1] as { client?: { fetch?: unknown } }) - ?.client?.fetch; - expect(clientFetch).toBeTypeOf("function"); - expect(clientFetch).not.toBe(fetchSpy); - } finally { - globalThis.fetch = originalFetch; - } - }); - - it("sequentializes updates by chat and thread", () => { - createTelegramBot({ token: "tok" }); - expect(sequentializeSpy).toHaveBeenCalledTimes(1); - expect(middlewareUseSpy).toHaveBeenCalledWith(sequentializeSpy.mock.results[0]?.value); - expect(sequentializeKey).toBe(getTelegramSequentialKey); - expect(getTelegramSequentialKey({ message: { chat: { id: 123 } } })).toBe("telegram:123"); - expect( - getTelegramSequentialKey({ - message: { chat: { id: 123, type: "private" }, message_thread_id: 9 }, - }), - ).toBe("telegram:123:topic:9"); - expect( - getTelegramSequentialKey({ - message: { chat: { id: 123, type: "supergroup" }, message_thread_id: 9 }, - }), - ).toBe("telegram:123"); - expect( - getTelegramSequentialKey({ - message: { chat: { id: 123, type: "supergroup", is_forum: true } }, - }), - ).toBe("telegram:123:topic:1"); - expect( - getTelegramSequentialKey({ - update: { message: { chat: { id: 555 } } }, - }), - ).toBe("telegram:555"); - }); - - it("routes callback_query payloads as messages and answers callbacks", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - createTelegramBot({ token: "tok" }); - const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as ( - ctx: Record, - ) => Promise; - expect(callbackHandler).toBeDefined(); - - await callbackHandler({ - callbackQuery: { - id: "cbq-1", - data: "cmd:option_a", - from: { id: 9, first_name: "Ada", username: "ada_bot" }, - message: { - chat: { id: 1234, type: "private" }, - date: 1736380800, - message_id: 10, - }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.Body).toContain("cmd:option_a"); - expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-1"); - }); - it("blocks callback_query when inline buttons are allowlist-only and sender not authorized", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; @@ -529,193 +445,6 @@ describe("createTelegramBot", () => { expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-4"); }); - it("wraps inbound message with Telegram envelope", async () => { - const originalTz = process.env.TZ; - process.env.TZ = "Europe/Vienna"; - - try { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - createTelegramBot({ token: "tok" }); - expect(onSpy).toHaveBeenCalledWith("message", expect.any(Function)); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - const message = { - chat: { id: 1234, type: "private" }, - text: "hello world", - date: 1736380800, // 2025-01-09T00:00:00Z - from: { - first_name: "Ada", - last_name: "Lovelace", - username: "ada_bot", - }, - }; - await handler({ - message, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z")); - const timestampPattern = escapeRegExp(expectedTimestamp); - expect(payload.Body).toMatch( - new RegExp( - `^\\[Telegram Ada Lovelace \\(@ada_bot\\) id:1234 (\\+\\d+[smhd] )?${timestampPattern}\\]`, - ), - ); - expect(payload.Body).toContain("hello world"); - } finally { - process.env.TZ = originalTz; - } - }); - - it("requests pairing by default for unknown DM senders", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { telegram: { dmPolicy: "pairing" } }, - }); - readChannelAllowFromStore.mockResolvedValue([]); - upsertChannelPairingRequest.mockResolvedValue({ - code: "PAIRME12", - created: true, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 1234, type: "private" }, - text: "hello", - date: 1736380800, - from: { id: 999, username: "random" }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).not.toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(sendMessageSpy.mock.calls[0]?.[0]).toBe(1234); - const pairingText = String(sendMessageSpy.mock.calls[0]?.[1]); - expect(pairingText).toContain("Your Telegram user id: 999"); - expect(pairingText).toContain("Pairing code:"); - expect(pairingText).toContain("PAIRME12"); - expect(pairingText).toContain("openclaw pairing approve telegram PAIRME12"); - expect(pairingText).not.toContain(""); - }); - - it("does not resend pairing code when a request is already pending", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { telegram: { dmPolicy: "pairing" } }, - }); - readChannelAllowFromStore.mockResolvedValue([]); - upsertChannelPairingRequest - .mockResolvedValueOnce({ code: "PAIRME12", created: true }) - .mockResolvedValueOnce({ code: "PAIRME12", created: false }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - const message = { - chat: { id: 1234, type: "private" }, - text: "hello", - date: 1736380800, - from: { id: 999, username: "random" }, - }; - - await handler({ - message, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - await handler({ - message: { ...message, text: "hello again" }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).not.toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - }); - - it("triggers typing cue via onReplyStart", async () => { - onSpy.mockReset(); - sendChatActionSpy.mockReset(); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - await handler({ - message: { chat: { id: 42, type: "private" }, text: "hi" }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendChatActionSpy).toHaveBeenCalledWith(42, "typing", undefined); - }); - - it("accepts group messages when mentionPatterns match (without @botUsername)", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - agents: { - defaults: { - envelopeTimezone: "utc", - }, - }, - identity: { name: "Bert" }, - messages: { groupChat: { mentionPatterns: ["\\bbert\\b"] } }, - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 7, type: "group", title: "Test Group" }, - text: "bert: introduce yourself", - date: 1736380800, - message_id: 1, - from: { id: 9, first_name: "Ada" }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expectInboundContextContract(payload); - expect(payload.WasMentioned).toBe(true); - const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z")); - const timestampPattern = escapeRegExp(expectedTimestamp); - expect(payload.Body).toMatch( - new RegExp(`^\\[Telegram Test Group id:7 (\\+\\d+[smhd] )?${timestampPattern}\\]`), - ); - expect(payload.SenderName).toBe("Ada"); - expect(payload.SenderId).toBe("9"); - }); - it("includes sender identity in group envelope headers", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; @@ -768,155 +497,6 @@ describe("createTelegramBot", () => { expect(payload.SenderUsername).toBe("ada"); }); - it("reacts to mention-gated group messages when ackReaction is enabled", async () => { - onSpy.mockReset(); - setMessageReactionSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - messages: { - ackReaction: "👀", - ackReactionScope: "group-mentions", - groupChat: { mentionPatterns: ["\\bbert\\b"] }, - }, - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 7, type: "group", title: "Test Group" }, - text: "bert hello", - date: 1736380800, - message_id: 123, - from: { id: 9, first_name: "Ada" }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(setMessageReactionSpy).toHaveBeenCalledWith(7, 123, [{ type: "emoji", emoji: "👀" }]); - }); - - it("clears native commands when disabled", () => { - loadConfig.mockReturnValue({ - commands: { native: false }, - }); - - createTelegramBot({ token: "tok" }); - - expect(setMyCommandsSpy).toHaveBeenCalledWith([]); - }); - - it("skips group messages when requireMention is enabled and no mention matches", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - messages: { groupChat: { mentionPatterns: ["\\bbert\\b"] } }, - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 7, type: "group", title: "Test Group" }, - text: "hello everyone", - date: 1736380800, - message_id: 2, - from: { id: 9, first_name: "Ada" }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).not.toHaveBeenCalled(); - }); - - it("allows group messages when requireMention is enabled but mentions cannot be detected", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - messages: { groupChat: { mentionPatterns: [] } }, - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 7, type: "group", title: "Test Group" }, - text: "hello everyone", - date: 1736380800, - message_id: 3, - from: { id: 9, first_name: "Ada" }, - }, - me: {}, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.WasMentioned).toBe(false); - }); - - it("includes reply-to context when a Telegram reply is received", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 7, type: "private" }, - text: "Sure, see below", - date: 1736380800, - reply_to_message: { - message_id: 9001, - text: "Can you summarize this?", - from: { first_name: "Ada" }, - }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.Body).toContain("[Replying to Ada id:9001]"); - expect(payload.Body).toContain("Can you summarize this?"); - expect(payload.ReplyToId).toBe("9001"); - expect(payload.ReplyToBody).toBe("Can you summarize this?"); - expect(payload.ReplyToSender).toBe("Ada"); - }); - it("uses quote text when a Telegram partial reply is received", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); @@ -1020,177 +600,6 @@ describe("createTelegramBot", () => { expect(payload.ReplyToSender).toBe("Ada"); }); - it("sends replies without native reply threading", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - replySpy.mockResolvedValue({ text: "a".repeat(4500) }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - await handler({ - message: { - chat: { id: 5, type: "private" }, - text: "hi", - date: 1736380800, - message_id: 101, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendMessageSpy.mock.calls.length).toBeGreaterThan(1); - for (const call of sendMessageSpy.mock.calls) { - expect(call[2]?.reply_to_message_id).toBeUndefined(); - } - }); - - it("honors replyToMode=first for threaded replies", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - replySpy.mockResolvedValue({ - text: "a".repeat(4500), - replyToId: "101", - }); - - createTelegramBot({ token: "tok", replyToMode: "first" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - await handler({ - message: { - chat: { id: 5, type: "private" }, - text: "hi", - date: 1736380800, - message_id: 101, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendMessageSpy.mock.calls.length).toBeGreaterThan(1); - const [first, ...rest] = sendMessageSpy.mock.calls; - expect(first?.[2]?.reply_to_message_id).toBe(101); - for (const call of rest) { - expect(call[2]?.reply_to_message_id).toBeUndefined(); - } - }); - - it("prefixes final replies with responsePrefix", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - replySpy.mockResolvedValue({ text: "final reply" }); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - messages: { responsePrefix: "PFX" }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - await handler({ - message: { - chat: { id: 5, type: "private" }, - text: "hi", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(sendMessageSpy.mock.calls[0][1]).toBe("PFX final reply"); - }); - - it("honors replyToMode=all for threaded replies", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - replySpy.mockResolvedValue({ - text: "a".repeat(4500), - replyToId: "101", - }); - - createTelegramBot({ token: "tok", replyToMode: "all" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - await handler({ - message: { - chat: { id: 5, type: "private" }, - text: "hi", - date: 1736380800, - message_id: 101, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendMessageSpy.mock.calls.length).toBeGreaterThan(1); - for (const call of sendMessageSpy.mock.calls) { - expect(call[2]?.reply_to_message_id).toBe(101); - } - }); - - it("blocks group messages when telegram.groups is set without a wildcard", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groups: { - "123": { requireMention: false }, - }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 456, type: "group", title: "Ops" }, - text: "@openclaw_bot hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).not.toHaveBeenCalled(); - }); - - it("skips group messages without mention when requireMention is enabled", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { groups: { "*": { requireMention: true } } }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 123, type: "group", title: "Dev Chat" }, - text: "hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).not.toHaveBeenCalled(); - }); - it("accepts group replies to the bot without explicit mention when requireMention is enabled", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; @@ -1224,174 +633,6 @@ describe("createTelegramBot", () => { expect(payload.WasMentioned).toBe(true); }); - it("honors routed group activation from session store", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - const storeDir = createTempDir("openclaw-telegram-"); - const storePath = path.join(storeDir, "sessions.json"); - fs.writeFileSync( - storePath, - JSON.stringify({ - "agent:ops:telegram:group:123": { groupActivation: "always" }, - }), - "utf-8", - ); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - bindings: [ - { - agentId: "ops", - match: { - channel: "telegram", - peer: { kind: "group", id: "123" }, - }, - }, - ], - session: { store: storePath }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 123, type: "group", title: "Routing" }, - text: "hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("routes DMs by telegram accountId binding", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { - accounts: { - opie: { - botToken: "tok-opie", - dmPolicy: "open", - }, - }, - }, - }, - bindings: [ - { - agentId: "opie", - match: { channel: "telegram", accountId: "opie" }, - }, - ], - }); - - createTelegramBot({ token: "tok", accountId: "opie" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 123, type: "private" }, - from: { id: 999, username: "testuser" }, - text: "hello", - date: 1736380800, - message_id: 42, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.AccountId).toBe("opie"); - expect(payload.SessionKey).toBe("agent:opie:main"); - }); - - it("allows per-group requireMention override", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { - "*": { requireMention: true }, - "123": { requireMention: false }, - }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 123, type: "group", title: "Dev Chat" }, - text: "hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("allows per-topic requireMention override", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { - "*": { requireMention: true }, - "-1001234567890": { - requireMention: true, - topics: { - "99": { requireMention: false }, - }, - }, - }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { - id: -1001234567890, - type: "supergroup", - title: "Forum Group", - is_forum: true, - }, - text: "hello", - date: 1736380800, - message_thread_id: 99, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - it("inherits group allowlist + requireMention in topics", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; @@ -1479,254 +720,6 @@ describe("createTelegramBot", () => { expect(replySpy).toHaveBeenCalledTimes(0); }); - it("honors groups default when no explicit group override exists", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: false } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 456, type: "group", title: "Ops" }, - text: "hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("does not block group messages when bot username is unknown", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: true } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 789, type: "group", title: "No Me" }, - text: "hello", - date: 1736380800, - }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("sends GIF replies as animations", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - replySpy.mockResolvedValueOnce({ - text: "caption", - mediaUrl: "https://example.com/fun", - }); - - loadWebMedia.mockResolvedValueOnce({ - buffer: Buffer.from("GIF89a"), - contentType: "image/gif", - fileName: "fun.gif", - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 1234, type: "private" }, - text: "hello world", - date: 1736380800, - message_id: 5, - from: { first_name: "Ada" }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendAnimationSpy).toHaveBeenCalledTimes(1); - expect(sendAnimationSpy).toHaveBeenCalledWith("1234", expect.anything(), { - caption: "caption", - parse_mode: "HTML", - reply_to_message_id: undefined, - }); - expect(sendPhotoSpy).not.toHaveBeenCalled(); - }); - - // groupPolicy tests - it("blocks all group messages when groupPolicy is 'disabled'", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "disabled", - allowFrom: ["123456789"], - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: -100123456789, type: "group", title: "Test Group" }, - from: { id: 123456789, username: "testuser" }, - text: "@openclaw_bot hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - // Should NOT call getReplyFromConfig because groupPolicy is disabled - expect(replySpy).not.toHaveBeenCalled(); - }); - - it("allows all group messages when groupPolicy is 'open'", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: false } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: -100123456789, type: "group", title: "Test Group" }, - from: { id: 999999, username: "random" }, // Random sender - text: "hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("allows direct messages regardless of groupPolicy", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "disabled", // Even with disabled, DMs should work - allowFrom: ["123456789"], - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 123456789, type: "private" }, // Direct message - from: { id: 123456789, username: "testuser" }, - text: "hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("allows direct messages with tg/Telegram-prefixed allowFrom entries", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - allowFrom: [" TG:123456789 "], - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 123456789, type: "private" }, // Direct message - from: { id: 123456789, username: "testuser" }, - text: "hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("blocks group messages when groupPolicy allowlist has no groupAllowFrom", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "allowlist", - groups: { "*": { requireMention: false } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: -100123456789, type: "group", title: "Test Group" }, - from: { id: 123456789, username: "testuser" }, - text: "hello", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).not.toHaveBeenCalled(); - }); - it("allows group messages for per-group groupPolicy open override (global groupPolicy allowlist)", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; @@ -1798,312 +791,6 @@ describe("createTelegramBot", () => { expect(replySpy).not.toHaveBeenCalled(); }); - - it("allows control commands with TG-prefixed groupAllowFrom entries", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "allowlist", - groupAllowFrom: [" TG:123456789 "], - groups: { "*": { requireMention: true } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: -100123456789, type: "group", title: "Test Group" }, - from: { id: 123456789, username: "testuser" }, - text: "/status", - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("isolates forum topic sessions and carries thread metadata", async () => { - onSpy.mockReset(); - sendChatActionSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: false } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { - id: -1001234567890, - type: "supergroup", - title: "Forum Group", - is_forum: true, - }, - from: { id: 12345, username: "testuser" }, - text: "hello", - date: 1736380800, - message_id: 42, - message_thread_id: 99, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.SessionKey).toContain("telegram:group:-1001234567890:topic:99"); - expect(payload.From).toBe("telegram:group:-1001234567890:topic:99"); - expect(payload.MessageThreadId).toBe(99); - expect(payload.IsForum).toBe(true); - expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", { - message_thread_id: 99, - }); - }); - - it("falls back to General topic thread id for typing in forums", async () => { - onSpy.mockReset(); - sendChatActionSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: false } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { - id: -1001234567890, - type: "supergroup", - title: "Forum Group", - is_forum: true, - }, - from: { id: 12345, username: "testuser" }, - text: "hello", - date: 1736380800, - message_id: 42, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", { - message_thread_id: 1, - }); - }); - - it("routes General topic replies using thread id 1", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - replySpy.mockResolvedValue({ text: "response" }); - - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: false } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { - id: -1001234567890, - type: "supergroup", - title: "Forum Group", - is_forum: true, - }, - from: { id: 12345, username: "testuser" }, - text: "hello", - date: 1736380800, - message_id: 42, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - const sendParams = sendMessageSpy.mock.calls[0]?.[2] as { message_thread_id?: number }; - expect(sendParams?.message_thread_id).toBeUndefined(); - }); - - it("applies topic skill filters and system prompts", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { - "-1001234567890": { - requireMention: false, - systemPrompt: "Group prompt", - skills: ["group-skill"], - topics: { - "99": { - skills: [], - systemPrompt: "Topic prompt", - }, - }, - }, - }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { - id: -1001234567890, - type: "supergroup", - title: "Forum Group", - is_forum: true, - }, - from: { id: 12345, username: "testuser" }, - text: "hello", - date: 1736380800, - message_id: 42, - message_thread_id: 99, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.GroupSystemPrompt).toBe("Group prompt\n\nTopic prompt"); - const opts = replySpy.mock.calls[0][1]; - expect(opts?.skillFilter).toEqual([]); - }); - - it("passes message_thread_id to topic replies", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - commandSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - replySpy.mockResolvedValue({ text: "response" }); - - loadConfig.mockReturnValue({ - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: false } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { - id: -1001234567890, - type: "supergroup", - title: "Forum Group", - is_forum: true, - }, - from: { id: 12345, username: "testuser" }, - text: "hello", - date: 1736380800, - message_id: 42, - message_thread_id: 99, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendMessageSpy).toHaveBeenCalledWith( - "-1001234567890", - expect.any(String), - expect.objectContaining({ message_thread_id: 99 }), - ); - }); - - it("threads native command replies inside topics", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - commandSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - replySpy.mockResolvedValue({ text: "response" }); - - loadConfig.mockReturnValue({ - commands: { native: true }, - channels: { - telegram: { - dmPolicy: "open", - allowFrom: ["*"], - groups: { "*": { requireMention: false } }, - }, - }, - }); - - createTelegramBot({ token: "tok" }); - expect(commandSpy).toHaveBeenCalled(); - const handler = commandSpy.mock.calls[0][1] as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { - id: -1001234567890, - type: "supergroup", - title: "Forum Group", - is_forum: true, - }, - from: { id: 12345, username: "testuser" }, - text: "/status", - date: 1736380800, - message_id: 42, - message_thread_id: 99, - }, - match: "", - }); - - expect(sendMessageSpy).toHaveBeenCalledWith( - "-1001234567890", - expect.any(String), - expect.objectContaining({ message_thread_id: 99 }), - ); - }); it("sets command target session key for dm topic commands", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); @@ -2235,170 +922,6 @@ describe("createTelegramBot", () => { ); }); - it("skips tool summaries for native slash commands", async () => { - onSpy.mockReset(); - sendMessageSpy.mockReset(); - commandSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - replySpy.mockImplementation(async (_ctx, opts) => { - await opts?.onToolResult?.({ text: "tool update" }); - return { text: "final reply" }; - }); - - loadConfig.mockReturnValue({ - commands: { native: true }, - channels: { - telegram: { - dmPolicy: "open", - allowFrom: ["*"], - }, - }, - }); - - createTelegramBot({ token: "tok" }); - const verboseHandler = commandSpy.mock.calls.find((call) => call[0] === "verbose")?.[1] as - | ((ctx: Record) => Promise) - | undefined; - if (!verboseHandler) { - throw new Error("verbose command handler missing"); - } - - await verboseHandler({ - message: { - chat: { id: 12345, type: "private" }, - from: { id: 12345, username: "testuser" }, - text: "/verbose on", - date: 1736380800, - message_id: 42, - }, - match: "on", - }); - - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(sendMessageSpy.mock.calls[0]?.[1]).toContain("final reply"); - }); - - it("dedupes duplicate message updates by update_id", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - const ctx = { - update: { update_id: 111 }, - message: { - chat: { id: 123, type: "private" }, - from: { id: 456, username: "testuser" }, - text: "hello", - date: 1736380800, - message_id: 42, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }; - - await handler(ctx); - await handler(ctx); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("dedupes duplicate callback_query updates by update_id", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("callback_query") as ( - ctx: Record, - ) => Promise; - - const ctx = { - update: { update_id: 222 }, - callbackQuery: { - id: "cb-1", - data: "ping", - from: { id: 789, username: "testuser" }, - message: { - chat: { id: 123, type: "private" }, - date: 1736380800, - message_id: 9001, - }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({}), - }; - - await handler(ctx); - await handler(ctx); - - expect(replySpy).toHaveBeenCalledTimes(1); - }); - - it("allows distinct callback_query ids without update_id", async () => { - onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; - replySpy.mockReset(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("callback_query") as ( - ctx: Record, - ) => Promise; - - await handler({ - callbackQuery: { - id: "cb-1", - data: "ping", - from: { id: 789, username: "testuser" }, - message: { - chat: { id: 123, type: "private" }, - date: 1736380800, - message_id: 9001, - }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({}), - }); - - await handler({ - callbackQuery: { - id: "cb-2", - data: "ping", - from: { id: 789, username: "testuser" }, - message: { - chat: { id: 123, type: "private" }, - date: 1736380800, - message_id: 9001, - }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({}), - }); - - expect(replySpy).toHaveBeenCalledTimes(2); - }); - it("registers message_reaction handler", () => { onSpy.mockReset(); createTelegramBot({ token: "tok" });