adaptoclaw 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/dist/channels/telegram.test.d.ts +2 -0
  2. package/dist/channels/telegram.test.d.ts.map +1 -0
  3. package/dist/channels/telegram.test.js +640 -0
  4. package/dist/channels/telegram.test.js.map +1 -0
  5. package/dist/channels/whatsapp.test.d.ts +2 -0
  6. package/dist/channels/whatsapp.test.d.ts.map +1 -0
  7. package/dist/channels/whatsapp.test.js +664 -0
  8. package/dist/channels/whatsapp.test.js.map +1 -0
  9. package/dist/container-runner.d.ts.map +1 -1
  10. package/dist/container-runner.js +17 -1
  11. package/dist/container-runner.js.map +1 -1
  12. package/dist/container-runner.test.d.ts +2 -0
  13. package/dist/container-runner.test.d.ts.map +1 -0
  14. package/dist/container-runner.test.js +152 -0
  15. package/dist/container-runner.test.js.map +1 -0
  16. package/dist/container-runtime.test.d.ts +2 -0
  17. package/dist/container-runtime.test.d.ts.map +1 -0
  18. package/dist/container-runtime.test.js +90 -0
  19. package/dist/container-runtime.test.js.map +1 -0
  20. package/dist/core/engine.d.ts +3 -0
  21. package/dist/core/engine.d.ts.map +1 -1
  22. package/dist/core/engine.js +50 -1
  23. package/dist/core/engine.js.map +1 -1
  24. package/dist/create.test.d.ts +2 -0
  25. package/dist/create.test.d.ts.map +1 -0
  26. package/dist/create.test.js +47 -0
  27. package/dist/create.test.js.map +1 -0
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +33 -0
  30. package/dist/db.js.map +1 -1
  31. package/dist/db.test.d.ts +2 -0
  32. package/dist/db.test.d.ts.map +1 -0
  33. package/dist/db.test.js +262 -0
  34. package/dist/db.test.js.map +1 -0
  35. package/dist/formatting.test.d.ts +2 -0
  36. package/dist/formatting.test.d.ts.map +1 -0
  37. package/dist/formatting.test.js +228 -0
  38. package/dist/formatting.test.js.map +1 -0
  39. package/dist/group-folder.test.d.ts +2 -0
  40. package/dist/group-folder.test.d.ts.map +1 -0
  41. package/dist/group-folder.test.js +29 -0
  42. package/dist/group-folder.test.js.map +1 -0
  43. package/dist/group-queue.test.d.ts +2 -0
  44. package/dist/group-queue.test.d.ts.map +1 -0
  45. package/dist/group-queue.test.js +314 -0
  46. package/dist/group-queue.test.js.map +1 -0
  47. package/dist/health.test.d.ts +2 -0
  48. package/dist/health.test.d.ts.map +1 -0
  49. package/dist/health.test.js +30 -0
  50. package/dist/health.test.js.map +1 -0
  51. package/dist/hooks/runner.test.d.ts +2 -0
  52. package/dist/hooks/runner.test.d.ts.map +1 -0
  53. package/dist/hooks/runner.test.js +42 -0
  54. package/dist/hooks/runner.test.js.map +1 -0
  55. package/dist/index.d.ts +1 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +2 -33
  58. package/dist/index.js.map +1 -1
  59. package/dist/ipc-auth.test.d.ts +2 -0
  60. package/dist/ipc-auth.test.d.ts.map +1 -0
  61. package/dist/ipc-auth.test.js +434 -0
  62. package/dist/ipc-auth.test.js.map +1 -0
  63. package/dist/media-cleanup.test.d.ts +2 -0
  64. package/dist/media-cleanup.test.d.ts.map +1 -0
  65. package/dist/media-cleanup.test.js +65 -0
  66. package/dist/media-cleanup.test.js.map +1 -0
  67. package/dist/media.test.d.ts +2 -0
  68. package/dist/media.test.d.ts.map +1 -0
  69. package/dist/media.test.js +38 -0
  70. package/dist/media.test.js.map +1 -0
  71. package/dist/mining/data-pipeline.d.ts +17 -0
  72. package/dist/mining/data-pipeline.d.ts.map +1 -0
  73. package/dist/mining/data-pipeline.js +108 -0
  74. package/dist/mining/data-pipeline.js.map +1 -0
  75. package/dist/mining/data-pipeline.test.d.ts +2 -0
  76. package/dist/mining/data-pipeline.test.d.ts.map +1 -0
  77. package/dist/mining/data-pipeline.test.js +52 -0
  78. package/dist/mining/data-pipeline.test.js.map +1 -0
  79. package/dist/mining/index.d.ts +8 -0
  80. package/dist/mining/index.d.ts.map +1 -0
  81. package/dist/mining/index.js +8 -0
  82. package/dist/mining/index.js.map +1 -0
  83. package/dist/mining/mining-mcp.d.ts +6 -0
  84. package/dist/mining/mining-mcp.d.ts.map +1 -0
  85. package/dist/mining/mining-mcp.js +192 -0
  86. package/dist/mining/mining-mcp.js.map +1 -0
  87. package/dist/mining/mining-mcp.test.d.ts +2 -0
  88. package/dist/mining/mining-mcp.test.d.ts.map +1 -0
  89. package/dist/mining/mining-mcp.test.js +30 -0
  90. package/dist/mining/mining-mcp.test.js.map +1 -0
  91. package/dist/mining/parsers/egov-legal-entities.d.ts +19 -0
  92. package/dist/mining/parsers/egov-legal-entities.d.ts.map +1 -0
  93. package/dist/mining/parsers/egov-legal-entities.js +116 -0
  94. package/dist/mining/parsers/egov-legal-entities.js.map +1 -0
  95. package/dist/mining/parsers/google-maps.d.ts +21 -0
  96. package/dist/mining/parsers/google-maps.d.ts.map +1 -0
  97. package/dist/mining/parsers/google-maps.js +109 -0
  98. package/dist/mining/parsers/google-maps.js.map +1 -0
  99. package/dist/mining/parsers/goszakup.d.ts +14 -0
  100. package/dist/mining/parsers/goszakup.d.ts.map +1 -0
  101. package/dist/mining/parsers/goszakup.js +104 -0
  102. package/dist/mining/parsers/goszakup.js.map +1 -0
  103. package/dist/mining/parsers/index.d.ts +3 -0
  104. package/dist/mining/parsers/index.d.ts.map +1 -0
  105. package/dist/mining/parsers/index.js +13 -0
  106. package/dist/mining/parsers/index.js.map +1 -0
  107. package/dist/mining/parsers/parsers.test.d.ts +2 -0
  108. package/dist/mining/parsers/parsers.test.d.ts.map +1 -0
  109. package/dist/mining/parsers/parsers.test.js +924 -0
  110. package/dist/mining/parsers/parsers.test.js.map +1 -0
  111. package/dist/mining/parsers/twogis.d.ts +31 -0
  112. package/dist/mining/parsers/twogis.d.ts.map +1 -0
  113. package/dist/mining/parsers/twogis.js +121 -0
  114. package/dist/mining/parsers/twogis.js.map +1 -0
  115. package/dist/mining/rqlite-client.d.ts +17 -0
  116. package/dist/mining/rqlite-client.d.ts.map +1 -0
  117. package/dist/mining/rqlite-client.js +61 -0
  118. package/dist/mining/rqlite-client.js.map +1 -0
  119. package/dist/mining/rqlite-client.test.d.ts +2 -0
  120. package/dist/mining/rqlite-client.test.d.ts.map +1 -0
  121. package/dist/mining/rqlite-client.test.js +15 -0
  122. package/dist/mining/rqlite-client.test.js.map +1 -0
  123. package/dist/mining/schema.d.ts +3 -0
  124. package/dist/mining/schema.d.ts.map +1 -0
  125. package/dist/mining/schema.js +73 -0
  126. package/dist/mining/schema.js.map +1 -0
  127. package/dist/mining/schema.test.d.ts +2 -0
  128. package/dist/mining/schema.test.d.ts.map +1 -0
  129. package/dist/mining/schema.test.js +18 -0
  130. package/dist/mining/schema.test.js.map +1 -0
  131. package/dist/mining/task-queue.d.ts +14 -0
  132. package/dist/mining/task-queue.d.ts.map +1 -0
  133. package/dist/mining/task-queue.js +63 -0
  134. package/dist/mining/task-queue.js.map +1 -0
  135. package/dist/mining/task-queue.test.d.ts +2 -0
  136. package/dist/mining/task-queue.test.d.ts.map +1 -0
  137. package/dist/mining/task-queue.test.js +92 -0
  138. package/dist/mining/task-queue.test.js.map +1 -0
  139. package/dist/mining/types.d.ts +88 -0
  140. package/dist/mining/types.d.ts.map +1 -0
  141. package/dist/mining/types.js +3 -0
  142. package/dist/mining/types.js.map +1 -0
  143. package/dist/mining/vectors.d.ts +17 -0
  144. package/dist/mining/vectors.d.ts.map +1 -0
  145. package/dist/mining/vectors.js +67 -0
  146. package/dist/mining/vectors.js.map +1 -0
  147. package/dist/mining/vectors.test.d.ts +2 -0
  148. package/dist/mining/vectors.test.d.ts.map +1 -0
  149. package/dist/mining/vectors.test.js +15 -0
  150. package/dist/mining/vectors.test.js.map +1 -0
  151. package/dist/rate-limiter.test.d.ts +2 -0
  152. package/dist/rate-limiter.test.d.ts.map +1 -0
  153. package/dist/rate-limiter.test.js +51 -0
  154. package/dist/rate-limiter.test.js.map +1 -0
  155. package/dist/router.test.d.ts +2 -0
  156. package/dist/router.test.d.ts.map +1 -0
  157. package/dist/router.test.js +49 -0
  158. package/dist/router.test.js.map +1 -0
  159. package/dist/routing.test.d.ts +2 -0
  160. package/dist/routing.test.d.ts.map +1 -0
  161. package/dist/routing.test.js +132 -0
  162. package/dist/routing.test.js.map +1 -0
  163. package/dist/rqlite-mock.d.ts +13 -0
  164. package/dist/rqlite-mock.d.ts.map +1 -0
  165. package/dist/rqlite-mock.js +428 -0
  166. package/dist/rqlite-mock.js.map +1 -0
  167. package/dist/supabase-migration.test.d.ts +2 -0
  168. package/dist/supabase-migration.test.d.ts.map +1 -0
  169. package/dist/supabase-migration.test.js +388 -0
  170. package/dist/supabase-migration.test.js.map +1 -0
  171. package/dist/task-scheduler.test.d.ts +2 -0
  172. package/dist/task-scheduler.test.d.ts.map +1 -0
  173. package/dist/task-scheduler.test.js +43 -0
  174. package/dist/task-scheduler.test.js.map +1 -0
  175. package/dist/types.d.ts +2 -0
  176. package/dist/types.d.ts.map +1 -1
  177. package/package.json +6 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=telegram.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram.test.d.ts","sourceRoot":"","sources":["../../src/channels/telegram.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,640 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ // --- Mocks ---
