diff --git a/scripts/dev/test-device-pair-telegram.ts b/scripts/dev/test-device-pair-telegram.ts index b5966522839c..d78eb8e66352 100644 --- a/scripts/dev/test-device-pair-telegram.ts +++ b/scripts/dev/test-device-pair-telegram.ts @@ -1,9 +1,37 @@ // Test Device Pair Telegram script supports OpenClaw repository automation. -import { sendMessageTelegram } from "../../extensions/telegram/runtime-api.js"; +import { pathToFileURL } from "node:url"; import { getRuntimeConfig } from "../../src/config/config.js"; import { matchPluginCommand, executePluginCommand } from "../../src/plugins/commands.js"; import { loadOpenClawPlugins } from "../../src/plugins/loader.js"; +type SendMessageTelegram = ( + chatId: string, + text: string, + options: { + accountId?: string; + cfg?: ReturnType; + }, +) => Promise<{ chatId?: string; messageId?: string }>; + +type DevicePairTelegramDeps = { + executePluginCommand: typeof executePluginCommand; + getRuntimeConfig: typeof getRuntimeConfig; + loadOpenClawPlugins: typeof loadOpenClawPlugins; + matchPluginCommand: typeof matchPluginCommand; + sendMessageTelegram: SendMessageTelegram; +}; + +type DevicePairTelegramResult = { + accountId?: string; + chatId: string; + messageId?: string; + sent: boolean; +}; + +class UsageError extends Error { + readonly exitCode = 1; +} + function writeStdoutLine(...parts: string[]): void { process.stdout.write(`${parts.join(" ")}\n`); } @@ -12,8 +40,7 @@ function writeStderrLine(message: string): void { process.stderr.write(`${message}\n`); } -const args = process.argv.slice(2); -const getArg = (flag: string, short?: string) => { +function readArg(args: string[], flag: string, short?: string): string | undefined { const idx = args.indexOf(flag); if (idx !== -1 && idx + 1 < args.length) { return args[idx + 1]; @@ -25,44 +52,102 @@ const getArg = (flag: string, short?: string) => { } } return undefined; -}; +} -const chatId = getArg("--chat", "-c"); -const accountId = getArg("--account", "-a"); -if (!chatId) { - writeStderrLine( +function usage(): string { + return [ "Usage: bun scripts/dev/test-device-pair-telegram.ts --chat [--account ]", - ); - process.exit(1); + ].join("\n"); } -const cfg = getRuntimeConfig(); -loadOpenClawPlugins({ config: cfg }); - -const match = matchPluginCommand("/pair"); -if (!match) { - writeStderrLine("/pair plugin command not registered."); - process.exit(1); +async function loadTelegramRuntimeSendMessage(): Promise { + const specifier = "../../extensions/telegram/runtime-api.js"; + const runtime = (await import(specifier)) as { sendMessageTelegram?: SendMessageTelegram }; + if (typeof runtime.sendMessageTelegram !== "function") { + throw new Error("Telegram runtime-api.js did not export sendMessageTelegram"); + } + return runtime.sendMessageTelegram; } -const result = await executePluginCommand({ - command: match.command, - args: match.args, - senderId: chatId, - channel: "telegram", - channelId: "telegram", - isAuthorizedSender: true, - commandBody: "/pair", - config: cfg, - from: `telegram:${chatId}`, - to: `telegram:${chatId}`, - accountId, -}); +function createDefaultDeps(): DevicePairTelegramDeps { + return { + executePluginCommand, + getRuntimeConfig, + loadOpenClawPlugins, + matchPluginCommand, + sendMessageTelegram: async (...args) => { + const sendMessageTelegram = await loadTelegramRuntimeSendMessage(); + return await sendMessageTelegram(...args); + }, + }; +} -if (result.text) { - await sendMessageTelegram(chatId, result.text, { +async function runDevicePairTelegram( + args = process.argv.slice(2), + deps: DevicePairTelegramDeps = createDefaultDeps(), +): Promise { + const chatId = readArg(args, "--chat", "-c"); + const accountId = readArg(args, "--account", "-a"); + if (!chatId) { + throw new UsageError(usage()); + } + + const cfg = deps.getRuntimeConfig(); + deps.loadOpenClawPlugins({ config: cfg }); + + const match = deps.matchPluginCommand("/pair", { channel: "telegram" }); + if (!match) { + throw new Error("/pair plugin command not registered."); + } + + const result = await deps.executePluginCommand({ + command: match.command, + args: match.args, + senderId: chatId, + channel: "telegram", + channelId: "telegram", + isAuthorizedSender: true, + commandBody: "/pair", + config: cfg, + from: `telegram:${chatId}`, + to: `telegram:${chatId}`, accountId, }); + + if (!result.text) { + return { accountId, chatId, sent: false }; + } + + const sent = await deps.sendMessageTelegram(chatId, result.text, { + accountId, + cfg, + }); + + return { + accountId, + chatId: sent.chatId ?? chatId, + messageId: sent.messageId, + sent: true, + }; } -writeStdoutLine("Sent split /pair messages to", chatId, accountId ? `(${accountId})` : ""); +async function main(): Promise { + try { + const result = await runDevicePairTelegram(); + writeStdoutLine( + "Sent split /pair messages to", + result.chatId, + result.accountId ? `(${result.accountId})` : "", + result.messageId ? `message=${result.messageId}` : "", + ); + } catch (error) { + writeStderrLine(error instanceof Error ? error.message : String(error)); + process.exitCode = error instanceof UsageError ? error.exitCode : 1; + } +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + await main(); +} + +export { runDevicePairTelegram }; diff --git a/test/scripts/test-device-pair-telegram.test.ts b/test/scripts/test-device-pair-telegram.test.ts new file mode 100644 index 000000000000..c2085573ad04 --- /dev/null +++ b/test/scripts/test-device-pair-telegram.test.ts @@ -0,0 +1,53 @@ +// Test Device Pair Telegram tests cover the dev Telegram pairing smoke helper. +import { pathToFileURL } from "node:url"; +import { describe, expect, it, vi } from "vitest"; +import { runDevicePairTelegram } from "../../scripts/dev/test-device-pair-telegram.ts"; + +const scriptUrl = pathToFileURL("scripts/dev/test-device-pair-telegram.ts").href; + +describe("scripts/dev/test-device-pair-telegram.ts", () => { + it("loads without resolving the Telegram runtime sidecar", async () => { + await expect(import(`${scriptUrl}?case=load-${Date.now()}`)).resolves.toBeDefined(); + }); + + it("sends the generated /pair reply through the injected Telegram runtime", async () => { + const cfg = { channels: { telegram: { enabled: true } } }; + const loadOpenClawPlugins = vi.fn(); + const executePluginCommand = vi.fn(async () => ({ text: "pair this device" })); + const sendMessageTelegram = vi.fn(async () => ({ + chatId: "chat-123", + messageId: "message-456", + })); + + const result = await runDevicePairTelegram(["--chat", "chat-123", "--account", "main"], { + executePluginCommand, + getRuntimeConfig: () => cfg, + loadOpenClawPlugins, + matchPluginCommand: () => ({ args: "from-match", command: { name: "pair" } as never }), + sendMessageTelegram, + }); + + expect(loadOpenClawPlugins).toHaveBeenCalledWith({ config: cfg }); + expect(executePluginCommand).toHaveBeenCalledWith( + expect.objectContaining({ + accountId: "main", + args: "from-match", + channel: "telegram", + commandBody: "/pair", + from: "telegram:chat-123", + senderId: "chat-123", + to: "telegram:chat-123", + }), + ); + expect(sendMessageTelegram).toHaveBeenCalledWith("chat-123", "pair this device", { + accountId: "main", + cfg, + }); + expect(result).toEqual({ + accountId: "main", + chatId: "chat-123", + messageId: "message-456", + sent: true, + }); + }); +});