All checks were successful
ci/woodpecker/push/api Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
365 lines
13 KiB
TypeScript
365 lines
13 KiB
TypeScript
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>(PersonalitiesService);
|
|
prisma = module.get<PrismaService>(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
|
|
);
|
|
});
|
|
});
|
|
});
|