@ihazz/bitrix24 1.0.3 → 1.1.1

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.
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { HistoryCache } from '../src/history-cache.js';
3
+
4
+ describe('HistoryCache', () => {
5
+ it('keeps only the newest entries up to the per-key limit', () => {
6
+ const cache = new HistoryCache();
7
+
8
+ cache.append({
9
+ key: 'chat:1',
10
+ limit: 2,
11
+ entry: { messageId: '1', sender: 'A', body: 'first' },
12
+ });
13
+ cache.append({
14
+ key: 'chat:1',
15
+ limit: 2,
16
+ entry: { messageId: '2', sender: 'A', body: 'second' },
17
+ });
18
+ cache.append({
19
+ key: 'chat:1',
20
+ limit: 2,
21
+ entry: { messageId: '3', sender: 'A', body: 'third' },
22
+ });
23
+
24
+ expect(cache.get('chat:1')).toEqual([
25
+ { messageId: '2', sender: 'A', body: 'second' },
26
+ { messageId: '3', sender: 'A', body: 'third' },
27
+ ]);
28
+ });
29
+
30
+ it('finds entries by message id', () => {
31
+ const cache = new HistoryCache();
32
+ cache.append({
33
+ key: 'chat:1',
34
+ limit: 10,
35
+ entry: { messageId: '55', sender: 'A', body: 'hello' },
36
+ });
37
+
38
+ expect(cache.findByMessageId('chat:1', '55')).toMatchObject({
39
+ messageId: '55',
40
+ body: 'hello',
41
+ });
42
+ expect(cache.findByMessageId('chat:1', '99')).toBeUndefined();
43
+ });
44
+
45
+ it('evicts oldest keys when cache exceeds maxKeys', () => {
46
+ const cache = new HistoryCache({ maxKeys: 2 });
47
+ cache.append({
48
+ key: 'chat:1',
49
+ limit: 10,
50
+ entry: { messageId: '1', sender: 'A', body: 'one' },
51
+ });
52
+ cache.append({
53
+ key: 'chat:2',
54
+ limit: 10,
55
+ entry: { messageId: '2', sender: 'B', body: 'two' },
56
+ });
57
+ cache.append({
58
+ key: 'chat:3',
59
+ limit: 10,
60
+ entry: { messageId: '3', sender: 'C', body: 'three' },
61
+ });
62
+
63
+ expect(cache.size()).toBe(2);
64
+ expect(cache.get('chat:1')).toEqual([]);
65
+ expect(cache.get('chat:2')).toHaveLength(1);
66
+ expect(cache.get('chat:3')).toHaveLength(1);
67
+ });
68
+
69
+ it('stores and lists conversation metadata for history keys', () => {
70
+ const cache = new HistoryCache();
71
+
72
+ cache.append({
73
+ key: 'default:chat520',
74
+ limit: 10,
75
+ entry: { messageId: '1', sender: 'Alice', body: 'hello' },
76
+ meta: {
77
+ dialogId: 'chat520',
78
+ chatId: '520',
79
+ chatName: 'Чат зеленый 17',
80
+ chatType: 'chat',
81
+ isGroup: true,
82
+ lastActivityAt: 1,
83
+ },
84
+ });
85
+
86
+ expect(cache.listConversations()).toEqual([
87
+ {
88
+ key: 'default:chat520',
89
+ dialogId: 'chat520',
90
+ chatId: '520',
91
+ chatName: 'Чат зеленый 17',
92
+ chatType: 'chat',
93
+ isGroup: true,
94
+ lastActivityAt: 1,
95
+ },
96
+ ]);
97
+ });
98
+
99
+ it('does not append duplicate entries with the same message id to one history key', () => {
100
+ const cache = new HistoryCache();
101
+
102
+ cache.append({
103
+ key: 'chat:1',
104
+ limit: 10,
105
+ entry: { messageId: '55', sender: 'A', body: 'hello', eventScope: 'bot' },
106
+ });
107
+ cache.append({
108
+ key: 'chat:1',
109
+ limit: 10,
110
+ entry: { messageId: '55', sender: 'A', body: 'hello', eventScope: 'user' },
111
+ });
112
+
113
+ expect(cache.get('chat:1')).toEqual([
114
+ { messageId: '55', sender: 'A', body: 'hello', eventScope: 'bot' },
115
+ ]);
116
+ });
117
+ });
@@ -1,11 +1,14 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import {
3
+ accessApproved,
4
+ accessDenied,
5
+ groupPairingPending,
3
6
  resolveLocale,
4
7
  welcomeMessage,
5
8
  onboardingMessage,
9
+ ownerAndAllowedUsersOnly,
6
10
  personalBotOwnerOnly,
7
- forwardedMessageUnsupported,
8
- replyMessageUnsupported,
11
+ watchOwnerDmNotice,
9
12
  } from '../src/i18n.js';
