feat(#130): add Personality Prisma schema and backend

Implement Personality system backend with database schema, service,
controller, and comprehensive tests. Personalities define assistant
behavior with system prompts and LLM configuration.

Changes:
- Update Personality model in schema.prisma with LLM provider relation
- Create PersonalitiesService with CRUD and default management
- Create PersonalitiesController with REST endpoints
- Add DTOs with validation (create/update)
- Add entity for type safety
- Remove unused PromptFormatterService
- Achieve 26 tests with full coverage

Endpoints:
- GET /personality - List all
- GET /personality/default - Get default
- GET /personality/by-name/:name - Get by name
- GET /personality/:id - Get one
- POST /personality - Create
- PATCH /personality/:id - Update
- DELETE /personality/:id - Delete
- POST /personality/:id/set-default - Set default

Fixes #130

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 12:44:50 -06:00
parent 1f97e6de40
commit 64cb5c1edd
12 changed files with 516 additions and 549 deletions

View File

@@ -2,51 +2,57 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { PersonalitiesController } from "./personalities.controller";
import { PersonalitiesService } from "./personalities.service";
import { PromptFormatterService } from "./services/prompt-formatter.service";
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
import { AuthGuard } from "../auth/guards/auth.guard";
describe("PersonalitiesController", () => {
let controller: PersonalitiesController;
let service: PersonalitiesService;
let promptFormatter: PromptFormatterService;
const mockWorkspaceId = "workspace-123";
const mockUserId = "user-123";
const mockPersonalityId = "personality-123";
const mockRequest = { user: { id: "user-123" }, workspaceId: mockWorkspaceId };
const mockPersonality = {
id: mockPersonalityId,
workspaceId: mockWorkspaceId,
name: "Professional",
tone: "professional",
formalityLevel: "FORMAL",
systemPromptTemplate: "You are a professional assistant.",
name: "professional-assistant",
displayName: "Professional Assistant",
description: "A professional communication assistant",
systemPrompt: "You are a professional assistant who helps with tasks.",
temperature: 0.7,
maxTokens: 2000,
llmProviderInstanceId: "provider-123",
isDefault: true,
isActive: true,
isEnabled: true,
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPersonalitiesService = {
findAll: vi.fn(),
findOne: vi.fn(),
findDefault: vi.fn(),
create: vi.fn(),
update: vi.fn(),
remove: vi.fn(),
const mockRequest = {
user: { id: mockUserId },
workspaceId: mockWorkspaceId,
};
const mockPromptFormatterService = {
formatPrompt: vi.fn(),
getFormalityLevels: vi.fn(),
const mockPersonalitiesService = {
create: vi.fn(),
findAll: vi.fn(),
findOne: vi.fn(),
findByName: vi.fn(),
findDefault: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
setDefault: vi.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PersonalitiesController],
providers: [
{ provide: PersonalitiesService, useValue: mockPersonalitiesService },
{ provide: PromptFormatterService, useValue: mockPromptFormatterService },
{
provide: PersonalitiesService,
useValue: mockPersonalitiesService,
},
],
})
.overrideGuard(AuthGuard)
@@ -55,85 +61,119 @@ describe("PersonalitiesController", () => {
controller = module.get<PersonalitiesController>(PersonalitiesController);
service = module.get<PersonalitiesService>(PersonalitiesService);
promptFormatter = module.get<PromptFormatterService>(PromptFormatterService);
// Reset mocks
vi.clearAllMocks();
});
describe("findAll", () => {
it("should return all personalities", async () => {
mockPersonalitiesService.findAll.mockResolvedValue([mockPersonality]);
const mockPersonalities = [mockPersonality];
mockPersonalitiesService.findAll.mockResolvedValue(mockPersonalities);
const result = await controller.findAll(mockRequest);
expect(result).toEqual([mockPersonality]);
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, undefined);
expect(result).toEqual(mockPersonalities);
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId);
});
});
describe("findOne", () => {
it("should return a personality by id", async () => {
mockPersonalitiesService.findOne.mockResolvedValue(mockPersonality);
const result = await controller.findOne(mockRequest, mockPersonalityId);
expect(result).toEqual(mockPersonality);
expect(service.findOne).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
});
});
describe("findByName", () => {
it("should return a personality by name", async () => {
mockPersonalitiesService.findByName.mockResolvedValue(mockPersonality);
const result = await controller.findByName(mockRequest, "professional-assistant");
expect(result).toEqual(mockPersonality);
expect(service.findByName).toHaveBeenCalledWith(mockWorkspaceId, "professional-assistant");
});
});
describe("findDefault", () => {
it("should return the default personality", async () => {
mockPersonalitiesService.findDefault.mockResolvedValue(mockPersonality);
const result = await controller.findDefault(mockRequest);
expect(result).toEqual(mockPersonality);
});
});
describe("getFormalityLevels", () => {
it("should return formality levels", () => {
const levels = [{ level: "FORMAL", description: "Professional" }];
mockPromptFormatterService.getFormalityLevels.mockReturnValue(levels);
const result = controller.getFormalityLevels();
expect(result).toEqual(levels);
const result = await controller.findDefault(mockRequest);
expect(result).toEqual(mockPersonality);
expect(service.findDefault).toHaveBeenCalledWith(mockWorkspaceId);
});
});
describe("create", () => {
it("should create a new personality", async () => {
const createDto = {
name: "Casual",
tone: "casual",
formalityLevel: "CASUAL" as const,
systemPromptTemplate: "You are a casual assistant.",
const createDto: CreatePersonalityDto = {
name: "casual-helper",
displayName: "Casual Helper",
description: "A casual helper",
systemPrompt: "You are a casual assistant.",
temperature: 0.8,
maxTokens: 1500,
};
mockPersonalitiesService.create.mockResolvedValue({ ...mockPersonality, ...createDto });
await controller.create(mockRequest, createDto);
mockPersonalitiesService.create.mockResolvedValue({
...mockPersonality,
...createDto,
});
const result = await controller.create(mockRequest, createDto);
expect(result).toMatchObject(createDto);
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
});
});
describe("update", () => {
it("should update a personality", async () => {
const updateDto = { description: "Updated" };
mockPersonalitiesService.update.mockResolvedValue({ ...mockPersonality, ...updateDto });
await controller.update(mockRequest, mockPersonalityId, updateDto);
const updateDto: UpdatePersonalityDto = {
description: "Updated description",
temperature: 0.9,
};
mockPersonalitiesService.update.mockResolvedValue({
...mockPersonality,
...updateDto,
});
const result = await controller.update(mockRequest, mockPersonalityId, updateDto);
expect(result).toMatchObject(updateDto);
expect(service.update).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId, updateDto);
});
});
describe("remove", () => {
describe("delete", () => {
it("should delete a personality", async () => {
mockPersonalitiesService.remove.mockResolvedValue(mockPersonality);
await controller.remove(mockRequest, mockPersonalityId);
expect(service.remove).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
mockPersonalitiesService.delete.mockResolvedValue(undefined);
await controller.delete(mockRequest, mockPersonalityId);
expect(service.delete).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
});
});
describe("previewPrompt", () => {
it("should return formatted system prompt", async () => {
const context = { userName: "John" };
mockPersonalitiesService.findOne.mockResolvedValue(mockPersonality);
mockPromptFormatterService.formatPrompt.mockReturnValue({
systemPrompt: "Formatted prompt",
metadata: {},
describe("setDefault", () => {
it("should set a personality as default", async () => {
mockPersonalitiesService.setDefault.mockResolvedValue({
...mockPersonality,
isDefault: true,
});
const result = await controller.previewPrompt(mockRequest, mockPersonalityId, context);
expect(result.systemPrompt).toBe("Formatted prompt");
const result = await controller.setDefault(mockRequest, mockPersonalityId);
expect(result).toMatchObject({ isDefault: true });
expect(service.setDefault).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
});
});
});