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,168 @@
import { describe, expect, it } from "vitest";
import { validate } from "class-validator";
import { plainToClass } from "class-transformer";
import { QueryTasksDto } from "./query-tasks.dto";
import { TaskStatus, TaskPriority } from "@prisma/client";
import { SortOrder } from "../../common/dto";
describe("QueryTasksDto", () => {
const validWorkspaceId = "123e4567-e89b-12d3-a456-426614174000";
it("should accept valid workspaceId", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
});
it("should reject invalid workspaceId", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: "not-a-uuid",
});
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
expect(errors.some(e => e.property === "workspaceId")).toBe(true);
});
it("should accept valid status filter", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
status: TaskStatus.IN_PROGRESS,
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(Array.isArray(dto.status)).toBe(true);
expect(dto.status).toEqual([TaskStatus.IN_PROGRESS]);
});
it("should accept multiple status filters", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
status: [TaskStatus.IN_PROGRESS, TaskStatus.NOT_STARTED],
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(Array.isArray(dto.status)).toBe(true);
expect(dto.status).toHaveLength(2);
});
it("should accept valid priority filter", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
priority: TaskPriority.HIGH,
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(Array.isArray(dto.priority)).toBe(true);
expect(dto.priority).toEqual([TaskPriority.HIGH]);
});
it("should accept multiple priority filters", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
priority: [TaskPriority.HIGH, TaskPriority.LOW],
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(Array.isArray(dto.priority)).toBe(true);
expect(dto.priority).toHaveLength(2);
});
it("should accept search parameter", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
search: "test task",
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(dto.search).toBe("test task");
});
it("should accept sortBy parameter", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
sortBy: "priority,dueDate",
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(dto.sortBy).toBe("priority,dueDate");
});
it("should accept sortOrder parameter", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
sortOrder: SortOrder.ASC,
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(dto.sortOrder).toBe(SortOrder.ASC);
});
it("should accept domainId filter", async () => {
const domainId = "123e4567-e89b-12d3-a456-426614174001";
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
domainId,
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(Array.isArray(dto.domainId)).toBe(true);
expect(dto.domainId).toEqual([domainId]);
});
it("should accept multiple domainId filters", async () => {
const domainIds = [
"123e4567-e89b-12d3-a456-426614174001",
"123e4567-e89b-12d3-a456-426614174002",
];
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
domainId: domainIds,
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
expect(Array.isArray(dto.domainId)).toBe(true);
expect(dto.domainId).toHaveLength(2);
});
it("should accept date range filters", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
dueDateFrom: "2024-01-01T00:00:00Z",
dueDateTo: "2024-12-31T23:59:59Z",
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
});
it("should accept all filters combined", async () => {
const dto = plainToClass(QueryTasksDto, {
workspaceId: validWorkspaceId,
status: [TaskStatus.IN_PROGRESS, TaskStatus.NOT_STARTED],
priority: [TaskPriority.HIGH, TaskPriority.MEDIUM],
search: "urgent task",
sortBy: "priority,dueDate",
sortOrder: SortOrder.ASC,
page: 2,
limit: 25,
dueDateFrom: "2024-01-01T00:00:00Z",
dueDateTo: "2024-12-31T23:59:59Z",
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
});
});