import { describe, it, expect } from 'vitest'; import { ASSISTANT_NAME, TRIGGER_PATTERN } from './config.js'; import { escapeXml, formatMessages, formatOutbound, stripInternalTags, } from './router.js'; import { NewMessage } from './types.js'; function makeMsg(overrides: Partial = {}): NewMessage { return { id: '1', chat_jid: 'group@g.us', sender: '123@s.whatsapp.net', sender_name: 'Alice', content: 'hello', timestamp: '2024-01-01T00:00:00.000Z', ...overrides, }; } // --- escapeXml --- describe('escapeXml', () => { it('escapes ampersands', () => { expect(escapeXml('a & b')).toBe('a & b'); }); it('escapes less-than', () => { expect(escapeXml('a < b')).toBe('a < b'); }); it('escapes greater-than', () => { expect(escapeXml('a > b')).toBe('a > b'); }); it('escapes double quotes', () => { expect(escapeXml('"hello"')).toBe('"hello"'); }); it('handles multiple special characters together', () => { expect(escapeXml('a & b < c > d "e"')).toBe( 'a & b < c > d "e"', ); }); it('passes through strings with no special chars', () => { expect(escapeXml('hello world')).toBe('hello world'); }); it('handles empty string', () => { expect(escapeXml('')).toBe(''); }); }); // --- formatMessages --- describe('formatMessages', () => { it('formats a single message as XML', () => { const result = formatMessages([makeMsg()]); expect(result).toBe( '\n' + 'hello\n' + '', ); }); it('formats multiple messages', () => { const msgs = [ makeMsg({ id: '1', sender_name: 'Alice', content: 'hi', timestamp: 't1', }), makeMsg({ id: '2', sender_name: 'Bob', content: 'hey', timestamp: 't2' }), ]; const result = formatMessages(msgs); expect(result).toContain('sender="Alice"'); expect(result).toContain('sender="Bob"'); expect(result).toContain('>hi'); expect(result).toContain('>hey'); }); it('escapes special characters in sender names', () => { const result = formatMessages([makeMsg({ sender_name: 'A & B ' })]); expect(result).toContain('sender="A & B <Co>"'); }); it('escapes special characters in content', () => { const result = formatMessages([ makeMsg({ content: '' }), ]); expect(result).toContain( '<script>alert("xss")</script>', ); }); it('handles empty array', () => { const result = formatMessages([]); expect(result).toBe('\n\n'); }); }); // --- TRIGGER_PATTERN --- describe('TRIGGER_PATTERN', () => { const name = ASSISTANT_NAME; const lower = name.toLowerCase(); const upper = name.toUpperCase(); it('matches @name at start of message', () => { expect(TRIGGER_PATTERN.test(`@${name} hello`)).toBe(true); }); it('matches case-insensitively', () => { expect(TRIGGER_PATTERN.test(`@${lower} hello`)).toBe(true); expect(TRIGGER_PATTERN.test(`@${upper} hello`)).toBe(true); }); it('does not match when not at start of message', () => { expect(TRIGGER_PATTERN.test(`hello @${name}`)).toBe(false); }); it('does not match partial name like @NameExtra (word boundary)', () => { expect(TRIGGER_PATTERN.test(`@${name}extra hello`)).toBe(false); }); it('matches with word boundary before apostrophe', () => { expect(TRIGGER_PATTERN.test(`@${name}'s thing`)).toBe(true); }); it('matches @name alone (end of string is a word boundary)', () => { expect(TRIGGER_PATTERN.test(`@${name}`)).toBe(true); }); it('matches with leading whitespace after trim', () => { // The actual usage trims before testing: TRIGGER_PATTERN.test(m.content.trim()) expect(TRIGGER_PATTERN.test(`@${name} hey`.trim())).toBe(true); }); }); // --- Outbound formatting (internal tag stripping + prefix) --- describe('stripInternalTags', () => { it('strips single-line internal tags', () => { expect(stripInternalTags('hello secret world')).toBe( 'hello world', ); }); it('strips multi-line internal tags', () => { expect( stripInternalTags('hello \nsecret\nstuff\n world'), ).toBe('hello world'); }); it('strips multiple internal tag blocks', () => { expect( stripInternalTags('ahellob'), ).toBe('hello'); }); it('returns empty string when text is only internal tags', () => { expect(stripInternalTags('only this')).toBe(''); }); }); describe('formatOutbound', () => { it('returns text with internal tags stripped', () => { expect(formatOutbound('hello world')).toBe('hello world'); }); it('returns empty string when all text is internal', () => { expect(formatOutbound('hidden')).toBe(''); }); it('strips internal tags from remaining text', () => { expect( formatOutbound('thinkingThe answer is 42'), ).toBe('The answer is 42'); }); }); // --- Trigger gating with requiresTrigger flag --- describe('trigger gating (requiresTrigger interaction)', () => { // Replicates the exact logic from processGroupMessages and startMessageLoop: // if (!isMainGroup && group.requiresTrigger !== false) { check trigger } function shouldRequireTrigger( isMainGroup: boolean, requiresTrigger: boolean | undefined, ): boolean { return !isMainGroup && requiresTrigger !== false; } function shouldProcess( isMainGroup: boolean, requiresTrigger: boolean | undefined, messages: NewMessage[], ): boolean { if (!shouldRequireTrigger(isMainGroup, requiresTrigger)) return true; return messages.some((m) => TRIGGER_PATTERN.test(m.content.trim())); } it('main group always processes (no trigger needed)', () => { const msgs = [makeMsg({ content: 'hello no trigger' })]; expect(shouldProcess(true, undefined, msgs)).toBe(true); }); it('main group processes even with requiresTrigger=true', () => { const msgs = [makeMsg({ content: 'hello no trigger' })]; expect(shouldProcess(true, true, msgs)).toBe(true); }); it('non-main group with requiresTrigger=undefined requires trigger (defaults to true)', () => { const msgs = [makeMsg({ content: 'hello no trigger' })]; expect(shouldProcess(false, undefined, msgs)).toBe(false); }); it('non-main group with requiresTrigger=true requires trigger', () => { const msgs = [makeMsg({ content: 'hello no trigger' })]; expect(shouldProcess(false, true, msgs)).toBe(false); }); it('non-main group with requiresTrigger=true processes when trigger present', () => { const msgs = [makeMsg({ content: `@${ASSISTANT_NAME} do something` })]; expect(shouldProcess(false, true, msgs)).toBe(true); }); it('non-main group with requiresTrigger=false always processes (no trigger needed)', () => { const msgs = [makeMsg({ content: 'hello no trigger' })]; expect(shouldProcess(false, false, msgs)).toBe(true); }); });