3
+ // Mock config
4
+ vi.mock('../config.js', () => ({
5
+ ASSISTANT_NAME: 'Andy',
6
+ TRIGGER_PATTERN: /^@Andy\b/i,
7
+ }));
8
+ // Mock logger
9
+ vi.mock('../logger.js', () => ({
10
+ logger: {
11
+ debug: vi.fn(),
12
+ info: vi.fn(),
13
+ warn: vi.fn(),
14
+ error: vi.fn(),
15
+ },
16
+ }));
17
+ const botRef = vi.hoisted(() => ({ current: null }));
18
+ vi.mock('grammy', () => ({
19
+ Bot: class MockBot {
20
+ token;
21
+ commandHandlers = new Map();
22
+ filterHandlers = new Map();
23
+ errorHandler = null;
24
+ api = {
25
+ sendMessage: vi.fn().mockResolvedValue(undefined),
26
+ sendChatAction: vi.fn().mockResolvedValue(undefined),
27
+ };
28
+ constructor(token) {
29
+ this.token = token;
30
+ botRef.current = this;
31
+ }
32
+ command(name, handler) {
33
+ this.commandHandlers.set(name, handler);
34
+ }
35
+ on(filter, handler) {
36
+ const existing = this.filterHandlers.get(filter) || [];
37
+ existing.push(handler);
38
+ this.filterHandlers.set(filter, existing);
39
+ }
40
+ callbackQuery(data, handler) {
41
+ // Store as a special filter handler so tests can trigger it
42
+ const key = `callback_query:${data}`;
43
+ const existing = this.filterHandlers.get(key) || [];
44
+ existing.push(handler);
45
+ this.filterHandlers.set(key, existing);
46
+ }
47
+ catch(handler) {
48
+ this.errorHandler = handler;
49
+ }
50
+ start(opts) {
51
+ opts.onStart({ username: 'andy_ai_bot', id: 12345 });
52
+ }
53
+ stop() { }
54
+ },
55
+ }));
56
+ import { TelegramChannel, toTelegramHtml } from './telegram.js';
57
+ // --- Test helpers ---
58
+ function createTestOpts(overrides) {
59
+ return {
60
+ onMessage: vi.fn(),
61
+ onChatMetadata: vi.fn(),
62
+ registeredGroups: vi.fn(() => ({
63
+ 'tg:100200300': {
64
+ name: 'Test Group',
65
+ folder: 'test-group',
66
+ trigger: '@Andy',
67
+ added_at: '2024-01-01T00:00:00.000Z',
68
+ },
69
+ })),
70
+ ...overrides,
71
+ };
72
+ }
73
+ function createTextCtx(overrides) {
74
+ const chatId = overrides.chatId ?? 100200300;
75
+ const chatType = overrides.chatType ?? 'group';
76
+ return {
77
+ chat: {
78
+ id: chatId,
79
+ type: chatType,
80
+ title: overrides.chatTitle ?? 'Test Group',
81
+ },
82
+ from: {
83
+ id: overrides.fromId ?? 99001,
84
+ first_name: overrides.firstName ?? 'Alice',
85
+ username: overrides.username ?? 'alice_user',
86
+ },
87
+ message: {
88
+ text: overrides.text,
89
+ date: overrides.date ?? Math.floor(Date.now() / 1000),
90
+ message_id: overrides.messageId ?? 1,
91
+ entities: overrides.entities ?? [],
92
+ },
93
+ me: { username: 'andy_ai_bot' },
94
+ reply: vi.fn(),
95
+ };
96
+ }
97
+ function createMediaCtx(overrides) {
98
+ const chatId = overrides.chatId ?? 100200300;
99
+ return {
100
+ chat: {
101
+ id: chatId,
102
+ type: overrides.chatType ?? 'group',
103
+ title: 'Test Group',
104
+ },
105
+ from: {
106
+ id: overrides.fromId ?? 99001,
107
+ first_name: overrides.firstName ?? 'Alice',
108
+ username: 'alice_user',
109
+ },
110
+ message: {
111
+ date: overrides.date ?? Math.floor(Date.now() / 1000),
112
+ message_id: overrides.messageId ?? 1,
113
+ caption: overrides.caption,
114
+ ...(overrides.extra || {}),
115
+ },
116
+ me: { username: 'andy_ai_bot' },
117
+ };
118
+ }
119
+ function currentBot() {
120
+ return botRef.current;
121
+ }
122
+ async function triggerTextMessage(ctx) {
123
+ const handlers = currentBot().filterHandlers.get('message:text') || [];
124
+ for (const h of handlers)
125
+ await h(ctx);
126
+ }
127
+ async function triggerMediaMessage(filter, ctx) {
128
+ const handlers = currentBot().filterHandlers.get(filter) || [];
129
+ for (const h of handlers)
130
+ await h(ctx);
131
+ }
132
+ // --- Tests ---
133
+ describe('TelegramChannel', () => {
134
+ beforeEach(() => {
135
+ vi.clearAllMocks();
136
+ });
137
+ afterEach(() => {
138
+ vi.restoreAllMocks();
139
+ });
140
+ // --- Connection lifecycle ---
141
+ describe('connection lifecycle', () => {
142
+ it('resolves connect() when bot starts', async () => {
143
+ const opts = createTestOpts();
144
+ const channel = new TelegramChannel('test-token', opts);
145
+ await channel.connect();
146
+ expect(channel.isConnected()).toBe(true);
147
+ });
148
+ it('registers command and message handlers on connect', async () => {
149
+ const opts = createTestOpts();
150
+ const channel = new TelegramChannel('test-token', opts);
151
+ await channel.connect();
152
+ expect(currentBot().commandHandlers.has('chatid')).toBe(true);
153
+ expect(currentBot().commandHandlers.has('ping')).toBe(true);
154
+ expect(currentBot().filterHandlers.has('message:text')).toBe(true);
155
+ expect(currentBot().filterHandlers.has('message:photo')).toBe(true);
156
+ expect(currentBot().filterHandlers.has('message:video')).toBe(true);
157
+ expect(currentBot().filterHandlers.has('message:voice')).toBe(true);
158
+ expect(currentBot().filterHandlers.has('message:audio')).toBe(true);
159
+ expect(currentBot().filterHandlers.has('message:document')).toBe(true);
160
+ expect(currentBot().filterHandlers.has('message:sticker')).toBe(true);
161
+ expect(currentBot().filterHandlers.has('message:location')).toBe(true);
162
+ expect(currentBot().filterHandlers.has('message:contact')).toBe(true);
163
+ });
164
+ it('registers error handler on connect', async () => {
165
+ const opts = createTestOpts();
166
+ const channel = new TelegramChannel('test-token', opts);
167
+ await channel.connect();
168
+ expect(currentBot().errorHandler).not.toBeNull();
169
+ });
170
+ it('disconnects cleanly', async () => {
171
+ const opts = createTestOpts();
172
+ const channel = new TelegramChannel('test-token', opts);
173
+ await channel.connect();
174
+ expect(channel.isConnected()).toBe(true);
175
+ await channel.disconnect();
176
+ expect(channel.isConnected()).toBe(false);
177
+ });
178
+ it('isConnected() returns false before connect', () => {
179
+ const opts = createTestOpts();
180
+ const channel = new TelegramChannel('test-token', opts);
181
+ expect(channel.isConnected()).toBe(false);
182
+ });
183
+ });
184
+ // --- Text message handling ---
185
+ describe('text message handling', () => {
186
+ it('delivers message for registered group', async () => {
187
+ const opts = createTestOpts();
188
+ const channel = new TelegramChannel('test-token', opts);
189
+ await channel.connect();
190
+ const ctx = createTextCtx({ text: 'Hello everyone' });
191
+ await triggerTextMessage(ctx);
192
+ expect(opts.onChatMetadata).toHaveBeenCalledWith('tg:100200300', expect.any(String), 'Test Group');
193
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({
194
+ id: '1',
195
+ chat_jid: 'tg:100200300',
196
+ sender: '99001',
197
+ sender_name: 'Alice',
198
+ content: 'Hello everyone',
199
+ is_from_me: false,
200
+ }));
201
+ });
202
+ it('only emits metadata for unregistered chats', async () => {
203
+ const opts = createTestOpts();
204
+ const channel = new TelegramChannel('test-token', opts);
205
+ await channel.connect();
206
+ const ctx = createTextCtx({ chatId: 999999, text: 'Unknown chat' });
207
+ await triggerTextMessage(ctx);
208
+ expect(opts.onChatMetadata).toHaveBeenCalledWith('tg:999999', expect.any(String), 'Test Group');
209
+ expect(opts.onMessage).not.toHaveBeenCalled();
210
+ });
211
+ it('skips command messages (starting with /)', async () => {
212
+ const opts = createTestOpts();
213
+ const channel = new TelegramChannel('test-token', opts);
214
+ await channel.connect();
215
+ const ctx = createTextCtx({ text: '/start' });
216
+ await triggerTextMessage(ctx);
217
+ expect(opts.onMessage).not.toHaveBeenCalled();
218
+ expect(opts.onChatMetadata).not.toHaveBeenCalled();
219
+ });
220
+ it('extracts sender name from first_name', async () => {
221
+ const opts = createTestOpts();
222
+ const channel = new TelegramChannel('test-token', opts);
223
+ await channel.connect();
224
+ const ctx = createTextCtx({ text: 'Hi', firstName: 'Bob' });
225
+ await triggerTextMessage(ctx);
226
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ sender_name: 'Bob' }));
227
+ });
228
+ it('falls back to username when first_name missing', async () => {
229
+ const opts = createTestOpts();
230
+ const channel = new TelegramChannel('test-token', opts);
231
+ await channel.connect();
232
+ const ctx = createTextCtx({ text: 'Hi' });
233
+ ctx.from.first_name = undefined;
234
+ await triggerTextMessage(ctx);
235
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ sender_name: 'alice_user' }));
236
+ });
237
+ it('falls back to user ID when name and username missing', async () => {
238
+ const opts = createTestOpts();
239
+ const channel = new TelegramChannel('test-token', opts);
240
+ await channel.connect();
241
+ const ctx = createTextCtx({ text: 'Hi', fromId: 42 });
242
+ ctx.from.first_name = undefined;
243
+ ctx.from.username = undefined;
244
+ await triggerTextMessage(ctx);
245
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ sender_name: '42' }));
246
+ });
247
+ it('uses sender name as chat name for private chats', async () => {
248
+ const opts = createTestOpts({
249
+ registeredGroups: vi.fn(() => ({
250
+ 'tg:100200300': {
251
+ name: 'Private',
252
+ folder: 'private',
253
+ trigger: '@Andy',
254
+ added_at: '2024-01-01T00:00:00.000Z',
255
+ },
256
+ })),
257
+ });
258
+ const channel = new TelegramChannel('test-token', opts);
259
+ await channel.connect();
260
+ const ctx = createTextCtx({
261
+ text: 'Hello',
262
+ chatType: 'private',
263
+ firstName: 'Alice',
264
+ });
265
+ await triggerTextMessage(ctx);
266
+ expect(opts.onChatMetadata).toHaveBeenCalledWith('tg:100200300', expect.any(String), 'Alice');
267
+ });
268
+ it('uses chat title as name for group chats', async () => {
269
+ const opts = createTestOpts();
270
+ const channel = new TelegramChannel('test-token', opts);
271
+ await channel.connect();
272
+ const ctx = createTextCtx({
273
+ text: 'Hello',
274
+ chatType: 'supergroup',
275
+ chatTitle: 'Project Team',
276
+ });
277
+ await triggerTextMessage(ctx);
278
+ expect(opts.onChatMetadata).toHaveBeenCalledWith('tg:100200300', expect.any(String), 'Project Team');
279
+ });
280
+ it('converts message.date to ISO timestamp', async () => {
281
+ const opts = createTestOpts();
282
+ const channel = new TelegramChannel('test-token', opts);
283
+ await channel.connect();
284
+ const unixTime = 1704067200; // 2024-01-01T00:00:00.000Z
285
+ const ctx = createTextCtx({ text: 'Hello', date: unixTime });
286
+ await triggerTextMessage(ctx);
287
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({
288
+ timestamp: '2024-01-01T00:00:00.000Z',
289
+ }));
290
+ });
291
+ });
292
+ // --- @mention translation ---
293
+ describe('@mention translation', () => {
294
+ it('translates @bot_username mention to trigger format', async () => {
295
+ const opts = createTestOpts();
296
+ const channel = new TelegramChannel('test-token', opts);
297
+ await channel.connect();
298
+ const ctx = createTextCtx({
299
+ text: '@andy_ai_bot what time is it?',
300
+ entities: [{ type: 'mention', offset: 0, length: 12 }],
301
+ });
302
+ await triggerTextMessage(ctx);
303
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({
304
+ content: '@Andy @andy_ai_bot what time is it?',
305
+ }));
306
+ });
307
+ it('does not translate if message already matches trigger', async () => {
308
+ const opts = createTestOpts();
309
+ const channel = new TelegramChannel('test-token', opts);
310
+ await channel.connect();
311
+ const ctx = createTextCtx({
312
+ text: '@Andy @andy_ai_bot hello',
313
+ entities: [{ type: 'mention', offset: 6, length: 12 }],
314
+ });
315
+ await triggerTextMessage(ctx);
316
+ // Should NOT double-prepend — already starts with @Andy
317
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({
318
+ content: '@Andy @andy_ai_bot hello',
319
+ }));
320
+ });
321
+ it('does not translate mentions of other bots', async () => {
322
+ const opts = createTestOpts();
323
+ const channel = new TelegramChannel('test-token', opts);
324
+ await channel.connect();
325
+ const ctx = createTextCtx({
326
+ text: '@some_other_bot hi',
327
+ entities: [{ type: 'mention', offset: 0, length: 15 }],
328
+ });
329
+ await triggerTextMessage(ctx);
330
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({
331
+ content: '@some_other_bot hi', // No translation
332
+ }));
333
+ });
334
+ it('handles mention in middle of message', async () => {
335
+ const opts = createTestOpts();
336
+ const channel = new TelegramChannel('test-token', opts);
337
+ await channel.connect();
338
+ const ctx = createTextCtx({
339
+ text: 'hey @andy_ai_bot check this',
340
+ entities: [{ type: 'mention', offset: 4, length: 12 }],
341
+ });
342
+ await triggerTextMessage(ctx);
343
+ // Bot is mentioned, message doesn't match trigger → prepend trigger
344
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({
345
+ content: '@Andy hey @andy_ai_bot check this',
346
+ }));
347
+ });
348
+ it('handles message with no entities', async () => {
349
+ const opts = createTestOpts();
350
+ const channel = new TelegramChannel('test-token', opts);
351
+ await channel.connect();
352
+ const ctx = createTextCtx({ text: 'plain message' });
353
+ await triggerTextMessage(ctx);
354
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({
355
+ content: 'plain message',
356
+ }));
357
+ });
358
+ it('ignores non-mention entities', async () => {
359
+ const opts = createTestOpts();
360
+ const channel = new TelegramChannel('test-token', opts);
361
+ await channel.connect();
362
+ const ctx = createTextCtx({
363
+ text: 'check https://example.com',
364
+ entities: [{ type: 'url', offset: 6, length: 19 }],
365
+ });
366
+ await triggerTextMessage(ctx);
367
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({
368
+ content: 'check https://example.com',
369
+ }));
370
+ });
371
+ });
372
+ // --- Non-text messages ---
373
+ describe('non-text messages', () => {
374
+ it('stores photo with placeholder', async () => {
375
+ const opts = createTestOpts();
376
+ const channel = new TelegramChannel('test-token', opts);
377
+ await channel.connect();
378
+ const ctx = createMediaCtx({});
379
+ await triggerMediaMessage('message:photo', ctx);
380
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Photo]' }));
381
+ });
382
+ it('stores photo with caption', async () => {
383
+ const opts = createTestOpts();
384
+ const channel = new TelegramChannel('test-token', opts);
385
+ await channel.connect();
386
+ const ctx = createMediaCtx({ caption: 'Look at this' });
387
+ await triggerMediaMessage('message:photo', ctx);
388
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Photo] Look at this' }));
389
+ });
390
+ it('stores video with placeholder', async () => {
391
+ const opts = createTestOpts();
392
+ const channel = new TelegramChannel('test-token', opts);
393
+ await channel.connect();
394
+ const ctx = createMediaCtx({});
395
+ await triggerMediaMessage('message:video', ctx);
396
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Video]' }));
397
+ });
398
+ it('stores voice message with placeholder', async () => {
399
+ const opts = createTestOpts();
400
+ const channel = new TelegramChannel('test-token', opts);
401
+ await channel.connect();
402
+ const ctx = createMediaCtx({});
403
+ await triggerMediaMessage('message:voice', ctx);
404
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Voice message]' }));
405
+ });
406
+ it('stores audio with placeholder', async () => {
407
+ const opts = createTestOpts();
408
+ const channel = new TelegramChannel('test-token', opts);
409
+ await channel.connect();
410
+ const ctx = createMediaCtx({});
411
+ await triggerMediaMessage('message:audio', ctx);
412
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Audio]' }));
413
+ });
414
+ it('stores document with filename', async () => {
415
+ const opts = createTestOpts();
416
+ const channel = new TelegramChannel('test-token', opts);
417
+ await channel.connect();
418
+ const ctx = createMediaCtx({
419
+ extra: { document: { file_name: 'report.pdf' } },
420
+ });
421
+ await triggerMediaMessage('message:document', ctx);
422
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Document: report.pdf]' }));
423
+ });
424
+ it('stores document with fallback name when filename missing', async () => {
425
+ const opts = createTestOpts();
426
+ const channel = new TelegramChannel('test-token', opts);
427
+ await channel.connect();
428
+ const ctx = createMediaCtx({ extra: { document: {} } });
429
+ await triggerMediaMessage('message:document', ctx);
430
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Document: file]' }));
431
+ });
432
+ it('stores sticker with emoji', async () => {
433
+ const opts = createTestOpts();
434
+ const channel = new TelegramChannel('test-token', opts);
435
+ await channel.connect();
436
+ const ctx = createMediaCtx({
437
+ extra: { sticker: { emoji: '😂' } },
438
+ });
439
+ await triggerMediaMessage('message:sticker', ctx);
440
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Sticker 😂]' }));
441
+ });
442
+ it('stores location with placeholder', async () => {
443
+ const opts = createTestOpts();
444
+ const channel = new TelegramChannel('test-token', opts);
445
+ await channel.connect();
446
+ const ctx = createMediaCtx({});
447
+ await triggerMediaMessage('message:location', ctx);
448
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Location]' }));
449
+ });
450
+ it('stores contact with placeholder', async () => {
451
+ const opts = createTestOpts();
452
+ const channel = new TelegramChannel('test-token', opts);
453
+ await channel.connect();
454
+ const ctx = createMediaCtx({});
455
+ await triggerMediaMessage('message:contact', ctx);
456
+ expect(opts.onMessage).toHaveBeenCalledWith('tg:100200300', expect.objectContaining({ content: '[Contact]' }));
457
+ });
458
+ it('ignores non-text messages from unregistered chats', async () => {
459
+ const opts = createTestOpts();
460
+ const channel = new TelegramChannel('test-token', opts);
461
+ await channel.connect();
462
+ const ctx = createMediaCtx({ chatId: 999999 });
463
+ await triggerMediaMessage('message:photo', ctx);
464
+ expect(opts.onMessage).not.toHaveBeenCalled();
465
+ });
466
+ });
467
+ // --- sendMessage ---
468
+ describe('sendMessage', () => {
469
+ it('sends message via bot API with HTML parse_mode', async () => {
470
+ const opts = createTestOpts();
471
+ const channel = new TelegramChannel('test-token', opts);
472
+ await channel.connect();
473
+ await channel.sendMessage('tg:100200300', 'Hello');
474
+ expect(currentBot().api.sendMessage).toHaveBeenCalledWith('100200300', 'Hello', { parse_mode: 'HTML' });
475
+ });
476
+ it('strips tg: prefix from JID', async () => {
477
+ const opts = createTestOpts();
478
+ const channel = new TelegramChannel('test-token', opts);
479
+ await channel.connect();
480
+ await channel.sendMessage('tg:-1001234567890', 'Group message');
481
+ expect(currentBot().api.sendMessage).toHaveBeenCalledWith('-1001234567890', 'Group message', { parse_mode: 'HTML' });
482
+ });
483
+ it('splits long messages using splitHtmlForTelegram', async () => {
484
+ const opts = createTestOpts();
485
+ const channel = new TelegramChannel('test-token', opts);
486
+ await channel.connect();
487
+ const longText = 'x'.repeat(5000);
488
+ await channel.sendMessage('tg:100200300', longText);
489
+ // splitHtmlForTelegram splits at 4096 boundary
490
+ expect(currentBot().api.sendMessage).toHaveBeenCalledTimes(2);
491
+ // Both calls should have parse_mode HTML
492
+ for (const call of currentBot().api.sendMessage.mock.calls) {
493
+ expect(call[2]).toEqual({ parse_mode: 'HTML' });
494
+ }
495
+ });
496
+ it('converts markdown formatting to Telegram HTML', async () => {
497
+ const opts = createTestOpts();
498
+ const channel = new TelegramChannel('test-token', opts);
499
+ await channel.connect();
500
+ await channel.sendMessage('tg:100200300', '**bold** and _italic_');
501
+ expect(currentBot().api.sendMessage).toHaveBeenCalledWith('100200300', '<b>bold</b> and <i>italic</i>', { parse_mode: 'HTML' });
502
+ });
503
+ it('handles send failure gracefully', async () => {
504
+ const opts = createTestOpts();
505
+ const channel = new TelegramChannel('test-token', opts);
506
+ await channel.connect();
507
+ currentBot().api.sendMessage.mockRejectedValueOnce(new Error('Network error'));
508
+ // Should not throw
509
+ await expect(channel.sendMessage('tg:100200300', 'Will fail')).resolves.toBeUndefined();
510
+ });
511
+ it('does nothing when bot is not initialized', async () => {
512
+ const opts = createTestOpts();
513
+ const channel = new TelegramChannel('test-token', opts);
514
+ // Don't connect — bot is null
515
+ await channel.sendMessage('tg:100200300', 'No bot');
516
+ // No error, no API call
517
+ });
518
+ });
519
+ // --- ownsJid ---
520
+ describe('ownsJid', () => {
521
+ it('owns tg: JIDs', () => {
522
+ const channel = new TelegramChannel('test-token', createTestOpts());
523
+ expect(channel.ownsJid('tg:123456')).toBe(true);
524
+ });
525
+ it('owns tg: JIDs with negative IDs (groups)', () => {
526
+ const channel = new TelegramChannel('test-token', createTestOpts());
527
+ expect(channel.ownsJid('tg:-1001234567890')).toBe(true);
528
+ });
529
+ it('does not own WhatsApp group JIDs', () => {
530
+ const channel = new TelegramChannel('test-token', createTestOpts());
531
+ expect(channel.ownsJid('12345@g.us')).toBe(false);
532
+ });
533
+ it('does not own WhatsApp DM JIDs', () => {
534
+ const channel = new TelegramChannel('test-token', createTestOpts());
535
+ expect(channel.ownsJid('12345@s.whatsapp.net')).toBe(false);
536
+ });
537
+ it('does not own unknown JID formats', () => {
538
+ const channel = new TelegramChannel('test-token', createTestOpts());
539
+ expect(channel.ownsJid('random-string')).toBe(false);
540
+ });
541
+ });
542
+ // --- setTyping ---
543
+ describe('setTyping', () => {
544
+ it('sends typing action when isTyping is true', async () => {
545
+ const opts = createTestOpts();
546
+ const channel = new TelegramChannel('test-token', opts);
547
+ await channel.connect();
548
+ await channel.setTyping('tg:100200300', true);
549
+ expect(currentBot().api.sendChatAction).toHaveBeenCalledWith('100200300', 'typing');
550
+ });
551
+ it('does nothing when isTyping is false', async () => {
552
+ const opts = createTestOpts();
553
+ const channel = new TelegramChannel('test-token', opts);
554
+ await channel.connect();
555
+ await channel.setTyping('tg:100200300', false);
556
+ expect(currentBot().api.sendChatAction).not.toHaveBeenCalled();
557
+ });
558
+ it('does nothing when bot is not initialized', async () => {
559
+ const opts = createTestOpts();
560
+ const channel = new TelegramChannel('test-token', opts);
561
+ // Don't connect
562
+ await channel.setTyping('tg:100200300', true);
563
+ // No error, no API call
564
+ });
565
+ it('handles typing indicator failure gracefully', async () => {
566
+ const opts = createTestOpts();
567
+ const channel = new TelegramChannel('test-token', opts);
568
+ await channel.connect();
569
+ currentBot().api.sendChatAction.mockRejectedValueOnce(new Error('Rate limited'));
570
+ await expect(channel.setTyping('tg:100200300', true)).resolves.toBeUndefined();
571
+ });
572
+ });
573
+ // --- Bot commands ---
574
+ describe('bot commands', () => {
575
+ it('/chatid replies with chat ID and metadata', async () => {
576
+ const opts = createTestOpts();
577
+ const channel = new TelegramChannel('test-token', opts);
578
+ await channel.connect();
579
+ const handler = currentBot().commandHandlers.get('chatid');
580
+ const ctx = {
581
+ chat: { id: 100200300, type: 'group' },
582
+ from: { first_name: 'Alice' },
583
+ reply: vi.fn(),
584
+ };
585
+ await handler(ctx);
586
+ expect(ctx.reply).toHaveBeenCalledWith(expect.stringContaining('tg:100200300'), expect.objectContaining({ parse_mode: 'Markdown' }));
587
+ });
588
+ it('/chatid shows chat type', async () => {
589
+ const opts = createTestOpts();
590
+ const channel = new TelegramChannel('test-token', opts);
591
+ await channel.connect();
592
+ const handler = currentBot().commandHandlers.get('chatid');
593
+ const ctx = {
594
+ chat: { id: 555, type: 'private' },
595
+ from: { first_name: 'Bob' },
596
+ reply: vi.fn(),
597
+ };
598
+ await handler(ctx);
599
+ expect(ctx.reply).toHaveBeenCalledWith(expect.stringContaining('private'), expect.any(Object));
600
+ });
601
+ it('/ping replies with bot status', async () => {
602
+ const opts = createTestOpts();
603
+ const channel = new TelegramChannel('test-token', opts);
604
+ await channel.connect();
605
+ const handler = currentBot().commandHandlers.get('ping');
606
+ const ctx = { reply: vi.fn() };
607
+ await handler(ctx);
608
+ expect(ctx.reply).toHaveBeenCalledWith('Andy is online.');
609
+ });
610
+ });
611
+ // --- toTelegramHtml ---
612
+ describe('toTelegramHtml', () => {
613
+ it('converts **bold** to <b>', () => {
614
+ expect(toTelegramHtml('**bold**')).toBe('<b>bold</b>');
615
+ });
616
+ it('converts *italic* to <i>', () => {
617
+ expect(toTelegramHtml('*italic*')).toBe('<i>italic</i>');
618
+ });
619
+ it('converts `inline code` to <code>', () => {
620
+ expect(toTelegramHtml('use `npm install`')).toBe('use <code>npm install</code>');
621
+ });
622
+ it('passes through plain text', () => {
623
+ expect(toTelegramHtml('hello world')).toBe('hello world');
624
+ });
625
+ it('converts # headings to bold', () => {
626
+ expect(toTelegramHtml('# Title')).toBe('<b>Title</b>');
627
+ });
628
+ it('converts bullet lists', () => {
629
+ expect(toTelegramHtml('- item')).toBe('• item');
630
+ });
631
+ });
632
+ // --- Channel properties ---
633
+ describe('channel properties', () => {
634
+ it('has name "telegram"', () => {
635
+ const channel = new TelegramChannel('test-token', createTestOpts());
636
+ expect(channel.name).toBe('telegram');
637
+ });
638
+ });
639
+ });
640
+ //# sourceMappingURL=telegram.test.js.map