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:
Jason Woltje
2026-01-29 17:57:54 -06:00
parent 95833fb4ea
commit 5dd46c85af
43 changed files with 4782 additions and 2 deletions

View File

@@ -0,0 +1,157 @@
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 { AuthGuard } from "../auth/guards/auth.guard";
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
describe("PersonalitiesController", () => {
let controller: PersonalitiesController;
let service: PersonalitiesService;
const mockWorkspaceId = "workspace-123";
const mockUserId = "user-123";
const mockPersonalityId = "personality-123";
const mockRequest = {
user: { id: mockUserId },
workspaceId: mockWorkspaceId,
};
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 mockPersonalitiesService = {
findAll: vi.fn(),
findOne: vi.fn(),
findDefault: vi.fn(),
create: vi.fn(),
update: vi.fn(),
remove: vi.fn(),
};
const mockAuthGuard = {
canActivate: vi.fn().mockReturnValue(true),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PersonalitiesController],
providers: [
{
provide: PersonalitiesService,
useValue: mockPersonalitiesService,
},
],
})
.overrideGuard(AuthGuard)
.useValue(mockAuthGuard)
.compile();
controller = module.get<PersonalitiesController>(PersonalitiesController);
service = module.get<PersonalitiesService>(PersonalitiesService);
// Reset mocks
vi.clearAllMocks();
});
describe("findAll", () => {
it("should return all personalities", async () => {
const mockPersonalities = [mockPersonality];
mockPersonalitiesService.findAll.mockResolvedValue(mockPersonalities);
const result = await controller.findAll(mockRequest as any);
expect(result).toEqual(mockPersonalities);
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, true);
});
it("should filter by active status", async () => {
mockPersonalitiesService.findAll.mockResolvedValue([mockPersonality]);
await controller.findAll(mockRequest as any, false);
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, false);
});
});
describe("findOne", () => {
it("should return a personality by id", async () => {
mockPersonalitiesService.findOne.mockResolvedValue(mockPersonality);
const result = await controller.findOne(mockRequest as any, mockPersonalityId);
expect(result).toEqual(mockPersonality);
expect(service.findOne).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
});
});
describe("findDefault", () => {
it("should return the default personality", async () => {
mockPersonalitiesService.findDefault.mockResolvedValue(mockPersonality);
const result = await controller.findDefault(mockRequest as any);
expect(result).toEqual(mockPersonality);
expect(service.findDefault).toHaveBeenCalledWith(mockWorkspaceId);
});
});
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 () => {
const newPersonality = { ...mockPersonality, ...createDto, id: "new-id" };
mockPersonalitiesService.create.mockResolvedValue(newPersonality);
const result = await controller.create(mockRequest as any, createDto);
expect(result).toEqual(newPersonality);
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
});
});
describe("update", () => {
const updateDto: UpdatePersonalityDto = {
description: "Updated description",
};
it("should update a personality", async () => {
const updatedPersonality = { ...mockPersonality, ...updateDto };
mockPersonalitiesService.update.mockResolvedValue(updatedPersonality);
const result = await controller.update(mockRequest as any, mockPersonalityId, updateDto);
expect(result).toEqual(updatedPersonality);
expect(service.update).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId, updateDto);
});
});
describe("remove", () => {
it("should delete a personality", async () => {
mockPersonalitiesService.remove.mockResolvedValue(mockPersonality);
const result = await controller.remove(mockRequest as any, mockPersonalityId);
expect(result).toEqual(mockPersonality);
expect(service.remove).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
});
});
});