feat(#82): implement Personality Module
- Add Personality model to Prisma schema with FormalityLevel enum - Create migration and seed with 6 default personalities - Implement CRUD API with TDD approach (97.67% coverage) * PersonalitiesService: findAll, findOne, findDefault, create, update, remove * PersonalitiesController: REST endpoints with auth guards * Comprehensive test coverage (21 passing tests) - Add Personality types to shared package - Create frontend components: * PersonalitySelector: dropdown for choosing personality * PersonalityPreview: preview personality style and system prompt * PersonalityForm: create/edit personalities with validation * Settings page: manage personalities with CRUD operations - Integrate with Ollama API: * Support personalityId in chat endpoint * Auto-inject system prompt from personality * Fall back to default personality if not specified - API client for frontend personality management All tests passing with 97.67% backend coverage (exceeds 85% requirement)
This commit is contained in:
255
apps/api/src/personalities/personalities.service.spec.ts
Normal file
255
apps/api/src/personalities/personalities.service.spec.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
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 { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||
import { NotFoundException, ConflictException } from "@nestjs/common";
|
||||
|
||||
describe("PersonalitiesService", () => {
|
||||
let service: PersonalitiesService;
|
||||
let prisma: PrismaService;
|
||||
|
||||
const mockWorkspaceId = "workspace-123";
|
||||
const mockUserId = "user-123";
|
||||
const mockPersonalityId = "personality-123";
|
||||
|
||||
const mockPersonality = {
|
||||
id: mockPersonalityId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
name: "Professional",
|
||||
description: "Professional communication style",
|
||||
tone: "professional",
|
||||
formalityLevel: "FORMAL" as const,
|
||||
systemPromptTemplate: "You are a professional assistant.",
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockPrismaService = {
|
||||
personality: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn((callback) => callback(mockPrismaService)),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PersonalitiesService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PersonalitiesService>(PersonalitiesService);
|
||||
prisma = module.get<PrismaService>(PrismaService);
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should return all personalities for a workspace", async () => {
|
||||
const mockPersonalities = [mockPersonality];
|
||||
mockPrismaService.personality.findMany.mockResolvedValue(mockPersonalities);
|
||||
|
||||
const result = await service.findAll(mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockPersonalities);
|
||||
expect(prisma.personality.findMany).toHaveBeenCalledWith({
|
||||
where: { workspaceId: mockWorkspaceId, isActive: true },
|
||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by active status", async () => {
|
||||
mockPrismaService.personality.findMany.mockResolvedValue([mockPersonality]);
|
||||
|
||||
await service.findAll(mockWorkspaceId, false);
|
||||
|
||||
expect(prisma.personality.findMany).toHaveBeenCalledWith({
|
||||
where: { workspaceId: mockWorkspaceId, isActive: false },
|
||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("findOne", () => {
|
||||
it("should return a personality by id", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await service.findOne(mockWorkspaceId, mockPersonalityId);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(prisma.personality.findUnique).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: mockPersonalityId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when personality not found", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findDefault", () => {
|
||||
it("should return the default personality", async () => {
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await service.findDefault(mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(prisma.personality.findFirst).toHaveBeenCalledWith({
|
||||
where: { workspaceId: mockWorkspaceId, isDefault: true, isActive: 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("create", () => {
|
||||
const createDto: CreatePersonalityDto = {
|
||||
name: "Casual",
|
||||
description: "Casual communication style",
|
||||
tone: "casual",
|
||||
formalityLevel: "CASUAL",
|
||||
systemPromptTemplate: "You are a casual assistant.",
|
||||
};
|
||||
|
||||
it("should create a new personality", async () => {
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.personality.create.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
...createDto,
|
||||
id: "new-personality-id",
|
||||
});
|
||||
|
||||
const result = await service.create(mockWorkspaceId, createDto);
|
||||
|
||||
expect(result).toMatchObject(createDto);
|
||||
expect(prisma.personality.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
workspaceId: mockWorkspaceId,
|
||||
...createDto,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw ConflictException when name already exists", async () => {
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);
|
||||
|
||||
await expect(service.create(mockWorkspaceId, createDto)).rejects.toThrow(ConflictException);
|
||||
});
|
||||
|
||||
it("should unset other defaults when creating a new default personality", async () => {
|
||||
const createDefaultDto = { ...createDto, isDefault: true };
|
||||
// First call to findFirst checks for name conflict (should be null)
|
||||
// Second call to findFirst finds the existing default personality
|
||||
mockPrismaService.personality.findFirst
|
||||
.mockResolvedValueOnce(null) // No name conflict
|
||||
.mockResolvedValueOnce(mockPersonality); // Existing default
|
||||
mockPrismaService.personality.update.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
isDefault: false,
|
||||
});
|
||||
mockPrismaService.personality.create.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
...createDefaultDto,
|
||||
});
|
||||
|
||||
await service.create(mockWorkspaceId, createDefaultDto);
|
||||
|
||||
expect(prisma.personality.update).toHaveBeenCalledWith({
|
||||
where: { id: mockPersonalityId },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
const updateDto: UpdatePersonalityDto = {
|
||||
description: "Updated description",
|
||||
tone: "updated",
|
||||
};
|
||||
|
||||
it("should update a personality", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.personality.update.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
...updateDto,
|
||||
});
|
||||
|
||||
const result = await service.update(mockWorkspaceId, mockPersonalityId, updateDto);
|
||||
|
||||
expect(result).toMatchObject(updateDto);
|
||||
expect(prisma.personality.update).toHaveBeenCalledWith({
|
||||
where: { id: mockPersonalityId },
|
||||
data: updateDto,
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when personality not found", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.update(mockWorkspaceId, mockPersonalityId, updateDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it("should throw ConflictException when updating to existing name", async () => {
|
||||
const updateNameDto = { name: "Existing Name" };
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
id: "different-id",
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.update(mockWorkspaceId, mockPersonalityId, updateNameDto),
|
||||
).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("should delete a personality", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||
mockPrismaService.personality.delete.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await service.remove(mockWorkspaceId, mockPersonalityId);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(prisma.personality.delete).toHaveBeenCalledWith({
|
||||
where: { id: mockPersonalityId },
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when personality not found", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(service.remove(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user