@ihazz/bitrix24 1.0.3 → 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.
@@ -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,147 @@ 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('deduplicates bot and user message events that 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(1);
273
+ });
274
+
135
275
  it('keeps incoming dialogId for direct chats without replacing it by user id', async () => {
136
276
  const onMessage = vi.fn();
137
277
  handler = new InboundHandler({
@@ -157,7 +297,7 @@ describe('InboundHandler', () => {
157
297
  },
158
298
  });
159
299
 
160
- await handler.handleFetchEvent(event, fetchCtx);
300
+ await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
161
301
 
162
302
  const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
163
303
  expect(ctx.chatId).toBe('123');
@@ -173,8 +313,8 @@ describe('InboundHandler', () => {
173
313
  });
174
314
 
175
315
  const event = createFetchMessageEvent();
176
- await handler.handleFetchEvent(event, fetchCtx);
177
- await handler.handleFetchEvent(event, fetchCtx);
316
+ await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
317
+ await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
178
318
 
179
319
  expect(onMessage).toHaveBeenCalledOnce();
180
320
  });
@@ -281,6 +421,91 @@ describe('InboundHandler', () => {
281
421
  expect(ctx.isForwarded).toBe(true);
282
422
  });
283
423
 
424
+ it('detects bot mention in a group message from real Bitrix payload format', async () => {
425
+ const onMessage = vi.fn();
426
+ handler = new InboundHandler({
427
+ config: { dmPolicy: 'pairing', botName: 'OpenClaw' },
428
+ logger: silentLogger,
429
+ onMessage,
430
+ });
431
+
432
+ const event = createFetchMessageEvent({
433
+ eventId: 9745,
434
+ message: {
435
+ id: 30857,
436
+ chatId: 520,
437
+ authorId: 1,
438
+ date: '2026-03-19T14:36:03+02:00',
439
+ text: '[USER=6809]OpenClaw[/USER] привет',
440
+ isSystem: false,
441
+ uuid: '',
442
+ forward: null,
443
+ params: [],
444
+ viewedByOthers: false,
445
+ },
446
+ chat: {
447
+ id: 520,
448
+ dialogId: 'chat520',
449
+ type: 'chat',
450
+ name: 'Group Chat',
451
+ entityType: '',
452
+ owner: 1,
453
+ avatar: '',
454
+ color: '#ab7761',
455
+ },
456
+ });
457
+
458
+ await handler.handleFetchEvent(event, { ...fetchCtx, botId: 6809 });
459
+
460
+ const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
461
+ expect(ctx.chatId).toBe('chat520');
462
+ expect(ctx.chatInternalId).toBe('520');
463
+ expect(ctx.chatName).toBe('Group Chat');
464
+ expect(ctx.chatType).toBe('chat');
465
+ expect(ctx.isDm).toBe(false);
466
+ expect(ctx.isGroup).toBe(true);
467
+ expect(ctx.wasMentioned).toBe(true);
468
+ });
469
+
470
+ it('does not mark plain group messages as mentions', async () => {
471
+ const onMessage = vi.fn();
472
+ handler = new InboundHandler({
473
+ config: { dmPolicy: 'pairing', botName: 'OpenClaw' },
474
+ logger: silentLogger,
475
+ onMessage,
476
+ });
477
+
478
+ const event = createFetchMessageEvent({
479
+ message: {
480
+ id: 30858,
481
+ chatId: 520,
482
+ authorId: 1,
483
+ date: '2026-03-19T14:36:10+02:00',
484
+ text: 'привет',
485
+ isSystem: false,
486
+ uuid: '',
487
+ forward: null,
488
+ params: [],
489
+ viewedByOthers: false,
490
+ },
491
+ chat: {
492
+ id: 520,
493
+ dialogId: 'chat520',
494
+ type: 'chat',
495
+ name: 'Group Chat',
496
+ entityType: '',
497
+ owner: 1,
498
+ avatar: '',
499
+ color: '#ab7761',
500
+ },
501
+ });
502
+
503
+ await handler.handleFetchEvent(event, fetchCtx);
504
+
505
+ const ctx = onMessage.mock.calls[0][0] as B24MsgContext;
506
+ expect(ctx.wasMentioned).toBe(false);
507
+ });
508
+
284
509
  it('dispatches webhook message events from JSON body', async () => {
285
510
  const onMessage = vi.fn();
286
511
  handler = new InboundHandler({
@@ -440,13 +665,58 @@ describe('InboundHandler', () => {
440
665
  expect(result).toBe(true);
441
666
  expect(onJoinChat).toHaveBeenCalledOnce();
442
667
  expect(onJoinChat).toHaveBeenCalledWith({
668
+ senderId: '1',
443
669
  dialogId: '1',
670
+ chatId: '40985',
444
671
  chatType: 'private',
445
672
  language: 'ru',
446
673
  fetchCtx,
447
674
  });
448
675
  });
449
676
 
677
+ it('dispatches group join chat events with numeric chat id', async () => {
678
+ const onJoinChat = vi.fn();
679
+ handler = new InboundHandler({
680
+ config: { dmPolicy: 'pairing' },
681
+ logger: silentLogger,
682
+ onJoinChat,
683
+ });
684
+
685
+ const event = {
686
+ eventId: 1006,
687
+ type: 'ONIMBOTV2JOINCHAT',
688
+ date: '2026-03-19T14:36:03+02:00',
689
+ data: {
690
+ bot: baseBot,
691
+ dialogId: 'chat520',
692
+ chat: {
693
+ id: 520,
694
+ dialogId: 'chat520',
695
+ type: 'chat',
696
+ name: 'Group Chat',
697
+ entityType: '',
698
+ owner: 1,
699
+ avatar: '',
700
+ color: '#ab7761',
701
+ },
702
+ user: baseUser,
703
+ language: 'ru',
704
+ },
705
+ };
706
+
707
+ const result = await handler.handleFetchEvent(event as never, fetchCtx);
708
+ expect(result).toBe(true);
709
+ expect(onJoinChat).toHaveBeenCalledOnce();
710
+ expect(onJoinChat).toHaveBeenCalledWith({
711
+ senderId: '1',
712
+ dialogId: 'chat520',
713
+ chatId: '520',
714
+ chatType: 'chat',
715
+ language: 'ru',
716
+ fetchCtx,
717
+ });
718
+ });
719
+
450
720
  it('dispatches command events', async () => {
451
721
  const onCommand = vi.fn();
452
722
  handler = new InboundHandler({
@@ -504,6 +774,7 @@ describe('InboundHandler', () => {
504
774
  commandText: '/help topic',
505
775
  senderId: '1',
506
776
  dialogId: '1',
777
+ chatId: '40985',
507
778
  chatType: 'P',
508
779
  messageId: '490667',
509
780
  language: 'ru',
@@ -555,6 +826,7 @@ describe('InboundHandler', () => {
555
826
  commandText: '/status',
556
827
  senderId: '1',
557
828
  dialogId: '1',
829
+ chatId: '40985',
558
830
  chatType: 'P',
559
831
  messageId: '1004',
560
832
  language: 'ru',
@@ -562,6 +834,71 @@ describe('InboundHandler', () => {
562
834
  });
563
835
  });
564
836
 
837
+ it('dispatches group command events with numeric chat id', async () => {
838
+ const onCommand = vi.fn();
839
+ handler = new InboundHandler({
840
+ config: { dmPolicy: 'pairing' },
841
+ logger: silentLogger,
842
+ onCommand,
843
+ });
844
+
845
+ const event = {
846
+ eventId: 1007,
847
+ type: 'ONIMBOTV2COMMANDADD',
848
+ date: '2026-03-19T14:36:03+02:00',
849
+ data: {
850
+ bot: baseBot,
851
+ command: {
852
+ id: 56,
853
+ command: '/help',
854
+ params: '',
855
+ context: 'textarea',
856
+ },
857
+ message: {
858
+ id: 30857,
859
+ chatId: 520,
860
+ authorId: 1,
861
+ date: '2026-03-19T14:36:03+02:00',
862
+ text: '/help',
863
+ isSystem: false,
864
+ uuid: '',
865
+ forward: null,
866
+ params: [],
867
+ viewedByOthers: false,
868
+ },
869
+ chat: {
870
+ id: 520,
871
+ dialogId: 'chat520',
872
+ type: 'chat',
873
+ name: 'Group Chat',
874
+ entityType: '',
875
+ owner: 1,
876
+ avatar: '',
877
+ color: '#ab7761',
878
+ },
879
+ user: baseUser,
880
+ language: 'ru',
881
+ },
882
+ };
883
+
884
+ const result = await handler.handleFetchEvent(event as never, fetchCtx);
885
+ expect(result).toBe(true);
886
+ expect(onCommand).toHaveBeenCalledOnce();
887
+ expect(onCommand).toHaveBeenCalledWith({
888
+ commandId: 56,
889
+ commandName: 'help',
890
+ commandParams: '',
891
+ commandText: '/help',
892
+ senderId: '1',
893
+ dialogId: 'chat520',
894
+ chatId: '520',
895
+ chatType: 'chat',
896
+ messageId: '30857',
897
+ language: 'ru',
898
+ fetchCtx,
899
+ });
900
+ });
901
+
565
902
  it('builds command text from command payload instead of message text', async () => {
566
903
  const onCommand = vi.fn();
567
904
  handler = new InboundHandler({
@@ -619,6 +956,7 @@ describe('InboundHandler', () => {
619
956
  commandText: '/status',
620
957
  senderId: '1',
621
958
  dialogId: '1',
959
+ chatId: '40985',
622
960
  chatType: 'P',
623
961
  messageId: '490668',
624
962
  language: 'ru',
@@ -74,4 +74,42 @@ describe('PollingService', () => {
74
74
  expect(onEvent).toHaveBeenCalledTimes(2);
75
75
  expect(state.offset).toBe(11);
76
76
  });
77
+
78
+ it('passes withUserEvents through to fetch polling', async () => {
79
+ tempHome = mkdtempSync(join(tmpdir(), 'b24-polling-agent-'));
80
+ process.env.HOME = tempHome;
81
+
82
+ const abortController = new AbortController();
83
+ const fetchEvents = vi.fn().mockImplementation(async () => {
84
+ abortController.abort();
85
+ return {
86
+ events: [],
87
+ lastEventId: 0,
88
+ hasMore: false,
89
+ };
90
+ });
91
+
92
+ const api = { fetchEvents } as unknown as Bitrix24Api;
93
+ const service = new PollingService({
94
+ api,
95
+ webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
96
+ bot: { botId: 7, botToken: 'bot_token' },
97
+ accountId: 'default',
98
+ pollingIntervalMs: 1,
99
+ pollingFastIntervalMs: 1,
100
+ withUserEvents: true,
101
+ onEvent: vi.fn(),
102
+ abortSignal: abortController.signal,
103
+ logger: silentLogger,
104
+ });
105
+
106
+ await service.start();
107
+
108
+ expect(fetchEvents).toHaveBeenCalledOnce();
109
+ expect(fetchEvents).toHaveBeenCalledWith(
110
+ 'https://test.bitrix24.com/rest/1/token/',
111
+ { botId: 7, botToken: 'bot_token' },
112
+ { offset: 0, limit: 100, withUserEvents: true },
113
+ );
114
+ });
77
115
  });