import { describe, it, expect } from 'vitest' import { CompletionMessagesBuilder } from '../messages' import { ThreadMessage } from '@janhq/core' import { ChatCompletionMessageToolCall } from 'openai/resources' // Mock thread messages for testing const createMockThreadMessage = ( role: 'user' | 'assistant' | 'system', content: string, hasError = false ): ThreadMessage => ({ id: 'msg-123', object: 'thread.message', thread_id: 'thread-123', role, content: [ { type: 'text' as any, text: { value: content, annotations: [], }, }, ], status: 'completed' as any, created_at: Date.now(), completed_at: Date.now(), metadata: hasError ? { error: true } : {}, }) describe('CompletionMessagesBuilder', () => { describe('constructor', () => { it('should initialize with empty messages array when no system instruction', () => { const messages: ThreadMessage[] = [] const builder = new CompletionMessagesBuilder(messages) expect(builder.getMessages()).toEqual([]) }) it('should add system message when system instruction provided', () => { const messages: ThreadMessage[] = [] const systemInstruction = 'You are a helpful assistant.' const builder = new CompletionMessagesBuilder(messages, systemInstruction) const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0]).toEqual({ role: 'system', content: systemInstruction, }) }) it('should filter out messages with errors', () => { const messages: ThreadMessage[] = [ createMockThreadMessage('user', 'Hello', false), createMockThreadMessage('assistant', 'Hi there', true), // has error createMockThreadMessage('user', 'How are you?', false), ] const builder = new CompletionMessagesBuilder(messages) const result = builder.getMessages() expect(result).toHaveLength(2) expect(result[0].content).toBe('Hello') expect(result[1].content).toBe('How are you?') }) it('should normalize assistant message content', () => { const messages: ThreadMessage[] = [ createMockThreadMessage( 'assistant', 'Let me think...Hello there!' ), ] const builder = new CompletionMessagesBuilder(messages) const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0].content).toBe('Hello there!') }) it('should preserve user message content without normalization', () => { const messages: ThreadMessage[] = [ createMockThreadMessage( 'user', 'This should not be normalizedHello' ), ] const builder = new CompletionMessagesBuilder(messages) const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0].content).toBe( 'This should not be normalizedHello' ) }) it('should handle messages with empty content', () => { const message: ThreadMessage = { ...createMockThreadMessage('user', ''), content: [{ type: 'text' as any, text: undefined }], } const builder = new CompletionMessagesBuilder([message]) const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0].content).toBe('.') }) it('should handle messages with missing text value', () => { const message: ThreadMessage = { ...createMockThreadMessage('user', ''), content: [ { type: 'text' as any, text: { value: '', annotations: [] } }, ], } const builder = new CompletionMessagesBuilder([message]) const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0].content).toBe('.') }) }) describe('addUserMessage', () => { it('should add user message to messages array', () => { const builder = new CompletionMessagesBuilder([]) builder.addUserMessage('Hello, how are you?') const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0]).toEqual({ role: 'user', content: 'Hello, how are you?', }) }) it('should not add consecutive user messages', () => { const builder = new CompletionMessagesBuilder([]) builder.addUserMessage('First message') builder.addUserMessage('Second message') const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0].content).toBe('Second message') }) it('should handle empty user message', () => { const builder = new CompletionMessagesBuilder([]) builder.addUserMessage('') const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0].content).toBe('') }) }) describe('addAssistantMessage', () => { it('should add assistant message with normalized content', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage('Processing...Hello!') const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0]).toEqual({ role: 'assistant', content: 'Hello!', refusal: undefined, tool_calls: undefined, }) }) it('should add assistant message with refusal', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( 'I cannot help with that', 'Content policy violation' ) const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0]).toEqual({ role: 'assistant', content: 'I cannot help with that', refusal: 'Content policy violation', tool_calls: undefined, }) }) it('should add assistant message with tool calls', () => { const builder = new CompletionMessagesBuilder([]) const toolCalls: ChatCompletionMessageToolCall[] = [ { id: 'call_123', type: 'function', function: { name: 'get_weather', arguments: '{"location": "New York"}', }, }, ] builder.addAssistantMessage( 'Let me check the weather', undefined, toolCalls ) const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0]).toEqual({ role: 'assistant', content: 'Let me check the weather', refusal: undefined, tool_calls: toolCalls, }) }) it('should handle assistant message with all parameters', () => { const builder = new CompletionMessagesBuilder([]) const toolCalls: ChatCompletionMessageToolCall[] = [ { id: 'call_456', type: 'function', function: { name: 'search', arguments: '{"query": "test"}', }, }, ] builder.addAssistantMessage( 'Searching...Here are the results', 'Cannot search sensitive content', toolCalls ) const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0]).toEqual({ role: 'assistant', content: 'Here are the results', refusal: 'Cannot search sensitive content', tool_calls: toolCalls, }) }) }) describe('addToolMessage', () => { it('should add tool message with correct format', () => { const builder = new CompletionMessagesBuilder([]) builder.addToolMessage('Weather data: 72°F', 'call_123') const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0]).toEqual({ role: 'tool', content: 'Weather data: 72°F', tool_call_id: 'call_123', }) }) it('should add multiple tool messages', () => { const builder = new CompletionMessagesBuilder([]) builder.addToolMessage('First tool result', 'call_1') builder.addToolMessage('Second tool result', 'call_2') const result = builder.getMessages() expect(result).toHaveLength(2) expect(result[0].tool_call_id).toBe('call_1') expect(result[1].tool_call_id).toBe('call_2') }) it('should handle empty tool content', () => { const builder = new CompletionMessagesBuilder([]) builder.addToolMessage('', 'call_123') const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0].content).toBe('') expect(result[0].tool_call_id).toBe('call_123') }) }) describe('getMessages', () => { it('should return messages in correct order', () => { const threadMessages: ThreadMessage[] = [ createMockThreadMessage('user', 'Hello'), ] const builder = new CompletionMessagesBuilder( threadMessages, 'You are helpful' ) builder.addUserMessage('How are you?') builder.addAssistantMessage('I am well, thank you!') builder.addToolMessage('Tool response', 'call_123') const result = builder.getMessages() expect(result).toHaveLength(4) expect(result[0].role).toBe('system') expect(result[1].role).toBe('user') expect(result[2].role).toBe('assistant') expect(result[3].role).toBe('tool') }) it('should return the same array reference (not immutable)', () => { const builder = new CompletionMessagesBuilder([]) builder.addUserMessage('Test message') const result1 = builder.getMessages() builder.addAssistantMessage('Response') const result2 = builder.getMessages() // Both should reference the same array and have 2 messages now expect(result1).toBe(result2) // Same reference expect(result1).toHaveLength(2) expect(result2).toHaveLength(2) }) }) describe('content normalization', () => { it('should remove thinking content from the beginning', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( 'Let me analyze this...The answer is 42.' ) const result = builder.getMessages() expect(result[0].content).toBe('The answer is 42.') }) it('should handle nested thinking tags', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( 'First thoughtNestedMore thinkingFinal answer' ) const result = builder.getMessages() expect(result[0].content).toBe('More thinkingFinal answer') }) it('should handle multiple thinking blocks', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( 'FirstAnswerSecondMore content' ) const result = builder.getMessages() expect(result[0].content).toBe('AnswerSecondMore content') }) it('should handle content without thinking tags', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage('Just a normal response') const result = builder.getMessages() expect(result[0].content).toBe('Just a normal response') }) it('should handle empty content after removing thinking', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage('Only thinking content') const result = builder.getMessages() expect(result[0].content).toBe('') }) it('should handle unclosed thinking tags', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( 'Unclosed thinking tag... Regular content' ) const result = builder.getMessages() expect(result[0].content).toBe( 'Unclosed thinking tag... Regular content' ) }) it('should handle thinking tags with whitespace', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( ' \n Some thinking \n \n Clean answer' ) const result = builder.getMessages() expect(result[0].content).toBe('Clean answer') }) it('should remove analysis channel reasoning content', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( '<|channel|>analysis<|message|>Let me analyze this step by step...<|start|>assistant<|channel|>final<|message|>The final answer is 42.' ) const result = builder.getMessages() expect(result[0].content).toBe('The final answer is 42.') }) it('should handle analysis channel without final message', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( '<|channel|>analysis<|message|>Only analysis content here...' ) const result = builder.getMessages() expect(result[0].content).toBe('<|channel|>analysis<|message|>Only analysis content here...') }) it('should handle analysis channel with multiline content', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( '<|channel|>analysis<|message|>Step 1: First analysis\nStep 2: Second analysis\nStep 3: Final analysis<|start|>assistant<|channel|>final<|message|>Based on my analysis, here is the result.' ) const result = builder.getMessages() expect(result[0].content).toBe('Based on my analysis, here is the result.') }) it('should handle both think and analysis channel tags', () => { const builder = new CompletionMessagesBuilder([]) builder.addAssistantMessage( 'Initial thought<|channel|>analysis<|message|>Detailed analysis<|start|>assistant<|channel|>final<|message|>Final response' ) const result = builder.getMessages() expect(result[0].content).toBe('Final response') }) }) describe('integration tests', () => { it('should handle complex conversation flow', () => { const threadMessages: ThreadMessage[] = [ createMockThreadMessage('user', 'What is the weather like?'), createMockThreadMessage( 'assistant', 'I need to call weather APILet me check the weather for you.' ), ] const builder = new CompletionMessagesBuilder( threadMessages, 'You are a weather assistant' ) // Add tool call and response const toolCalls: ChatCompletionMessageToolCall[] = [ { id: 'call_weather', type: 'function', function: { name: 'get_weather', arguments: '{"location": "user_location"}', }, }, ] builder.addAssistantMessage( 'Calling weather service...', undefined, toolCalls ) builder.addToolMessage( '{"temperature": 72, "condition": "sunny"}', 'call_weather' ) builder.addAssistantMessage( 'The weather is niceThe weather is 72°F and sunny!' ) const result = builder.getMessages() expect(result).toHaveLength(6) expect(result[0].role).toBe('system') expect(result[1].role).toBe('user') expect(result[2].role).toBe('assistant') expect(result[2].content).toBe('Let me check the weather for you.') expect(result[3].role).toBe('assistant') expect(result[3].tool_calls).toEqual(toolCalls) expect(result[4].role).toBe('tool') expect(result[5].role).toBe('assistant') expect(result[5].content).toBe('The weather is 72°F and sunny!') }) it('should handle empty thread messages with system instruction', () => { const builder = new CompletionMessagesBuilder([], 'System instruction') const result = builder.getMessages() expect(result).toHaveLength(1) expect(result[0]).toEqual({ role: 'system', content: 'System instruction', }) }) }) })