10
13
 
11
14
  describe('i18n welcome messages', () => {
@@ -16,12 +19,12 @@ describe('i18n welcome messages', () => {
16
19
  });
17
20
 
18
21
  it('returns localized Russian welcome message', () => {
19
- expect(welcomeMessage('ru', 'OpenClaw')).toContain('OpenClaw готов');
22
+ expect(welcomeMessage('ru', 'OpenClaw')).toContain('OpenClaw готов к работе');
20
23
  });
21
24
 
22
25
  it('falls back to English welcome message for unknown locale', () => {
23
26
  expect(welcomeMessage('zz', 'OpenClaw')).toBe(
24
- 'OpenClaw ready. Send me a message or pick a command below.',
27
+ 'OpenClaw is ready. Send a message or choose a command below.',
25
28
  );
26
29
  });
27
30
 
@@ -31,17 +34,57 @@ describe('i18n welcome messages', () => {
31
34
  });
32
35
 
33
36
  it('returns localized personal bot access denied message', () => {
34
- expect(personalBotOwnerOnly('ru')).toContain('персональный');
35
- expect(personalBotOwnerOnly('en')).toContain('personal');
37
+ expect(personalBotOwnerOnly('ru')).toContain('Писать ему может только владелец');
38
+ expect(personalBotOwnerOnly('en')).toContain('Only the bot owner can message it');
36
39
  });
37
40
 
38
- it('returns localized reply unsupported message', () => {
39
- expect(replyMessageUnsupported('ru')).toContain('пока не поддерживаются');
40
- expect(replyMessageUnsupported('en')).toContain('not supported');
41
+ it('returns localized access approved message', () => {
42
+ expect(accessApproved('ru')).toBe('Доступ к боту подтвержден.');
43
+ expect(accessApproved('en')).toBe('Access to the bot has been approved.');
41
44
  });
42
45
 
43
- it('returns localized forwarded message unsupported message', () => {
44
- expect(forwardedMessageUnsupported('ru')).toContain('Пересланные сообщения');
45
- expect(forwardedMessageUnsupported('en')).toContain('Forwarded messages');
46
+ it('returns localized owner and allowlist access denied message', () => {
47
+ expect(ownerAndAllowedUsersOnly('ru')).toContain('пользователям с подтвержденным доступом');
48
+ expect(ownerAndAllowedUsersOnly('en')).toContain('approved access');
49
+ });
50
+
51
+ it('returns localized generic access denied message', () => {
52
+ expect(accessDenied('ru')).toContain('нет доступа');
53
+ expect(accessDenied('en')).toContain('do not have access');
54
+ });
55
+
56
+ it('returns localized group pairing pending message', () => {
57
+ expect(groupPairingPending('ru')).toContain('сначала нужно подтвердить доступ');
58
+ expect(groupPairingPending('en')).toContain('access must be approved first');
59
+ });
60
+
61
+ it('returns localized owner watch DM notification message', () => {
62
+ expect(watchOwnerDmNotice('ru', {
63
+ chatRef: '[URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]',
64
+ topicsRef: '[b]секрет[/b]',
65
+ })).toBe('Сработало правило отслеживания в чате [URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]. Совпавшие темы: [b]секрет[/b]');
66
+ expect(watchOwnerDmNotice('en', {
67
+ chatRef: '[URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]',
68
+ topicsRef: '[b]secret[/b]',
69
+ })).toBe('A watch rule was triggered in chat [URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]. Matched topics: [b]secret[/b]');
70
+ expect(watchOwnerDmNotice('de', {
71
+ chatRef: '[URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]',
72
+ topicsRef: '[b]geheim[/b]',
73
+ })).toBe('Eine Beobachtungsregel wurde im Chat [URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL] ausgeloest. Zugeordnete Themen: [b]geheim[/b]');
74
+ expect(watchOwnerDmNotice('zz', {
75
+ chatRef: '[URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]',
76
+ topicsRef: '[b]secret[/b]',
77
+ })).toBe('A watch rule was triggered in chat [URL=/online/?IM_DIALOG=chat520&IM_MESSAGE=710]Green Chat 15[/URL]. Matched topics: [b]secret[/b]');
78
+
79
+ expect(watchOwnerDmNotice('ru', {
80
+ chatRef: '[URL=/online/?IM_DIALOG=10&IM_MESSAGE=31671]Сергей Рыжиков[/URL]',
81
+ topicsRef: '[b]авария[/b]',
82
+ sourceKind: 'dm',
83
+ })).toBe('Сработало правило отслеживания в личном чате с [URL=/online/?IM_DIALOG=10&IM_MESSAGE=31671]Сергей Рыжиков[/URL]. Совпавшие темы: [b]авария[/b]');
84
+ expect(watchOwnerDmNotice('en', {
85
+ chatRef: '[URL=/online/?IM_DIALOG=10&IM_MESSAGE=31671]Sergey Ryzhikov[/URL]',
86
+ topicsRef: '[b]incident[/b]',
87
+ sourceKind: 'dm',
88
+ })).toBe('A watch rule was triggered in a direct chat with [URL=/online/?IM_DIALOG=10&IM_MESSAGE=31671]Sergey Ryzhikov[/URL]. Matched topics: [b]incident[/b]');
46
89
  });
47
90
  });
@@ -116,12 +116,14 @@ describe('InboundHandler', () => {
116
116
 
117
117
  const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
118
118
  expect(ctx.channel).toBe('bitrix24');
119
+ expect(ctx.eventScope).toBe('bot');
119
120
  expect(ctx.text).toBe('Hello, how are you?');
120
121
  expect(ctx.senderId).toBe('1');
121
122
  expect(ctx.senderName).toBe('Test User');
122
123
  expect(ctx.senderFirstName).toBe('Test');
123
124
  expect(ctx.chatId).toBe('1');
124
125
  expect(ctx.chatInternalId).toBe('40985');
126
+ expect(ctx.chatName).toBe('Test User');
125
127
  expect(ctx.messageId).toBe('490659');
126
128
  expect(ctx.replyToMessageId).toBeUndefined();
127
129
  expect(ctx.isForwarded).toBe(false);
@@ -129,9 +131,194 @@ describe('InboundHandler', () => {
129
131
  expect(ctx.isGroup).toBe(false);
130
132
  expect(ctx.media).toEqual([]);
131
133
  expect(ctx.language).toBe('ru');
134
+ expect(ctx.timestamp).toBe(Date.parse('2026-03-18T08:00:00+02:00'));
132
135
  expect(ctx.botId).toBe(2081);
133
136
  });
134
137
 
138
+ it('dispatches user message events from combined fetch in agent mode', async () => {
139
+ const onMessage = vi.fn();
140
+ handler = new InboundHandler({
141
+ config: { dmPolicy: 'pairing', agentMode: true },
142
+ logger: silentLogger,
143
+ onMessage,
144
+ });
145
+
146
+ const event = {
147
+ eventId: 1007,
148
+ type: 'ONIMV2MESSAGEADD',
149
+ date: '2026-03-19T14:36:03+02:00',
150
+ data: {
151
+ message: {
152
+ id: 490660,
153
+ chatId: 520,
154
+ authorId: 77,
155
+ date: '2026-03-19T14:36:03+02:00',
156
+ text: 'Авария в личке',
157
+ isSystem: false,
158
+ uuid: '',
159
+ forward: null,
160
+ params: {},
161
+ viewedByOthers: false,
162
+ },
163
+ chat: {
164
+ id: 520,
165
+ dialogId: '77',
166
+ type: 'private',
167
+ name: 'Sergey',
168
+ entityType: '',
169
+ owner: 1,
170
+ avatar: '',
171
+ color: '#ab7761',
172
+ },
173
+ user: {
174
+ ...baseUser,
175
+ id: 77,
176
+ name: 'Sergey',
177
+ firstName: 'Sergey',
178
+ },
179
+ language: 'ru',
180
+ },
181
+ };
182
+
183
+ const result = await handler.handleFetchEvent(event as never, fetchCtx);
184
+ expect(result).toBe(true);
185
+ expect(onMessage).toHaveBeenCalledOnce();
186
+ expect(onMessage.mock.calls[0][0]).toMatchObject({
187
+ eventScope: 'user',
188
+ chatId: '77',
189
+ senderId: '77',
190
+ text: 'Авария в личке',
191
+ wasMentioned: false,
192
+ });
193
+ });
194
+
195
+ it('processes bot and user message events separately when they share one message id', async () => {
196
+ const onMessage = vi.fn();
197
+ handler = new InboundHandler({
198
+ config: { dmPolicy: 'pairing', agentMode: true },
199
+ logger: silentLogger,
200
+ onMessage,
201
+ });
202
+
203
+ const botEvent = createFetchMessageEvent({
204
+ message: {
205
+ id: 555001,
206
+ chatId: 520,
207
+ authorId: 77,
208
+ date: '2026-03-19T14:36:03+02:00',
209
+ text: 'shared message',
210
+ isSystem: false,
211
+ uuid: '',
212
+ forward: null,
213
+ params: {},
214
+ viewedByOthers: false,
215
+ },
216
+ chat: {
217
+ id: 520,
218
+ dialogId: 'chat520',
219
+ type: 'chat',
220
+ name: 'Group Chat',
221
+ entityType: '',
222
+ owner: 1,
223
+ avatar: '',
224
+ color: '#ab7761',
225
+ },
226
+ user: {
227
+ ...baseUser,
228
+ id: 77,
229
+ name: 'Sergey',
230
+ },
231
+ });
232
+
233
+ const userEvent = {
234
+ eventId: 1008,
235
+ type: 'ONIMV2MESSAGEADD',
236
+ date: '2026-03-19T14:36:04+02:00',
237
+ data: {
238
+ message: {
239
+ id: 555001,
240
+ chatId: 520,
241
+ authorId: 77,
242
+ date: '2026-03-19T14:36:03+02:00',
243
+ text: 'shared message',
244
+ isSystem: false,
245
+ uuid: '',
246
+ forward: null,
247
+ params: {},
248
+ viewedByOthers: false,
249
+ },
250
+ chat: {
251
+ id: 520,
252
+ dialogId: 'chat520',
253
+ type: 'chat',
254
+ name: 'Group Chat',
255
+ entityType: '',
256
+ owner: 1,
257
+ avatar: '',
258
+ color: '#ab7761',
259
+ },
260
+ user: {
261
+ ...baseUser,
262
+ id: 77,
263
+ name: 'Sergey',
264
+ },
265
+ language: 'ru',
266
+ },
267
+ };
268
+
269
+ await handler.handleFetchEvent(botEvent as never, fetchCtx);
270
+ await handler.handleFetchEvent(userEvent as never, fetchCtx);
271
+
272
+ expect(onMessage).toHaveBeenCalledTimes(2);
273
+ expect((onMessage.mock.calls[0][0] as B24MsgContext).eventScope).toBe('bot');
274
+ expect((onMessage.mock.calls[1][0] as B24MsgContext).eventScope).toBe('user');
275
+ });
276
+
277
+ it('still deduplicates repeated events within the same scope', async () => {
278
+ const onMessage = vi.fn();
279
+ handler = new InboundHandler({
280
+ config: { dmPolicy: 'pairing', agentMode: true },
281
+ logger: silentLogger,
282
+ onMessage,
283
+ });
284
+
285
+ const botEvent = createFetchMessageEvent({
286
+ message: {
287
+ id: 555002,
288
+ chatId: 520,
289
+ authorId: 77,
290
+ date: '2026-03-19T14:36:03+02:00',
291
+ text: 'same scope duplicate',
292
+ isSystem: false,
293
+ uuid: '',
294
+ forward: null,
295
+ params: {},
296
+ viewedByOthers: false,
297
+ },
298
+ chat: {
299
+ id: 520,
300
+ dialogId: 'chat520',
301
+ type: 'chat',
302
+ name: 'Group Chat',
303
+ entityType: '',
304
+ owner: 1,
305
+ avatar: '',
306
+ color: '#ab7761',
307
+ },
308
+ user: {
309
+ ...baseUser,
310
+ id: 77,
311
+ name: 'Sergey',
312
+ },
313
+ });
314
+
315
+ await handler.handleFetchEvent(botEvent as never, fetchCtx);
316
+ await handler.handleFetchEvent(botEvent as never, fetchCtx);
317
+
318
+ expect(onMessage).toHaveBeenCalledTimes(1);
319
+ expect((onMessage.mock.calls[0][0] as B24MsgContext).eventScope).toBe('bot');
320
+ });
321
+
135
322
  it('keeps incoming dialogId for direct chats without replacing it by user id', async () => {
136
323
  const onMessage = vi.fn();
137
324
  handler = new InboundHandler({
@@ -157,7 +344,7 @@ describe('InboundHandler', () => {
157
344
  },
158
345
  });
159
346
 
160
- await handler.handleFetchEvent(event, fetchCtx);
347
+ await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
161
348
 
162
349
  const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
163
350
  expect(ctx.chatId).toBe('123');
@@ -173,8 +360,8 @@ describe('InboundHandler', () => {
173
360
  });
174
361
 
175
362
  const event = createFetchMessageEvent();
176
- await handler.handleFetchEvent(event, fetchCtx);
177
- await handler.handleFetchEvent(event, fetchCtx);
363
+ await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
364
+ await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
178
365
 
179
366
  expect(onMessage).toHaveBeenCalledOnce();
180
367
  });
@@ -281,6 +468,91 @@ describe('InboundHandler', () => {
281
468
  expect(ctx.isForwarded).toBe(true);
282
469
  });
283
470
 
471
+ it('detects bot mention in a group message from real Bitrix payload format', async () => {
472
+ const onMessage = vi.fn();
473
+ handler = new InboundHandler({
474
+ config: { dmPolicy: 'pairing', botName: 'OpenClaw' },
475
+ logger: silentLogger,
476
+ onMessage,
477
+ });
478
+
479
+ const event = createFetchMessageEvent({
480
+ eventId: 9745,
481
+ message: {
482
+ id: 30857,
483
+ chatId: 520,
484
+ authorId: 1,
485
+ date: '2026-03-19T14:36:03+02:00',
486
+ text: '[USER=6809]OpenClaw[/USER] привет',
487
+ isSystem: false,
488
+ uuid: '',
489
+ forward: null,
490
+ params: [],
491
+ viewedByOthers: false,
492
+ },
493
+ chat: {
494
+ id: 520,
495
+ dialogId: 'chat520',
496
+ type: 'chat',
497
+ name: 'Group Chat',
498
+ entityType: '',
499
+ owner: 1,
500
+ avatar: '',
501
+ color: '#ab7761',
502
+ },
503
+ });
504
+
505
+ await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
506
+
507
+ const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
508
+ expect(ctx.chatId).toBe('chat520');
509
+ expect(ctx.chatInternalId).toBe('520');
510
+ expect(ctx.chatName).toBe('Group Chat');
511
+ expect(ctx.chatType).toBe('chat');
512
+ expect(ctx.isDm).toBe(false);
513
+ expect(ctx.isGroup).toBe(true);
514
+ expect(ctx.wasMentioned).toBe(true);
515
+ });
516
+
517
+ it('does not mark plain group messages as mentions', async () => {
518
+ const onMessage = vi.fn();
519
+ handler = new InboundHandler({
520
+ config: { dmPolicy: 'pairing', botName: 'OpenClaw' },
521
+ logger: silentLogger,
522
+ onMessage,
523
+ });
524
+
525
+ const event = createFetchMessageEvent({
526
+ message: {
527
+ id: 30858,
528
+ chatId: 520,
529
+ authorId: 1,
530
+ date: '2026-03-19T14:36:10+02:00',
531
+ text: 'привет',
532
+ isSystem: false,
533
+ uuid: '',
534
+ forward: null,
535
+ params: [],
536
+ viewedByOthers: false,
537
+ },
538
+ chat: {
539
+ id: 520,
540
+ dialogId: 'chat520',
541
+ type: 'chat',
542
+ name: 'Group Chat',
543
+ entityType: '',
544
+ owner: 1,
545
+ avatar: '',
546
+ color: '#ab7761',
547
+ },
548
+ });
549
+
550
+ await handler.handleFetchEvent(event, fetchCtx);
551
+
552
+ const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
553
+ expect(ctx.wasMentioned).toBe(false);
554
+ });
555
+
284
556
  it('dispatches webhook message events from JSON body', async () => {
285
557
  const onMessage = vi.fn();
286
558
  handler = new InboundHandler({
@@ -440,13 +712,58 @@ describe('InboundHandler', () => {
440
712
  expect(result).toBe(true);
441
713
  expect(onJoinChat).toHaveBeenCalledOnce();
442
714
  expect(onJoinChat).toHaveBeenCalledWith({
715
+ senderId: '1',
443
716
  dialogId: '1',
717
+ chatId: '40985',
444
718
  chatType: 'private',
445
719
  language: 'ru',
446
720
  fetchCtx,
447
721
  });
448
722
  });
449
723
 
724
+ it('dispatches group join chat events with numeric chat id', async () => {
725
+ const onJoinChat = vi.fn();
726
+ handler = new InboundHandler({
727
+ config: { dmPolicy: 'pairing' },
728
+ logger: silentLogger,
729
+ onJoinChat,
730
+ });
731
+
732
+ const event = {
733
+ eventId: 1006,
734
+ type: 'ONIMBOTV2JOINCHAT',
735
+ date: '2026-03-19T14:36:03+02:00',
736
+ data: {
737
+ bot: baseBot,
738
+ dialogId: 'chat520',
739
+ chat: {
740
+ id: 520,
741
+ dialogId: 'chat520',
742
+ type: 'chat',
743
+ name: 'Group Chat',
744
+ entityType: '',
745
+ owner: 1,
746
+ avatar: '',
747
+ color: '#ab7761',
748
+ },
749
+ user: baseUser,
750
+ language: 'ru',
751
+ },
752
+ };
753
+
754
+ const result = await handler.handleFetchEvent(event as never, fetchCtx);
755
+ expect(result).toBe(true);
756
+ expect(onJoinChat).toHaveBeenCalledOnce();
757
+ expect(onJoinChat).toHaveBeenCalledWith({
758
+ senderId: '1',
759
+ dialogId: 'chat520',
760
+ chatId: '520',
761
+ chatType: 'chat',
762
+ language: 'ru',
763
+ fetchCtx,
764
+ });
765
+ });
766
+
450
767
  it('dispatches command events', async () => {
451
768
  const onCommand = vi.fn();
452
769
  handler = new InboundHandler({
@@ -504,6 +821,7 @@ describe('InboundHandler', () => {
504
821
  commandText: '/help topic',
505
822
  senderId: '1',
506
823
  dialogId: '1',
824
+ chatId: '40985',
507
825
  chatType: 'P',
508
826
  messageId: '490667',
509
827
  language: 'ru',
@@ -555,6 +873,7 @@ describe('InboundHandler', () => {
555
873
  commandText: '/status',
556
874
  senderId: '1',
557
875
  dialogId: '1',
876
+ chatId: '40985',
558
877
  chatType: 'P',
559
878
  messageId: '1004',
560
879
  language: 'ru',
@@ -562,6 +881,71 @@ describe('InboundHandler', () => {
562
881
  });
563
882
  });
564
883
 
884
+ it('dispatches group command events with numeric chat id', async () => {
885
+ const onCommand = vi.fn();
886
+ handler = new InboundHandler({
887
+ config: { dmPolicy: 'pairing' },
888
+ logger: silentLogger,
889
+ onCommand,
890
+ });
891
+
892
+ const event = {
893
+ eventId: 1007,
894
+ type: 'ONIMBOTV2COMMANDADD',
895
+ date: '2026-03-19T14:36:03+02:00',
896
+ data: {
897
+ bot: baseBot,
898
+ command: {
899
+ id: 56,
900
+ command: '/help',
901
+ params: '',
902
+ context: 'textarea',
903
+ },
904
+ message: {
905
+ id: 30857,
906
+ chatId: 520,
907
+ authorId: 1,
908
+ date: '2026-03-19T14:36:03+02:00',
909
+ text: '/help',
910
+ isSystem: false,
911
+ uuid: '',
912
+ forward: null,
913
+ params: [],
914
+ viewedByOthers: false,
915
+ },
916
+ chat: {
917
+ id: 520,
918
+ dialogId: 'chat520',
919
+ type: 'chat',
920
+ name: 'Group Chat',
921
+ entityType: '',
922
+ owner: 1,
923
+ avatar: '',
924
+ color: '#ab7761',
925
+ },
926
+ user: baseUser,
927
+ language: 'ru',
928
+ },
929
+ };
930
+
931
+ const result = await handler.handleFetchEvent(event as never, fetchCtx);
932
+ expect(result).toBe(true);
933
+ expect(onCommand).toHaveBeenCalledOnce();
934
+ expect(onCommand).toHaveBeenCalledWith({
935
+ commandId: 56,
936
+ commandName: 'help',
937
+ commandParams: '',
938
+ commandText: '/help',
939
+ senderId: '1',
940
+ dialogId: 'chat520',
941
+ chatId: '520',
942
+ chatType: 'chat',
943
+ messageId: '30857',
944
+ language: 'ru',
945
+ fetchCtx,
946
+ });
947
+ });
948
+
565
949
  it('builds command text from command payload instead of message text', async () => {
566
950
  const onCommand = vi.fn();
567
951
  handler = new InboundHandler({
@@ -619,6 +1003,7 @@ describe('InboundHandler', () => {
619
1003
  commandText: '/status',
620
1004
  senderId: '1',
621
1005
  dialogId: '1',
1006
+ chatId: '40985',
622
1007
  chatType: 'P',
623
1008
  messageId: '490668',
624
1009
  language: 'ru',