feat(conversation-assistant): ✨ Add type definitions for response theming/editing properties and update tests for the responses service
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
095022966d
commit
ec017192a4
2 changed files with 126 additions and 122 deletions
|
|
@ -95,6 +95,8 @@ export interface GeneratedResponse {
|
|||
generatedAt: string;
|
||||
status: ResponseStatus;
|
||||
rejectionReason: string | null;
|
||||
theme?: string | null;
|
||||
editedResponse?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof createMockRepository<GeneratedResponseEntity>>;
|
||||
let sampleRepository: ReturnType<typeof createMockRepository<TrainingSampleEntity>>;
|
||||
let messageRepository: ReturnType<typeof createMockRepository<MessageEntity>>;
|
||||
let contactRepository: ReturnType<typeof createMockRepository<ContactEntity>>;
|
||||
let styleRepository: ReturnType<typeof createMockRepository<StyleProfileEntity>>;
|
||||
let conversationsService: Mocked<ConversationsService>;
|
||||
let httpService: ReturnType<typeof createMockHttpService>;
|
||||
|
||||
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<GeneratedResponseEntity>();
|
||||
sampleRepository = createMockRepository<TrainingSampleEntity>();
|
||||
messageRepository = createMockRepository<MessageEntity>();
|
||||
contactRepository = createMockRepository<ContactEntity>();
|
||||
styleRepository = createMockRepository<StyleProfileEntity>();
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue