godcrm/backend/services/__tests__/telegram-chunking.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

107 lines
4.2 KiB
JavaScript

// @vitest-environment node
/**
* Guard for the Telegram 4096-char split path.
*
* Telegram's sendMessage REJECTS (does not truncate) any text over 4096 chars.
* Long kitchen/voice EN dubs were silently dropped because of this. sendMessage
* now splits oversized text on line boundaries and sends the parts in order,
* while keeping the {success, messageId} contract and leaving the within-limit
* path byte-identical.
*
* Pins:
* - within-limit text → exactly one send, unchanged
* - oversized text → split on line boundaries, every part <= 4096
* - a single oversized line → hard-sliced, never dropped
* - sendMessage splits into N fetch calls, returns the FIRST part's messageId
* - a failed part surfaces immediately and stops the drip
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('../secrets/getSecret.js', () => ({
getSecret: vi.fn().mockResolvedValue('TESTTOKEN'),
}));
const { splitTelegramText, sendMessage, TG_MAX_LEN } = await import('../TelegramService.js');
describe('splitTelegramText', () => {
it('returns within-limit text as a single unchanged chunk', () => {
const t = 'a'.repeat(TG_MAX_LEN);
expect(splitTelegramText(t)).toEqual([t]);
});
it('splits oversized text into parts that are each <= limit, losing no content', () => {
// No trailing newline and no oversized line → join('\n') reconstructs exactly,
// because each chunk boundary drops exactly one '\n' that join re-adds.
const para = Array(500).fill('line of kitchen prose').join('\n'); // ~11k chars
const parts = splitTelegramText(para);
expect(parts.length).toBeGreaterThan(1);
for (const p of parts) expect(p.length).toBeLessThanOrEqual(TG_MAX_LEN);
expect(parts.join('\n')).toBe(para);
});
it('breaks on line boundaries, not mid-line, when lines fit', () => {
const line = 'x'.repeat(100);
const text = Array(60).fill(line).join('\n'); // 60 lines, ~6k chars
const parts = splitTelegramText(text, 1000);
for (const p of parts) {
for (const l of p.split('\n')) expect(l).toBe(line); // every line intact
}
});
it('hard-slices a single line longer than the limit', () => {
const huge = 'y'.repeat(10000); // one line, no newlines
const parts = splitTelegramText(huge, 4096);
expect(parts.length).toBe(3);
for (const p of parts) expect(p.length).toBeLessThanOrEqual(4096);
expect(parts.join('')).toBe(huge);
});
});
describe('sendMessage chunking', () => {
beforeEach(() => { vi.unstubAllGlobals(); });
const okFetch = (idStart = 10) => {
let id = idStart;
return vi.fn().mockImplementation(async () => ({
json: async () => ({ ok: true, result: { message_id: id++ } }),
}));
};
it('sends within-limit text in exactly one call', async () => {
const f = okFetch();
vi.stubGlobal('fetch', f);
const res = await sendMessage('@chan', 'short', { parse_mode: undefined });
expect(res).toEqual({ success: true, messageId: 10 });
expect(f).toHaveBeenCalledTimes(1);
});
it('splits oversized text into multiple sends and returns the first messageId', async () => {
const f = okFetch(100);
vi.stubGlobal('fetch', f);
const big = 'paragraph\n'.repeat(900); // ~9k chars → >1 part
const res = await sendMessage('@chan', big);
expect(res.success).toBe(true);
expect(res.messageId).toBe(100); // first part
expect(f.mock.calls.length).toBeGreaterThan(1);
// every part body is within the limit
for (const call of f.mock.calls) {
const body = JSON.parse(call[1].body);
expect(body.text.length).toBeLessThanOrEqual(TG_MAX_LEN);
}
});
it('surfaces the first failed part and stops sending the rest', async () => {
let n = 0;
const f = vi.fn().mockImplementation(async () => {
n++;
if (n === 1) return { json: async () => ({ ok: true, result: { message_id: 1 } }) };
return { json: async () => ({ ok: false, description: 'message is too long' }) };
});
vi.stubGlobal('fetch', f);
const big = 'paragraph\n'.repeat(900);
const res = await sendMessage('@chan', big);
expect(res.success).toBe(false);
expect(res.error).toMatch(/too long/);
expect(f).toHaveBeenCalledTimes(2); // first ok, second failed → stop
});
});