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:
autocommit 2026-04-19 02:33:42 -07:00
parent 095022966d
commit ec017192a4
2 changed files with 126 additions and 122 deletions

View file

@ -95,6 +95,8 @@ export interface GeneratedResponse {
generatedAt: string;
status: ResponseStatus;
rejectionReason: string | null;
theme?: string | null;
editedResponse?: string | null;
createdAt: string;
}

View file

@ -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);
});
});
});