import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { PersonalitiesService } from "./personalities.service"; import { PrismaService } from "../prisma/prisma.service"; import type { CreatePersonalityDto } from "./dto/create-personality.dto"; import type { UpdatePersonalityDto } from "./dto/update-personality.dto"; import { NotFoundException, ConflictException } from "@nestjs/common"; import { FormalityLevel } from "@prisma/client"; describe("PersonalitiesService", () => { let service: PersonalitiesService; let prisma: PrismaService; const mockWorkspaceId = "workspace-123"; const mockPersonalityId = "personality-123"; /** Raw Prisma record shape (uses Prisma field names) */ const mockPrismaRecord = { id: mockPersonalityId, workspaceId: mockWorkspaceId, name: "professional-assistant", displayName: "Professional Assistant", description: "A professional communication assistant", tone: "professional", formalityLevel: FormalityLevel.FORMAL, systemPrompt: "You are a professional assistant who helps with tasks.", temperature: 0.7, maxTokens: 2000, llmProviderInstanceId: "provider-123", isDefault: true, isEnabled: true, createdAt: new Date("2026-01-01"), updatedAt: new Date("2026-01-01"), }; /** Expected API response shape (uses frontend field names) */ const mockResponse = { id: mockPersonalityId, workspaceId: mockWorkspaceId, name: "professional-assistant", description: "A professional communication assistant", tone: "professional", formalityLevel: FormalityLevel.FORMAL, systemPromptTemplate: "You are a professional assistant who helps with tasks.", isDefault: true, isActive: true, createdAt: new Date("2026-01-01"), updatedAt: new Date("2026-01-01"), }; const mockPrismaService = { personality: { findMany: vi.fn(), findUnique: vi.fn(), findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn(), }, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ PersonalitiesService, { provide: PrismaService, useValue: mockPrismaService, }, ], }).compile(); service = module.get(PersonalitiesService); prisma = module.get(PrismaService); vi.clearAllMocks(); }); describe("create", () => { const createDto: CreatePersonalityDto = { name: "casual-helper", description: "A casual communication helper", tone: "casual", formalityLevel: FormalityLevel.CASUAL, systemPromptTemplate: "You are a casual assistant.", isDefault: false, isActive: true, }; const createdRecord = { ...mockPrismaRecord, name: createDto.name, description: createDto.description, tone: createDto.tone, formalityLevel: createDto.formalityLevel, systemPrompt: createDto.systemPromptTemplate, isDefault: false, isEnabled: true, id: "new-personality-id", }; it("should create a new personality and return API response shape", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(null); mockPrismaService.personality.create.mockResolvedValue(createdRecord); const result = await service.create(mockWorkspaceId, createDto); expect(result.name).toBe(createDto.name); expect(result.tone).toBe(createDto.tone); expect(result.formalityLevel).toBe(createDto.formalityLevel); expect(result.systemPromptTemplate).toBe(createDto.systemPromptTemplate); expect(result.isActive).toBe(true); expect(result.isDefault).toBe(false); expect(prisma.personality.create).toHaveBeenCalledWith({ data: { workspaceId: mockWorkspaceId, name: createDto.name, displayName: createDto.name, description: createDto.description ?? null, tone: createDto.tone, formalityLevel: createDto.formalityLevel, systemPrompt: createDto.systemPromptTemplate, isDefault: false, isEnabled: true, }, }); }); it("should throw ConflictException when name already exists", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(mockPrismaRecord); await expect(service.create(mockWorkspaceId, createDto)).rejects.toThrow(ConflictException); }); it("should unset other defaults when creating a new default personality", async () => { const createDefaultDto: CreatePersonalityDto = { ...createDto, isDefault: true }; const otherDefault = { ...mockPrismaRecord, id: "other-id" }; mockPrismaService.personality.findFirst .mockResolvedValueOnce(null) // name conflict check .mockResolvedValueOnce(otherDefault); // existing default lookup mockPrismaService.personality.update.mockResolvedValue({ ...otherDefault, isDefault: false }); mockPrismaService.personality.create.mockResolvedValue({ ...createdRecord, isDefault: true, }); await service.create(mockWorkspaceId, createDefaultDto); expect(prisma.personality.update).toHaveBeenCalledWith({ where: { id: "other-id" }, data: { isDefault: false }, }); }); }); describe("findAll", () => { it("should return mapped response list for a workspace", async () => { mockPrismaService.personality.findMany.mockResolvedValue([mockPrismaRecord]); const result = await service.findAll(mockWorkspaceId); expect(result).toHaveLength(1); expect(result[0]).toEqual(mockResponse); expect(prisma.personality.findMany).toHaveBeenCalledWith({ where: { workspaceId: mockWorkspaceId }, orderBy: [{ isDefault: "desc" }, { name: "asc" }], }); }); it("should filter by isActive when provided", async () => { mockPrismaService.personality.findMany.mockResolvedValue([mockPrismaRecord]); await service.findAll(mockWorkspaceId, { isActive: true }); expect(prisma.personality.findMany).toHaveBeenCalledWith({ where: { workspaceId: mockWorkspaceId, isEnabled: true }, orderBy: [{ isDefault: "desc" }, { name: "asc" }], }); }); }); describe("findOne", () => { it("should return a mapped personality response by id", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(mockPrismaRecord); const result = await service.findOne(mockWorkspaceId, mockPersonalityId); expect(result).toEqual(mockResponse); expect(prisma.personality.findFirst).toHaveBeenCalledWith({ where: { id: mockPersonalityId, workspaceId: mockWorkspaceId }, }); }); it("should throw NotFoundException when personality not found", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(null); await expect(service.findOne(mockWorkspaceId, mockPersonalityId)).rejects.toThrow( NotFoundException ); }); }); describe("findByName", () => { it("should return a mapped personality response by name", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(mockPrismaRecord); const result = await service.findByName(mockWorkspaceId, "professional-assistant"); expect(result).toEqual(mockResponse); expect(prisma.personality.findFirst).toHaveBeenCalledWith({ where: { workspaceId: mockWorkspaceId, name: "professional-assistant" }, }); }); it("should throw NotFoundException when personality not found", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(null); await expect(service.findByName(mockWorkspaceId, "non-existent")).rejects.toThrow( NotFoundException ); }); }); describe("findDefault", () => { it("should return the default personality", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(mockPrismaRecord); const result = await service.findDefault(mockWorkspaceId); expect(result).toEqual(mockResponse); expect(prisma.personality.findFirst).toHaveBeenCalledWith({ where: { workspaceId: mockWorkspaceId, isDefault: true, isEnabled: true }, }); }); it("should throw NotFoundException when no default personality exists", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(null); await expect(service.findDefault(mockWorkspaceId)).rejects.toThrow(NotFoundException); }); }); describe("update", () => { const updateDto: UpdatePersonalityDto = { description: "Updated description", tone: "formal", isActive: false, }; it("should update a personality and return mapped response", async () => { const updatedRecord = { ...mockPrismaRecord, description: updateDto.description, tone: updateDto.tone, isEnabled: false, }; mockPrismaService.personality.findFirst .mockResolvedValueOnce(mockPrismaRecord) // findOne check .mockResolvedValueOnce(null); // name conflict check (no dto.name here) mockPrismaService.personality.update.mockResolvedValue(updatedRecord); const result = await service.update(mockWorkspaceId, mockPersonalityId, updateDto); expect(result.description).toBe(updateDto.description); expect(result.tone).toBe(updateDto.tone); expect(result.isActive).toBe(false); }); it("should throw NotFoundException when personality not found", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(null); await expect(service.update(mockWorkspaceId, mockPersonalityId, updateDto)).rejects.toThrow( NotFoundException ); }); it("should throw ConflictException when updating to an existing name", async () => { const updateNameDto: UpdatePersonalityDto = { name: "existing-name" }; const conflictRecord = { ...mockPrismaRecord, id: "different-id" }; mockPrismaService.personality.findFirst .mockResolvedValueOnce(mockPrismaRecord) // findOne check .mockResolvedValueOnce(conflictRecord); // name conflict await expect( service.update(mockWorkspaceId, mockPersonalityId, updateNameDto) ).rejects.toThrow(ConflictException); }); it("should unset other defaults when setting as default", async () => { const updateDefaultDto: UpdatePersonalityDto = { isDefault: true }; const otherPersonality = { ...mockPrismaRecord, id: "other-id", isDefault: true }; const updatedRecord = { ...mockPrismaRecord, isDefault: true }; mockPrismaService.personality.findFirst .mockResolvedValueOnce(mockPrismaRecord) // findOne check .mockResolvedValueOnce(otherPersonality); // unsetOtherDefaults lookup mockPrismaService.personality.update .mockResolvedValueOnce({ ...otherPersonality, isDefault: false }) .mockResolvedValueOnce(updatedRecord); await service.update(mockWorkspaceId, mockPersonalityId, updateDefaultDto); expect(prisma.personality.update).toHaveBeenNthCalledWith(1, { where: { id: "other-id" }, data: { isDefault: false }, }); }); }); describe("delete", () => { it("should delete a personality", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(mockPrismaRecord); mockPrismaService.personality.delete.mockResolvedValue(undefined); await service.delete(mockWorkspaceId, mockPersonalityId); expect(prisma.personality.delete).toHaveBeenCalledWith({ where: { id: mockPersonalityId }, }); }); it("should throw NotFoundException when personality not found", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(null); await expect(service.delete(mockWorkspaceId, mockPersonalityId)).rejects.toThrow( NotFoundException ); }); }); describe("setDefault", () => { it("should set a personality as default", async () => { const otherPersonality = { ...mockPrismaRecord, id: "other-id", isDefault: true }; const updatedRecord = { ...mockPrismaRecord, isDefault: true }; mockPrismaService.personality.findFirst .mockResolvedValueOnce(mockPrismaRecord) // findOne check .mockResolvedValueOnce(otherPersonality); // unsetOtherDefaults lookup mockPrismaService.personality.update .mockResolvedValueOnce({ ...otherPersonality, isDefault: false }) .mockResolvedValueOnce(updatedRecord); const result = await service.setDefault(mockWorkspaceId, mockPersonalityId); expect(result.isDefault).toBe(true); expect(prisma.personality.update).toHaveBeenCalledWith({ where: { id: mockPersonalityId }, data: { isDefault: true }, }); }); it("should throw NotFoundException when personality not found", async () => { mockPrismaService.personality.findFirst.mockResolvedValue(null); await expect(service.setDefault(mockWorkspaceId, mockPersonalityId)).rejects.toThrow( NotFoundException ); }); }); });