|
1 | 1 | process.env.TZ = "UTC"; |
2 | 2 | import { describe, it } from "node:test"; |
3 | 3 | import assert from "node:assert/strict"; |
4 | | -import { resolveBinding, isAuthorized, sessionKey, isImageMimeType, imageExtensionForMime, buildSourcePrefix, shouldRespondInGroup, BOT_COMMANDS, isStaleMessage, buildReplyContext, buildForwardContext, extensionForDocument, formatFileSize, formatDocumentMeta, buildReactionContext, AUTO_RETRY_OPTIONS, extractMediaInfo, extensionForMedia, formatMediaMeta } from "../telegram-bot.js"; |
5 | | -import type { TelegramBinding } from "../types.js"; |
| 4 | +import { resolveBinding, isAuthorized, sessionKey, isImageMimeType, imageExtensionForMime, buildSourcePrefix, shouldRespondInGroup, BOT_COMMANDS, isStaleMessage, buildReplyContext, buildForwardContext, extensionForDocument, formatFileSize, formatDocumentMeta, buildReactionContext, AUTO_RETRY_OPTIONS, extractMediaInfo, extensionForMedia, formatMediaMeta, createTelegramBot } from "../telegram-bot.js"; |
| 5 | +import type { TelegramBinding, BotConfig } from "../types.js"; |
| 6 | +import type { SessionManager } from "../session-manager.js"; |
6 | 7 |
|
7 | 8 | const testBindings: TelegramBinding[] = [ |
8 | 9 | { chatId: 111111111, agentId: "main", kind: "dm", label: "User1 DM" }, |
@@ -191,9 +192,9 @@ describe("isImageMimeType", () => { |
191 | 192 | }); |
192 | 193 |
|
193 | 194 | describe("BOT_COMMANDS", () => { |
194 | | - it("contains start, reset, and status commands", () => { |
| 195 | + it("contains start, reconnect, clean, and status commands", () => { |
195 | 196 | const names = BOT_COMMANDS.map((c) => c.command); |
196 | | - assert.deepStrictEqual(names, ["start", "reset", "status"]); |
| 197 | + assert.deepStrictEqual(names, ["start", "reconnect", "clean", "status"]); |
197 | 198 | }); |
198 | 199 |
|
199 | 200 | it("each command has a non-empty description", () => { |
@@ -1257,3 +1258,93 @@ describe("formatMediaMeta", () => { |
1257 | 1258 | ); |
1258 | 1259 | }); |
1259 | 1260 | }); |
| 1261 | + |
| 1262 | +describe("command handler wiring", () => { |
| 1263 | + const testChatId = 111111111; |
| 1264 | + |
| 1265 | + const handlerConfig: BotConfig = { |
| 1266 | + telegramToken: "test:fake-token-for-handler-tests", |
| 1267 | + agents: { |
| 1268 | + main: { id: "main", workspaceCwd: "/tmp/test", model: "claude-opus-4-6" }, |
| 1269 | + }, |
| 1270 | + bindings: [ |
| 1271 | + { chatId: testChatId, agentId: "main", kind: "dm" as const }, |
| 1272 | + ], |
| 1273 | + sessionDefaults: { |
| 1274 | + idleTimeoutMs: 60000, |
| 1275 | + maxConcurrentSessions: 2, |
| 1276 | + maxMessageAgeMs: 300000, |
| 1277 | + requireMention: false, |
| 1278 | + }, |
| 1279 | + }; |
| 1280 | + |
| 1281 | + function createMockSessionManager(): SessionManager & { calls: string[] } { |
| 1282 | + const calls: string[] = []; |
| 1283 | + return { |
| 1284 | + calls, |
| 1285 | + closeSession: async (_chatId: string) => { calls.push("closeSession"); }, |
| 1286 | + destroySession: async (_chatId: string) => { calls.push("destroySession"); }, |
| 1287 | + sendSessionMessage: () => { throw new Error("unexpected"); }, |
| 1288 | + getOrCreateSession: async () => { throw new Error("unexpected"); }, |
| 1289 | + closeAll: async () => {}, |
| 1290 | + resolveStoredSession: () => ({ resume: false }), |
| 1291 | + activeCount: () => 0, |
| 1292 | + getActiveSession: () => undefined, |
| 1293 | + isActive: () => false, |
| 1294 | + } as unknown as SessionManager & { calls: string[] }; |
| 1295 | + } |
| 1296 | + |
| 1297 | + function makeCommandUpdate(command: string, updateId: number) { |
| 1298 | + const text = `/${command}`; |
| 1299 | + return { |
| 1300 | + update_id: updateId, |
| 1301 | + message: { |
| 1302 | + message_id: updateId, |
| 1303 | + from: { id: testChatId, is_bot: false, first_name: "Test" }, |
| 1304 | + chat: { id: testChatId, type: "private" as const, first_name: "Test" }, |
| 1305 | + date: Math.floor(Date.now() / 1000), |
| 1306 | + text, |
| 1307 | + entities: [{ offset: 0, length: text.length, type: "bot_command" as const }], |
| 1308 | + }, |
| 1309 | + }; |
| 1310 | + } |
| 1311 | + |
| 1312 | + function initBot(mockSM: SessionManager) { |
| 1313 | + const { bot } = createTelegramBot(handlerConfig, mockSM); |
| 1314 | + // Intercept all API calls so nothing reaches Telegram |
| 1315 | + bot.api.config.use(async (_prev, _method, _payload) => ({ ok: true, result: true } as any)); |
| 1316 | + // Provide bot info so handleUpdate works without calling getMe |
| 1317 | + bot.botInfo = { |
| 1318 | + id: 999, |
| 1319 | + is_bot: true, |
| 1320 | + first_name: "TestBot", |
| 1321 | + username: "test_bot", |
| 1322 | + can_join_groups: false, |
| 1323 | + can_read_all_group_messages: false, |
| 1324 | + supports_inline_queries: false, |
| 1325 | + can_connect_to_business: false, |
| 1326 | + has_main_web_app: false, |
| 1327 | + has_topics_enabled: false, |
| 1328 | + allows_users_to_create_topics: false, |
| 1329 | + }; |
| 1330 | + return bot; |
| 1331 | + } |
| 1332 | + |
| 1333 | + it("/reconnect calls closeSession (not destroySession)", async () => { |
| 1334 | + const mockSM = createMockSessionManager(); |
| 1335 | + const bot = initBot(mockSM); |
| 1336 | + |
| 1337 | + await bot.handleUpdate(makeCommandUpdate("reconnect", 1)); |
| 1338 | + assert.ok(mockSM.calls.includes("closeSession"), "/reconnect should call closeSession"); |
| 1339 | + assert.ok(!mockSM.calls.includes("destroySession"), "/reconnect should NOT call destroySession"); |
| 1340 | + }); |
| 1341 | + |
| 1342 | + it("/clean calls destroySession (not closeSession directly)", async () => { |
| 1343 | + const mockSM = createMockSessionManager(); |
| 1344 | + const bot = initBot(mockSM); |
| 1345 | + |
| 1346 | + await bot.handleUpdate(makeCommandUpdate("clean", 2)); |
| 1347 | + assert.ok(mockSM.calls.includes("destroySession"), "/clean should call destroySession"); |
| 1348 | + assert.ok(!mockSM.calls.includes("closeSession"), "/clean handler calls destroySession, not closeSession directly"); |
| 1349 | + }); |
| 1350 | +}); |
0 commit comments