diff --git a/@packages/@types/src/api/messages.types.ts b/@packages/@types/src/api/messages.types.ts index af31906f1..c2a356347 100755 --- a/@packages/@types/src/api/messages.types.ts +++ b/@packages/@types/src/api/messages.types.ts @@ -95,6 +95,8 @@ export interface GeneratedResponse { generatedAt: string; status: ResponseStatus; rejectionReason: string | null; + theme?: string | null; + editedResponse?: string | null; createdAt: string; } diff --git a/features/conversation-assistant/backend-api/src/modules/responses/responses.service.spec.ts b/features/conversation-assistant/backend-api/src/modules/responses/responses.service.spec.ts index 01e4e4023..3d7a87d1a 100755 --- a/features/conversation-assistant/backend-api/src/modules/responses/responses.service.spec.ts +++ b/features/conversation-assistant/backend-api/src/modules/responses/responses.service.spec.ts @@ -2,12 +2,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { HttpService } from '@nestjs/axios'; import { NotFoundException, BadRequestException } from '@nestjs/common'; -import { of, throwError } from 'rxjs'; +import { of } from 'rxjs'; import { ResponsesService } from './responses.service'; import { GeneratedResponseEntity, TrainingSampleEntity, MessageEntity, + ContactEntity, + StyleProfileEntity, } from '@/entities'; import { ConversationsService } from '@/conversations/conversations.service'; import { @@ -22,18 +24,37 @@ describe('ResponsesService', () => { let responseRepository: ReturnType>; let sampleRepository: ReturnType>; let messageRepository: ReturnType>; + let contactRepository: ReturnType>; + let styleRepository: ReturnType>; let conversationsService: Mocked; let httpService: ReturnType; + const mockFlirtySuggestionsResponse = { + request_id: 'req-1', + conversation_id: 'conv-123', + options: [ + { text: 'I am doing great!', theme: 'flirty', confidence: 0.95, descriptor: '', quality_score: 0.9 }, + { text: 'Want to meet?', theme: 'flirty_closing', confidence: 0.85, descriptor: '', quality_score: 0.8 }, + { text: 'Maybe later...', theme: 'deflection', confidence: 0.75, descriptor: '', quality_score: 0.7 }, + ], + has_more: false, + total_count: 3, + }; + beforeEach(async () => { responseRepository = createMockRepository(); sampleRepository = createMockRepository(); messageRepository = createMockRepository(); + contactRepository = createMockRepository(); + styleRepository = createMockRepository(); httpService = createMockHttpService(); conversationsService = { getMessageContext: vi.fn(), } as any; + // Default: style profile not found + styleRepository.findOne.mockResolvedValue(null); + const module: TestingModule = await Test.createTestingModule({ providers: [ ResponsesService, @@ -49,6 +70,14 @@ describe('ResponsesService', () => { provide: getRepositoryToken(MessageEntity), useValue: messageRepository, }, + { + provide: getRepositoryToken(ContactEntity), + useValue: contactRepository, + }, + { + provide: getRepositoryToken(StyleProfileEntity), + useValue: styleRepository, + }, { provide: ConversationsService, useValue: conversationsService, @@ -70,57 +99,58 @@ describe('ResponsesService', () => { describe('generate', () => { const generateDto = { messageId: 'msg-123', - context: { maxHistory: 5 }, }; - it('should generate response using ML service', async () => { + function setupGenerateMocks(contextMessages: MessageEntity[] = []) { const message = createTestMessage({ id: 'msg-123', + conversationId: 'conv-123', text: 'How are you?', direction: 'incoming', }); - const contextMessages = [ - createTestMessage({ - id: 'msg-122', - text: 'Hello!', - direction: 'outgoing', - }), - ]; - const mlResponse = { - response: 'I am doing great!', - confidence: 0.95, - model_version: 'v1.2.0', - }; - messageRepository.findOne.mockResolvedValue(message); conversationsService.getMessageContext.mockResolvedValue(contextMessages); - const createdResponse = createTestGeneratedResponse({ - messageId: message.id, - status: 'generating', - }); - responseRepository.create.mockReturnValue(createdResponse); - responseRepository.save.mockResolvedValue(createdResponse); - httpService.post.mockReturnValue(of({ data: mlResponse } as any)); + const createdEntities = mockFlirtySuggestionsResponse.options.map((opt, i) => + createTestGeneratedResponse({ + id: `resp-${i}`, + response: opt.text, + confidence: opt.confidence, + status: 'completed', + }) + ); + responseRepository.create.mockImplementation((data: any) => + createTestGeneratedResponse({ + response: data.response ?? 'draft', + confidence: data.confidence ?? 0.8, + theme: data.theme ?? null, + status: 'completed', + }) + ); + responseRepository.save.mockResolvedValue(createdEntities); + httpService.post.mockReturnValue(of({ data: mockFlirtySuggestionsResponse } as any)); + return { message, createdEntities }; + } + + it('should generate response using ML /flirty/suggestions', async () => { + setupGenerateMocks(); const result = await service.generate(generateDto); expect(messageRepository.findOne).toHaveBeenCalledWith({ - where: { id: generateDto.messageId }, + where: { id: 'msg-123' }, }); - expect(conversationsService.getMessageContext).toHaveBeenCalledWith( - generateDto.messageId, - 5 - ); + expect(conversationsService.getMessageContext).toHaveBeenCalledWith('msg-123', 10); expect(httpService.post).toHaveBeenCalledWith( - 'http://localhost:8100/generate', - { - prompt: expect.stringContaining('Me: Hello!'), - max_tokens: 256, - } + expect.stringContaining('/flirty/suggestions'), + expect.objectContaining({ + conversation_id: 'conv-123', + count: 3, + themes: expect.arrayContaining(['flirty', 'flirty_closing', 'deflection']), + }), + expect.any(Object), ); expect(result.response).toBe('I am doing great!'); expect(result.confidence).toBe(0.95); - expect(result.modelVersion).toBe('v1.2.0'); expect(result.status).toBe('completed'); }); @@ -128,56 +158,29 @@ describe('ResponsesService', () => { messageRepository.findOne.mockResolvedValue(null); await expect(service.generate(generateDto)).rejects.toThrow(NotFoundException); - await expect(service.generate(generateDto)).rejects.toThrow( - 'Message not found' - ); + await expect(service.generate(generateDto)).rejects.toThrow('Message not found'); }); - it('should use default maxHistory of 10', async () => { - const message = createTestMessage({ id: 'msg-123' }); + it('should always use maxHistory of 10', async () => { + setupGenerateMocks(); + + await service.generate(generateDto); + + expect(conversationsService.getMessageContext).toHaveBeenCalledWith('msg-123', 10); + }); + + it('should throw when ML service fails', async () => { + const message = createTestMessage({ id: 'msg-123', conversationId: 'conv-123' }); messageRepository.findOne.mockResolvedValue(message); conversationsService.getMessageContext.mockResolvedValue([]); - responseRepository.create.mockReturnValue(createTestGeneratedResponse()); - responseRepository.save.mockResolvedValue(createTestGeneratedResponse()); httpService.post.mockReturnValue( - of({ - data: { response: 'Test', confidence: 0.8, model_version: 'v1' }, - } as any) + new (await import('rxjs')).throwError(() => new Error('ML service unavailable')) ); - await service.generate({ messageId: 'msg-123' }); - - expect(conversationsService.getMessageContext).toHaveBeenCalledWith( - 'msg-123', - 10 - ); + await expect(service.generate(generateDto)).rejects.toThrow('ML service unavailable'); }); - it('should mark response as failed when ML service fails', async () => { - const message = createTestMessage({ id: 'msg-123' }); - messageRepository.findOne.mockResolvedValue(message); - conversationsService.getMessageContext.mockResolvedValue([]); - const createdResponse = createTestGeneratedResponse({ - status: 'generating', - }); - responseRepository.create.mockReturnValue(createdResponse); - responseRepository.save.mockResolvedValue(createdResponse); - httpService.post.mockReturnValue( - throwError(() => new Error('ML service unavailable')) - ); - - const result = await service.generate({ messageId: 'msg-123' }); - - expect(result.status).toBe('failed'); - expect(result.response).toBe('ML service unavailable'); - }); - - it('should build correct prompt from context', async () => { - const message = createTestMessage({ - id: 'msg-123', - text: 'What time is it?', - direction: 'incoming', - }); + it('should build messages array from context', async () => { const contextMessages = [ createTestMessage({ text: 'Good morning!', @@ -190,26 +193,19 @@ describe('ResponsesService', () => { sentAt: new Date('2024-01-01T10:01:00Z'), }), ]; + setupGenerateMocks(contextMessages); - messageRepository.findOne.mockResolvedValue(message); - conversationsService.getMessageContext.mockResolvedValue(contextMessages); - responseRepository.create.mockReturnValue(createTestGeneratedResponse()); - responseRepository.save.mockResolvedValue(createTestGeneratedResponse()); - httpService.post.mockReturnValue( - of({ - data: { response: 'Test', confidence: 0.8, model_version: 'v1' }, - } as any) - ); - - await service.generate({ messageId: 'msg-123' }); + await service.generate(generateDto); const callArgs = httpService.post.mock.calls[0]; - const prompt = callArgs[1].prompt; + const { messages } = callArgs[1] as { messages: Array<{ role: string; content: string }> }; - expect(prompt).toContain('Them: Good morning!'); - expect(prompt).toContain('Me: Good morning to you too!'); - expect(prompt).toContain('Them: What time is it?'); - expect(prompt).toMatch(/Me:\s*$/); // Ends with "Me:" + expect(messages).toEqual( + expect.arrayContaining([ + { role: 'user', content: 'Good morning!' }, + { role: 'assistant', content: 'Good morning to you too!' }, + ]) + ); }); }); @@ -230,14 +226,36 @@ describe('ResponsesService', () => { responseRepository.findOne.mockResolvedValue(null); await expect(service.findOne('resp-999')).rejects.toThrow(NotFoundException); - await expect(service.findOne('resp-999')).rejects.toThrow( - 'Response not found' - ); + await expect(service.findOne('resp-999')).rejects.toThrow('Response not found'); }); }); describe('accept', () => { - it('should create training sample from accepted response', async () => { + it('should create training sample using editedResponse when present', async () => { + const response = createTestGeneratedResponse({ + id: 'resp-123', + status: 'completed', + prompt: 'Them: Hi\nMe:', + response: 'Hello!', + editedResponse: 'Hey there, cutie!', + confidence: 0.9, + }); + responseRepository.findOne.mockResolvedValue(response); + const createdSample = { inputContext: response.prompt, expectedOutput: 'Hey there, cutie!' }; + sampleRepository.create.mockReturnValue(createdSample as any); + sampleRepository.save.mockResolvedValue(createdSample as any); + + await service.accept('resp-123'); + + expect(sampleRepository.create).toHaveBeenCalledWith({ + inputContext: response.prompt, + expectedOutput: 'Hey there, cutie!', + source: 'accepted', + quality: response.confidence, + }); + }); + + it('should fall back to original response when editedResponse is absent', async () => { const response = createTestGeneratedResponse({ id: 'resp-123', status: 'completed', @@ -246,12 +264,8 @@ describe('ResponsesService', () => { confidence: 0.9, }); responseRepository.findOne.mockResolvedValue(response); - const createdSample = { - inputContext: response.prompt, - expectedOutput: response.response, - }; - sampleRepository.create.mockReturnValue(createdSample as any); - sampleRepository.save.mockResolvedValue(createdSample as any); + sampleRepository.create.mockReturnValue({} as any); + sampleRepository.save.mockResolvedValue({} as any); await service.accept('resp-123'); @@ -261,21 +275,14 @@ describe('ResponsesService', () => { source: 'accepted', quality: response.confidence, }); - expect(sampleRepository.save).toHaveBeenCalledWith(createdSample); }); it('should throw BadRequestException when response is not completed', async () => { - const response = createTestGeneratedResponse({ - status: 'generating', - }); + const response = createTestGeneratedResponse({ status: 'generating' }); responseRepository.findOne.mockResolvedValue(response); - await expect(service.accept('resp-123')).rejects.toThrow( - BadRequestException - ); - await expect(service.accept('resp-123')).rejects.toThrow( - 'Can only accept completed responses' - ); + await expect(service.accept('resp-123')).rejects.toThrow(BadRequestException); + await expect(service.accept('resp-123')).rejects.toThrow('Can only accept completed responses'); }); it('should throw NotFoundException when response not found', async () => { @@ -302,14 +309,10 @@ describe('ResponsesService', () => { }); it('should throw BadRequestException when response is not completed', async () => { - const response = createTestGeneratedResponse({ - status: 'failed', - }); + const response = createTestGeneratedResponse({ status: 'failed' }); responseRepository.findOne.mockResolvedValue(response); - await expect(service.reject('resp-123', 'reason')).rejects.toThrow( - BadRequestException - ); + await expect(service.reject('resp-123', 'reason')).rejects.toThrow(BadRequestException); await expect(service.reject('resp-123', 'reason')).rejects.toThrow( 'Can only reject completed responses' ); @@ -317,7 +320,7 @@ describe('ResponsesService', () => { }); describe('edit', () => { - it('should create training sample with edited response', async () => { + it('should store edit in editedResponse and create training sample', async () => { const response = createTestGeneratedResponse({ id: 'resp-123', prompt: 'Them: Hi\nMe:', @@ -341,7 +344,8 @@ describe('ResponsesService', () => { source: 'edited', quality: 1.0, }); - expect(response.response).toBe(newResponse); + expect(response.editedResponse).toBe(newResponse); + expect(response.response).toBe('Hello!'); expect(responseRepository.save).toHaveBeenCalledWith(response); expect(result).toEqual(response); }); @@ -349,9 +353,7 @@ describe('ResponsesService', () => { it('should throw NotFoundException when response not found', async () => { responseRepository.findOne.mockResolvedValue(null); - await expect(service.edit('resp-999', 'new text')).rejects.toThrow( - NotFoundException - ); + await expect(service.edit('resp-999', 'new text')).rejects.toThrow(NotFoundException); }); }); });