@ihazz/bitrix24 0.2.5 → 1.0.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.
- package/README.md +118 -164
- package/index.ts +46 -11
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/skills/bitrix24/SKILL.md +70 -0
- package/src/access-control.ts +102 -48
- package/src/api.ts +434 -232
- package/src/channel.ts +1441 -365
- package/src/commands.ts +169 -31
- package/src/config-schema.ts +8 -3
- package/src/config.ts +11 -0
- package/src/dedup.ts +4 -0
- package/src/i18n.ts +127 -0
- package/src/inbound-handler.ts +306 -110
- package/src/media-service.ts +218 -65
- package/src/message-utils.ts +252 -10
- package/src/polling-service.ts +240 -0
- package/src/rate-limiter.ts +11 -6
- package/src/send-service.ts +140 -60
- package/src/types.ts +279 -185
- package/src/utils.ts +54 -3
- package/tests/access-control.test.ts +174 -58
- package/tests/api.test.ts +95 -0
- package/tests/channel.test.ts +230 -9
- package/tests/commands.test.ts +57 -0
- package/tests/config.test.ts +5 -1
- package/tests/i18n.test.ts +47 -0
- package/tests/inbound-handler.test.ts +554 -69
- package/tests/index.test.ts +94 -0
- package/tests/media-service.test.ts +146 -51
- package/tests/message-utils.test.ts +64 -0
- package/tests/polling-service.test.ts +77 -0
- package/tests/rate-limiter.test.ts +2 -2
- package/tests/send-service.test.ts +145 -0
package/tests/channel.test.ts
CHANGED
|
@@ -2,9 +2,16 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { Readable, PassThrough } from 'node:stream';
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
4
|
import {
|
|
5
|
+
__setGatewayStateForTests,
|
|
6
|
+
buildBotCodeCandidates,
|
|
7
|
+
canCoalesceDirectMessage,
|
|
5
8
|
convertButtonsToKeyboard,
|
|
6
9
|
extractKeyboardFromPayload,
|
|
7
10
|
handleWebhookRequest,
|
|
11
|
+
mergeBufferedDirectMessages,
|
|
12
|
+
mergeForwardedMessageContext,
|
|
13
|
+
resolveDirectMessageCoalesceDelay,
|
|
14
|
+
shouldSkipJoinChatWelcome,
|
|
8
15
|
bitrix24Plugin,
|
|
9
16
|
} from '../src/channel.js';
|
|
10
17
|
import type { KeyboardButton, KeyboardNewline, B24Keyboard } from '../src/types.js';
|
|
@@ -26,6 +33,26 @@ function isNewline(item: KeyboardButton | KeyboardNewline): item is KeyboardNewl
|
|
|
26
33
|
return 'TYPE' in item && item.TYPE === 'NEWLINE';
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
describe('buildBotCodeCandidates', () => {
|
|
37
|
+
it('uses explicit botCode when it is configured', () => {
|
|
38
|
+
expect(buildBotCodeCandidates({
|
|
39
|
+
webhookUrl: 'https://test.bitrix24.com/rest/42/token/',
|
|
40
|
+
botCode: 'custom_bot',
|
|
41
|
+
}, 5)).toEqual(['custom_bot']);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('builds automatic codes from webhook user id', () => {
|
|
45
|
+
expect(buildBotCodeCandidates({
|
|
46
|
+
webhookUrl: 'https://test.bitrix24.com/rest/42/token/',
|
|
47
|
+
}, 4)).toEqual([
|
|
48
|
+
'openclaw_42',
|
|
49
|
+
'openclaw_42_2',
|
|
50
|
+
'openclaw_42_3',
|
|
51
|
+
'openclaw_42_4',
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
29
56
|
// ─── convertButtonsToKeyboard ────────────────────────────────────────────────
|
|
30
57
|
|
|
31
58
|
describe('convertButtonsToKeyboard', () => {
|
|
@@ -284,9 +311,124 @@ describe('extractKeyboardFromPayload', () => {
|
|
|
284
311
|
});
|
|
285
312
|
});
|
|
286
313
|
|
|
314
|
+
describe('direct message coalescing helpers', () => {
|
|
315
|
+
const baseMsgCtx = {
|
|
316
|
+
channel: 'bitrix24' as const,
|
|
317
|
+
senderId: '1',
|
|
318
|
+
senderName: 'Test User',
|
|
319
|
+
senderFirstName: 'Test',
|
|
320
|
+
chatId: '1',
|
|
321
|
+
chatInternalId: '40985',
|
|
322
|
+
messageId: '100',
|
|
323
|
+
text: 'hello',
|
|
324
|
+
isDm: true,
|
|
325
|
+
isGroup: false,
|
|
326
|
+
media: [],
|
|
327
|
+
language: 'ru',
|
|
328
|
+
raw: { type: 'ONIMBOTV2MESSAGEADD' },
|
|
329
|
+
botId: 295,
|
|
330
|
+
memberId: '',
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
it('coalesces only plain direct text messages', () => {
|
|
334
|
+
expect(canCoalesceDirectMessage(baseMsgCtx, { dmPolicy: 'webhookUser' })).toBe(true);
|
|
335
|
+
expect(canCoalesceDirectMessage({ ...baseMsgCtx, text: '/status' }, { dmPolicy: 'webhookUser' })).toBe(false);
|
|
336
|
+
expect(canCoalesceDirectMessage({ ...baseMsgCtx, media: [{ id: '1', name: 'x', extension: '', size: 1, type: 'file' }] }, { dmPolicy: 'webhookUser' })).toBe(false);
|
|
337
|
+
expect(canCoalesceDirectMessage({ ...baseMsgCtx, replyToMessageId: '77' }, { dmPolicy: 'webhookUser' })).toBe(false);
|
|
338
|
+
expect(canCoalesceDirectMessage({ ...baseMsgCtx, isForwarded: true }, { dmPolicy: 'webhookUser' })).toBe(false);
|
|
339
|
+
expect(canCoalesceDirectMessage({ ...baseMsgCtx, isDm: false, isGroup: true }, { dmPolicy: 'webhookUser' })).toBe(false);
|
|
340
|
+
expect(canCoalesceDirectMessage(baseMsgCtx, { dmPolicy: 'pairing' })).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('merges buffered direct messages with newline separators and last message id', () => {
|
|
344
|
+
const merged = mergeBufferedDirectMessages([
|
|
345
|
+
baseMsgCtx,
|
|
346
|
+
{ ...baseMsgCtx, messageId: '101', text: 'second' },
|
|
347
|
+
{ ...baseMsgCtx, messageId: '102', text: 'third', language: 'en' },
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
expect(merged.text).toBe('hello\nsecond\nthird');
|
|
351
|
+
expect(merged.messageId).toBe('102');
|
|
352
|
+
expect(merged.language).toBe('en');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('merges a forwarded message with buffered question context', () => {
|
|
356
|
+
const merged = mergeForwardedMessageContext(
|
|
357
|
+
baseMsgCtx,
|
|
358
|
+
{
|
|
359
|
+
...baseMsgCtx,
|
|
360
|
+
messageId: '103',
|
|
361
|
+
text: 'Forwarded body',
|
|
362
|
+
media: [{ id: '1', name: 'photo.jpg', extension: '', size: 42, type: 'file' }],
|
|
363
|
+
isForwarded: true,
|
|
364
|
+
},
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
expect(merged.text).toBe('hello\n\n[Forwarded message]\nForwarded body');
|
|
368
|
+
expect(merged.messageId).toBe('103');
|
|
369
|
+
expect(merged.media).toHaveLength(1);
|
|
370
|
+
expect(merged.isForwarded).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('uses short debounce and extends the window until max wait', () => {
|
|
374
|
+
expect(resolveDirectMessageCoalesceDelay({
|
|
375
|
+
startedAt: 1000,
|
|
376
|
+
now: 1000,
|
|
377
|
+
})).toBe(200);
|
|
378
|
+
|
|
379
|
+
expect(resolveDirectMessageCoalesceDelay({
|
|
380
|
+
startedAt: 1000,
|
|
381
|
+
now: 1150,
|
|
382
|
+
})).toBe(200);
|
|
383
|
+
|
|
384
|
+
expect(resolveDirectMessageCoalesceDelay({
|
|
385
|
+
startedAt: 1000,
|
|
386
|
+
now: 5900,
|
|
387
|
+
})).toBe(100);
|
|
388
|
+
|
|
389
|
+
expect(resolveDirectMessageCoalesceDelay({
|
|
390
|
+
startedAt: 1000,
|
|
391
|
+
now: 6200,
|
|
392
|
+
})).toBe(0);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('join chat helpers', () => {
|
|
397
|
+
it('does not skip unsupported group chats even in webhookUser mode', () => {
|
|
398
|
+
expect(shouldSkipJoinChatWelcome({
|
|
399
|
+
dialogId: 'chat123',
|
|
400
|
+
chatType: 'chat',
|
|
401
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
402
|
+
dmPolicy: 'webhookUser',
|
|
403
|
+
})).toBe(false);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('skips welcome for non-owner direct dialogs in webhookUser mode', () => {
|
|
407
|
+
expect(shouldSkipJoinChatWelcome({
|
|
408
|
+
dialogId: '42',
|
|
409
|
+
chatType: 'user',
|
|
410
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
411
|
+
dmPolicy: 'webhookUser',
|
|
412
|
+
})).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('does not skip welcome for owner direct dialogs in webhookUser mode', () => {
|
|
416
|
+
expect(shouldSkipJoinChatWelcome({
|
|
417
|
+
dialogId: '1',
|
|
418
|
+
chatType: 'user',
|
|
419
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
420
|
+
dmPolicy: 'webhookUser',
|
|
421
|
+
})).toBe(false);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
287
425
|
// ─── handleWebhookRequest ────────────────────────────────────────────────────
|
|
288
426
|
|
|
289
427
|
describe('handleWebhookRequest', () => {
|
|
428
|
+
afterEach(() => {
|
|
429
|
+
__setGatewayStateForTests(null);
|
|
430
|
+
});
|
|
431
|
+
|
|
290
432
|
function mockReq(method: string, body?: string): IncomingMessage {
|
|
291
433
|
const stream = new PassThrough();
|
|
292
434
|
if (body) {
|
|
@@ -330,6 +472,71 @@ describe('handleWebhookRequest', () => {
|
|
|
330
472
|
expect(res._statusCode).toBe(503);
|
|
331
473
|
expect(res._body).toBe('Channel not started');
|
|
332
474
|
});
|
|
475
|
+
|
|
476
|
+
it('returns fetch mode acknowledgement when gateway runs in fetch mode', async () => {
|
|
477
|
+
__setGatewayStateForTests({
|
|
478
|
+
eventMode: 'fetch',
|
|
479
|
+
inboundHandler: { handleWebhook: vi.fn() },
|
|
480
|
+
} as never);
|
|
481
|
+
|
|
482
|
+
const req = mockReq('POST', '{"event":"test"}');
|
|
483
|
+
const res = mockRes();
|
|
484
|
+
|
|
485
|
+
await handleWebhookRequest(req, res);
|
|
486
|
+
|
|
487
|
+
expect(res._statusCode).toBe(200);
|
|
488
|
+
expect(res._body).toBe('FETCH mode active');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('returns 400 when webhook payload is invalid', async () => {
|
|
492
|
+
const handleWebhook = vi.fn().mockResolvedValue(false);
|
|
493
|
+
__setGatewayStateForTests({
|
|
494
|
+
eventMode: 'webhook',
|
|
495
|
+
inboundHandler: { handleWebhook },
|
|
496
|
+
} as never);
|
|
497
|
+
|
|
498
|
+
const req = mockReq('POST', 'not-json');
|
|
499
|
+
const res = mockRes();
|
|
500
|
+
|
|
501
|
+
await handleWebhookRequest(req, res);
|
|
502
|
+
|
|
503
|
+
expect(handleWebhook).toHaveBeenCalledWith('not-json');
|
|
504
|
+
expect(res._statusCode).toBe(400);
|
|
505
|
+
expect(res._body).toBe('Invalid webhook payload');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('returns 500 when webhook processing throws', async () => {
|
|
509
|
+
const handleWebhook = vi.fn().mockRejectedValue(new Error('boom'));
|
|
510
|
+
__setGatewayStateForTests({
|
|
511
|
+
eventMode: 'webhook',
|
|
512
|
+
inboundHandler: { handleWebhook },
|
|
513
|
+
} as never);
|
|
514
|
+
|
|
515
|
+
const req = mockReq('POST', '{"event":"test"}');
|
|
516
|
+
const res = mockRes();
|
|
517
|
+
|
|
518
|
+
await handleWebhookRequest(req, res);
|
|
519
|
+
|
|
520
|
+
expect(res._statusCode).toBe(500);
|
|
521
|
+
expect(res._body).toBe('Webhook processing failed');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('rejects oversized webhook bodies with 413', async () => {
|
|
525
|
+
const handleWebhook = vi.fn();
|
|
526
|
+
__setGatewayStateForTests({
|
|
527
|
+
eventMode: 'webhook',
|
|
528
|
+
inboundHandler: { handleWebhook },
|
|
529
|
+
} as never);
|
|
530
|
+
|
|
531
|
+
const req = mockReq('POST', 'x'.repeat((1024 * 1024) + 1));
|
|
532
|
+
const res = mockRes();
|
|
533
|
+
|
|
534
|
+
await handleWebhookRequest(req, res);
|
|
535
|
+
|
|
536
|
+
expect(handleWebhook).not.toHaveBeenCalled();
|
|
537
|
+
expect(res._statusCode).toBe(413);
|
|
538
|
+
expect(res._body).toBe('Payload Too Large');
|
|
539
|
+
});
|
|
333
540
|
});
|
|
334
541
|
|
|
335
542
|
// ─── bitrix24Plugin structure ────────────────────────────────────────────────
|
|
@@ -347,32 +554,46 @@ describe('bitrix24Plugin', () => {
|
|
|
347
554
|
});
|
|
348
555
|
|
|
349
556
|
it('declares capabilities', () => {
|
|
350
|
-
expect(bitrix24Plugin.capabilities.chatTypes).toEqual(['direct'
|
|
557
|
+
expect(bitrix24Plugin.capabilities.chatTypes).toEqual(['direct']);
|
|
351
558
|
expect(bitrix24Plugin.capabilities.media).toBe(true);
|
|
352
|
-
expect(bitrix24Plugin.capabilities.reactions).toBe(
|
|
559
|
+
expect(bitrix24Plugin.capabilities.reactions).toBe(true);
|
|
353
560
|
expect(bitrix24Plugin.capabilities.threads).toBe(false);
|
|
354
561
|
expect(bitrix24Plugin.capabilities.nativeCommands).toBe(true);
|
|
355
562
|
});
|
|
356
563
|
});
|
|
357
564
|
|
|
358
565
|
describe('security', () => {
|
|
359
|
-
it('resolveDmPolicy returns policy
|
|
566
|
+
it('resolveDmPolicy returns webhookUser policy when configured', () => {
|
|
360
567
|
const result = bitrix24Plugin.security.resolveDmPolicy({
|
|
361
|
-
account: { config: { dmPolicy: '
|
|
568
|
+
account: { config: { dmPolicy: 'webhookUser', allowFrom: ['bitrix24:42', '42', 'b24:7'] } },
|
|
362
569
|
});
|
|
363
|
-
expect(result.policy).toBe('
|
|
364
|
-
expect(result.allowFrom).toEqual(['
|
|
570
|
+
expect(result.policy).toBe('webhookUser');
|
|
571
|
+
expect(result.allowFrom).toEqual(['42', '7']);
|
|
365
572
|
expect(result.policyPath).toBe('channels.bitrix24.dmPolicy');
|
|
366
|
-
expect(result.allowFromPath).toBe('channels.bitrix24.');
|
|
573
|
+
expect(result.allowFromPath).toBe('channels.bitrix24.allowFrom');
|
|
367
574
|
expect(result.approveHint).toContain('openclaw pairing approve bitrix24');
|
|
368
575
|
});
|
|
369
576
|
|
|
370
|
-
it('resolveDmPolicy defaults to
|
|
577
|
+
it('resolveDmPolicy defaults to webhookUser', () => {
|
|
371
578
|
const result = bitrix24Plugin.security.resolveDmPolicy({});
|
|
372
|
-
expect(result.policy).toBe('
|
|
579
|
+
expect(result.policy).toBe('webhookUser');
|
|
373
580
|
expect(result.allowFrom).toEqual([]);
|
|
374
581
|
});
|
|
375
582
|
|
|
583
|
+
it('resolveDmPolicy reads allowFrom from cfg when account is omitted', () => {
|
|
584
|
+
const result = bitrix24Plugin.security.resolveDmPolicy({
|
|
585
|
+
cfg: {
|
|
586
|
+
channels: {
|
|
587
|
+
bitrix24: {
|
|
588
|
+
allowFrom: ['bx24:100', '100', 'bitrix24:42'],
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
expect(result.allowFrom).toEqual(['100', '42']);
|
|
595
|
+
});
|
|
596
|
+
|
|
376
597
|
it('resolveDmPolicy normalizeEntry strips prefixes', () => {
|
|
377
598
|
const result = bitrix24Plugin.security.resolveDmPolicy({});
|
|
378
599
|
expect(result.normalizeEntry('bitrix24:42')).toBe('42');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildCommandsHelpText, formatModelsCommandReply } from '../src/commands.js';
|
|
3
|
+
|
|
4
|
+
describe('buildCommandsHelpText', () => {
|
|
5
|
+
it('builds concise Russian help for keyboard commands', () => {
|
|
6
|
+
const text = buildCommandsHelpText('ru', { concise: true });
|
|
7
|
+
|
|
8
|
+
expect(text).toContain('[B]Основные команды[/B]');
|
|
9
|
+
expect(text).toContain('[send=/status]/status[/send]');
|
|
10
|
+
expect(text).toContain('[send=/commands]/commands[/send]');
|
|
11
|
+
expect(text).toContain('[put=/models ]/models[/put]');
|
|
12
|
+
expect(text).toContain('[COLOR=#6C788A]Полный список:[/COLOR] [send=/commands]');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('builds full Russian commands list', () => {
|
|
16
|
+
const text = buildCommandsHelpText('ru');
|
|
17
|
+
|
|
18
|
+
expect(text).toContain('[B]Доступные команды[/B]');
|
|
19
|
+
expect(text).toContain('[B]Справка и статус[/B]');
|
|
20
|
+
expect(text).toContain('[send=/help]/help[/send]');
|
|
21
|
+
expect(text).toContain('[send=/export-session]/export-session[/send]');
|
|
22
|
+
expect(text).toContain('[COLOR=#6C788A]Параметры: off | tokens | full | cost[/COLOR]');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('falls back to English text', () => {
|
|
26
|
+
const text = buildCommandsHelpText('en', { concise: true });
|
|
27
|
+
|
|
28
|
+
expect(text).toContain('[B]Key commands[/B]');
|
|
29
|
+
expect(text).toContain('[COLOR=#6C788A]Full list:[/COLOR] [send=/commands]');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('formatModelsCommandReply', () => {
|
|
34
|
+
it('formats provider summary into interactive Bitrix text', () => {
|
|
35
|
+
const text = formatModelsCommandReply(
|
|
36
|
+
[
|
|
37
|
+
'Providers:',
|
|
38
|
+
'• openai (36)',
|
|
39
|
+
'• anthropic (23)',
|
|
40
|
+
'',
|
|
41
|
+
'Use: /models <provider>',
|
|
42
|
+
'Switch: /model <provider/model>',
|
|
43
|
+
].join('\n'),
|
|
44
|
+
'ru',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(text).toContain('[B]Провайдеры[/B]');
|
|
48
|
+
expect(text).toContain('[send=/models openai]openai[/send]');
|
|
49
|
+
expect(text).toContain('[send=/models anthropic]anthropic[/send]');
|
|
50
|
+
expect(text).toContain('[put=/models ]/models <provider>[/put]');
|
|
51
|
+
expect(text).toContain('[put=/model ]/model <provider/model>[/put]');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns null for unrelated text', () => {
|
|
55
|
+
expect(formatModelsCommandReply('Status ok', 'ru')).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
package/tests/config.test.ts
CHANGED
|
@@ -6,11 +6,13 @@ const mockCfg = {
|
|
|
6
6
|
bitrix24: {
|
|
7
7
|
webhookUrl: 'https://test.bitrix24.com/rest/1/abc123/',
|
|
8
8
|
botName: 'TestBot',
|
|
9
|
-
dmPolicy: '
|
|
9
|
+
dmPolicy: 'pairing',
|
|
10
|
+
allowFrom: ['bitrix24:1'],
|
|
10
11
|
accounts: {
|
|
11
12
|
portal2: {
|
|
12
13
|
webhookUrl: 'https://portal2.bitrix24.com/rest/1/xyz789/',
|
|
13
14
|
botName: 'Portal2Bot',
|
|
15
|
+
allowFrom: ['bitrix24:2'],
|
|
14
16
|
},
|
|
15
17
|
},
|
|
16
18
|
},
|
|
@@ -22,12 +24,14 @@ describe('getConfig', () => {
|
|
|
22
24
|
const cfg = getConfig(mockCfg);
|
|
23
25
|
expect(cfg.webhookUrl).toBe('https://test.bitrix24.com/rest/1/abc123/');
|
|
24
26
|
expect(cfg.botName).toBe('TestBot');
|
|
27
|
+
expect(cfg.allowFrom).toEqual(['bitrix24:1']);
|
|
25
28
|
});
|
|
26
29
|
|
|
27
30
|
it('returns account-specific config', () => {
|
|
28
31
|
const cfg = getConfig(mockCfg, 'portal2');
|
|
29
32
|
expect(cfg.webhookUrl).toBe('https://portal2.bitrix24.com/rest/1/xyz789/');
|
|
30
33
|
expect(cfg.botName).toBe('Portal2Bot');
|
|
34
|
+
expect(cfg.allowFrom).toEqual(['bitrix24:2']);
|
|
31
35
|
});
|
|
32
36
|
|
|
33
37
|
it('returns empty config when no bitrix24 section', () => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
resolveLocale,
|
|
4
|
+
welcomeMessage,
|
|
5
|
+
onboardingMessage,
|
|
6
|
+
personalBotOwnerOnly,
|
|
7
|
+
forwardedMessageUnsupported,
|
|
8
|
+
replyMessageUnsupported,
|
|
9
|
+
} from '../src/i18n.js';
|
|
10
|
+
|
|
11
|
+
describe('i18n welcome messages', () => {
|
|
12
|
+
it('resolves locale to two-letter lowercase code', () => {
|
|
13
|
+
expect(resolveLocale('RU')).toBe('ru');
|
|
14
|
+
expect(resolveLocale('en_US')).toBe('en');
|
|
15
|
+
expect(resolveLocale(undefined)).toBe('en');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns localized Russian welcome message', () => {
|
|
19
|
+
expect(welcomeMessage('ru', 'OpenClaw')).toContain('OpenClaw готов');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('falls back to English welcome message for unknown locale', () => {
|
|
23
|
+
expect(welcomeMessage('zz', 'OpenClaw')).toBe(
|
|
24
|
+
'OpenClaw ready. Send me a message or pick a command below.',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns pairing onboarding message when dmPolicy is pairing', () => {
|
|
29
|
+
expect(onboardingMessage('ru', 'OpenClaw', 'pairing')).toContain('код привязки');
|
|
30
|
+
expect(onboardingMessage('en', 'OpenClaw', 'pairing')).toContain('pairing code');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns localized personal bot access denied message', () => {
|
|
34
|
+
expect(personalBotOwnerOnly('ru')).toContain('персональный');
|
|
35
|
+
expect(personalBotOwnerOnly('en')).toContain('personal');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns localized reply unsupported message', () => {
|
|
39
|
+
expect(replyMessageUnsupported('ru')).toContain('пока не поддерживаются');
|
|
40
|
+
expect(replyMessageUnsupported('en')).toContain('not supported');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns localized forwarded message unsupported message', () => {
|
|
44
|
+
expect(forwardedMessageUnsupported('ru')).toContain('Пересланные сообщения');
|
|
45
|
+
expect(forwardedMessageUnsupported('en')).toContain('Forwarded messages');
|
|
46
|
+
});
|
|
47
|
+
});
|