Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
107 lines
4.2 KiB
JavaScript
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
|
|
});
|
|
});
|