import { describe, expect, it } from 'vitest' import { CHAT_TRUNCATION_MARKER, fitChatRequest, LLM_PROXY_CHAT_LIMITS, toChatRequest, toRoleMessages, } from '../src/llm-chat' describe('toChatRequest', () => { it('separates system messages from turns and keeps turn order', () => { expect(toChatRequest([ { role: 'system', content: 'persona' }, { role: 'user', content: 'hi' }, { role: 'assistant', content: 'hello' }, { role: 'user', content: 'again' }, ])).toEqual({ system: 'persona', turns: [ { role: 'user', content: 'hi' }, { role: 'assistant', content: 'hello' }, { role: 'user', content: 'again' }, ], }) }) it('joins several system messages and drops non-chat roles', () => { expect(toChatRequest([ { role: 'system', content: 'a' }, { role: 'tool', content: 'ignored' }, { role: 'system', content: 'b' }, { role: 'system', content: ' ' }, { role: 'user', content: 'q' }, ])).toEqual({ system: 'a\n\nb', turns: [{ role: 'user', content: 'q' }] }) }) it('omits system when there is none', () => { expect(toChatRequest([{ role: 'user', content: 'q' }])).toEqual({ turns: [{ role: 'user', content: 'q' }], }) }) }) describe('toRoleMessages', () => { it('round-trips a canonical Ollama message list unchanged', () => { const messages = [ { role: 'system', content: 'persona' }, { role: 'user', content: 'hi' }, { role: 'assistant', content: 'hello' }, ] expect(toRoleMessages(toChatRequest(messages))).toEqual(messages) }) it('puts the system message first', () => { expect(toRoleMessages({ system: 's', turns: [{ role: 'user', content: 'u' }] })).toEqual([ { role: 'system', content: 's' }, { role: 'user', content: 'u' }, ]) }) }) describe('fitChatRequest', () => { const limits = { maxSystemChars: 50, maxMessageChars: 20, maxMessages: 3, maxTotalChars: 40 } it('keeps the head of an over-long system prompt within the limit', () => { const system = `INSTRUCTIONS ${'x'.repeat(200)}` const fitted = fitChatRequest({ system, turns: [{ role: 'user', content: 'q' }] }, limits) expect(fitted.system?.length).toBeLessThanOrEqual(limits.maxSystemChars) expect(fitted.system?.startsWith('INSTRUCTIONS')).toBe(true) expect(fitted.system?.endsWith(CHAT_TRUNCATION_MARKER)).toBe(true) }) it('drops empty turns such as an empty assistant reply', () => { const fitted = fitChatRequest({ turns: [ { role: 'user', content: 'q1' }, { role: 'assistant', content: '' }, { role: 'user', content: 'q2' }, ], }, limits) expect(fitted.turns).toEqual([ { role: 'user', content: 'q1' }, { role: 'user', content: 'q2' }, ]) }) it('keeps the newest turns within count and size limits and never starts with assistant', () => { const fitted = fitChatRequest({ turns: [ { role: 'user', content: 'u1' }, { role: 'assistant', content: 'a1' }, { role: 'user', content: 'u2' }, { role: 'assistant', content: 'a2' }, { role: 'user', content: 'u3' }, ], }, limits) // maxMessages=3 keeps [u2, a2, u3] expect(fitted.turns.map((t) => t.content)).toEqual(['u2', 'a2', 'u3']) const trimmed = fitChatRequest({ turns: [ { role: 'user', content: 'u'.repeat(20) }, { role: 'assistant', content: 'a'.repeat(15) }, { role: 'user', content: 'q'.repeat(20) }, ], }, limits) // total 40: [a(15), q(20)] fits, then the leading assistant turn is dropped expect(trimmed.turns).toEqual([{ role: 'user', content: 'q'.repeat(20) }]) }) it('truncates a single turn over the per-message limit', () => { const fitted = fitChatRequest({ turns: [{ role: 'user', content: 'z'.repeat(100) }] }, limits) expect(fitted.turns[0].content.length).toBeLessThanOrEqual(limits.maxMessageChars) }) it('exposes the llm-proxy limits', () => { expect(LLM_PROXY_CHAT_LIMITS).toEqual({ maxSystemChars: 8_000, maxMessageChars: 8_000, maxMessages: 40, maxTotalChars: 50_000, }) }) })