- Build complete Next.js CRM for commercial real estate - Add authentication with JWT sessions and role-based access - Add GoHighLevel API integration for contacts, conversations, opportunities - Add AI-powered Control Center with tool calling - Add Setup page with onboarding checklist (/setup) - Add sidebar navigation with Setup menu item - Fix type errors in onboarding API, GHL services, and control center tools - Add Prisma schema with SQLite for local development - Add UI components with clay morphism design system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
297 lines
9.1 KiB
TypeScript
297 lines
9.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { ConversationService } from '@/lib/control-center/conversation-service';
|
|
import { prisma } from '@/lib/db';
|
|
|
|
// Get the mocked prisma instance
|
|
const mockPrisma = vi.mocked(prisma);
|
|
|
|
describe('ConversationService', () => {
|
|
let conversationService: ConversationService;
|
|
|
|
beforeEach(() => {
|
|
conversationService = new ConversationService();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('should create a conversation with userId and title', async () => {
|
|
const mockConversation = {
|
|
id: 'conv-123',
|
|
userId: 'user-456',
|
|
title: 'Test Conversation',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
};
|
|
|
|
mockPrisma.controlCenterConversation.create.mockResolvedValue(mockConversation);
|
|
|
|
const result = await conversationService.create('user-456', 'Test Conversation');
|
|
|
|
expect(mockPrisma.controlCenterConversation.create).toHaveBeenCalledWith({
|
|
data: { userId: 'user-456', title: 'Test Conversation' }
|
|
});
|
|
expect(result).toEqual(mockConversation);
|
|
});
|
|
|
|
it('should create a conversation without title', async () => {
|
|
const mockConversation = {
|
|
id: 'conv-123',
|
|
userId: 'user-456',
|
|
title: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
};
|
|
|
|
mockPrisma.controlCenterConversation.create.mockResolvedValue(mockConversation);
|
|
|
|
const result = await conversationService.create('user-456');
|
|
|
|
expect(mockPrisma.controlCenterConversation.create).toHaveBeenCalledWith({
|
|
data: { userId: 'user-456', title: undefined }
|
|
});
|
|
expect(result).toEqual(mockConversation);
|
|
});
|
|
});
|
|
|
|
describe('getById', () => {
|
|
it('should return conversation with messages ordered by createdAt', async () => {
|
|
const mockConversation = {
|
|
id: 'conv-123',
|
|
userId: 'user-456',
|
|
title: 'Test Conversation',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
messages: [
|
|
{
|
|
id: 'msg-1',
|
|
conversationId: 'conv-123',
|
|
role: 'user',
|
|
content: 'Hello',
|
|
createdAt: new Date('2024-01-01')
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
conversationId: 'conv-123',
|
|
role: 'assistant',
|
|
content: 'Hi there!',
|
|
createdAt: new Date('2024-01-02')
|
|
}
|
|
]
|
|
};
|
|
|
|
mockPrisma.controlCenterConversation.findUnique.mockResolvedValue(mockConversation);
|
|
|
|
const result = await conversationService.getById('conv-123');
|
|
|
|
expect(mockPrisma.controlCenterConversation.findUnique).toHaveBeenCalledWith({
|
|
where: { id: 'conv-123' },
|
|
include: { messages: { orderBy: { createdAt: 'asc' } } }
|
|
});
|
|
expect(result).toEqual(mockConversation);
|
|
expect(result?.messages).toHaveLength(2);
|
|
});
|
|
|
|
it('should return null for non-existent conversation', async () => {
|
|
mockPrisma.controlCenterConversation.findUnique.mockResolvedValue(null);
|
|
|
|
const result = await conversationService.getById('non-existent');
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('listByUser', () => {
|
|
it('should return paginated conversations for a user', async () => {
|
|
const mockConversations = [
|
|
{
|
|
id: 'conv-1',
|
|
userId: 'user-456',
|
|
title: 'Conversation 1',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
_count: { messages: 5 }
|
|
},
|
|
{
|
|
id: 'conv-2',
|
|
userId: 'user-456',
|
|
title: 'Conversation 2',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
_count: { messages: 3 }
|
|
}
|
|
];
|
|
|
|
mockPrisma.controlCenterConversation.findMany.mockResolvedValue(mockConversations);
|
|
|
|
const result = await conversationService.listByUser('user-456', 20, 0);
|
|
|
|
expect(mockPrisma.controlCenterConversation.findMany).toHaveBeenCalledWith({
|
|
where: { userId: 'user-456' },
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: 20,
|
|
skip: 0,
|
|
include: { _count: { select: { messages: true } } }
|
|
});
|
|
expect(result).toEqual(mockConversations);
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
it('should use default pagination values', async () => {
|
|
mockPrisma.controlCenterConversation.findMany.mockResolvedValue([]);
|
|
|
|
await conversationService.listByUser('user-456');
|
|
|
|
expect(mockPrisma.controlCenterConversation.findMany).toHaveBeenCalledWith({
|
|
where: { userId: 'user-456' },
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: 20,
|
|
skip: 0,
|
|
include: { _count: { select: { messages: true } } }
|
|
});
|
|
});
|
|
|
|
it('should apply custom pagination parameters', async () => {
|
|
mockPrisma.controlCenterConversation.findMany.mockResolvedValue([]);
|
|
|
|
await conversationService.listByUser('user-456', 10, 5);
|
|
|
|
expect(mockPrisma.controlCenterConversation.findMany).toHaveBeenCalledWith({
|
|
where: { userId: 'user-456' },
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: 10,
|
|
skip: 5,
|
|
include: { _count: { select: { messages: true } } }
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('addMessage', () => {
|
|
it('should add a message and update conversation timestamp', async () => {
|
|
const mockMessage = {
|
|
id: 'msg-123',
|
|
conversationId: 'conv-456',
|
|
role: 'user',
|
|
content: 'Hello!',
|
|
toolCalls: null,
|
|
toolResults: null,
|
|
model: null,
|
|
inputTokens: null,
|
|
outputTokens: null,
|
|
createdAt: new Date()
|
|
};
|
|
|
|
mockPrisma.controlCenterConversation.update.mockResolvedValue({} as any);
|
|
mockPrisma.controlCenterMessage.create.mockResolvedValue(mockMessage);
|
|
|
|
const messageData = {
|
|
role: 'user' as const,
|
|
content: 'Hello!'
|
|
};
|
|
|
|
const result = await conversationService.addMessage('conv-456', messageData);
|
|
|
|
expect(mockPrisma.controlCenterConversation.update).toHaveBeenCalledWith({
|
|
where: { id: 'conv-456' },
|
|
data: { updatedAt: expect.any(Date) }
|
|
});
|
|
expect(mockPrisma.controlCenterMessage.create).toHaveBeenCalledWith({
|
|
data: { conversationId: 'conv-456', ...messageData }
|
|
});
|
|
expect(result).toEqual(mockMessage);
|
|
});
|
|
|
|
it('should add assistant message with tool calls and usage data', async () => {
|
|
const toolCalls = [{ id: 'tc-1', name: 'get_contacts', input: {} }];
|
|
const toolResults = [{ toolCallId: 'tc-1', success: true, result: [] }];
|
|
|
|
const mockMessage = {
|
|
id: 'msg-123',
|
|
conversationId: 'conv-456',
|
|
role: 'assistant',
|
|
content: 'Here are your contacts',
|
|
toolCalls,
|
|
toolResults,
|
|
model: 'claude-sonnet-4-20250514',
|
|
inputTokens: 100,
|
|
outputTokens: 50,
|
|
createdAt: new Date()
|
|
};
|
|
|
|
mockPrisma.controlCenterConversation.update.mockResolvedValue({} as any);
|
|
mockPrisma.controlCenterMessage.create.mockResolvedValue(mockMessage);
|
|
|
|
const messageData = {
|
|
role: 'assistant' as const,
|
|
content: 'Here are your contacts',
|
|
toolCalls,
|
|
toolResults,
|
|
model: 'claude-sonnet-4-20250514',
|
|
inputTokens: 100,
|
|
outputTokens: 50
|
|
};
|
|
|
|
const result = await conversationService.addMessage('conv-456', messageData);
|
|
|
|
expect(mockPrisma.controlCenterMessage.create).toHaveBeenCalledWith({
|
|
data: { conversationId: 'conv-456', ...messageData }
|
|
});
|
|
expect(result.model).toBe('claude-sonnet-4-20250514');
|
|
expect(result.inputTokens).toBe(100);
|
|
expect(result.outputTokens).toBe(50);
|
|
});
|
|
});
|
|
|
|
describe('updateTitle', () => {
|
|
it('should update conversation title', async () => {
|
|
const mockConversation = {
|
|
id: 'conv-123',
|
|
userId: 'user-456',
|
|
title: 'New Title',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
};
|
|
|
|
mockPrisma.controlCenterConversation.update.mockResolvedValue(mockConversation);
|
|
|
|
const result = await conversationService.updateTitle('conv-123', 'New Title');
|
|
|
|
expect(mockPrisma.controlCenterConversation.update).toHaveBeenCalledWith({
|
|
where: { id: 'conv-123' },
|
|
data: { title: 'New Title' }
|
|
});
|
|
expect(result.title).toBe('New Title');
|
|
});
|
|
});
|
|
|
|
describe('delete', () => {
|
|
it('should delete a conversation', async () => {
|
|
const mockConversation = {
|
|
id: 'conv-123',
|
|
userId: 'user-456',
|
|
title: 'Deleted',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
};
|
|
|
|
mockPrisma.controlCenterConversation.delete.mockResolvedValue(mockConversation);
|
|
|
|
const result = await conversationService.delete('conv-123');
|
|
|
|
expect(mockPrisma.controlCenterConversation.delete).toHaveBeenCalledWith({
|
|
where: { id: 'conv-123' }
|
|
});
|
|
expect(result).toEqual(mockConversation);
|
|
});
|
|
|
|
it('should throw error for non-existent conversation', async () => {
|
|
mockPrisma.controlCenterConversation.delete.mockRejectedValue(
|
|
new Error('Record not found')
|
|
);
|
|
|
|
await expect(conversationService.delete('non-existent')).rejects.toThrow(
|
|
'Record not found'
|
|
);
|
|
});
|
|
});
|
|
});
|