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:
410
apps/api/src/knowledge/services/link-sync.service.spec.ts
Normal file
410
apps/api/src/knowledge/services/link-sync.service.spec.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { LinkSyncService } from "./link-sync.service";
|
||||
import { LinkResolutionService } from "./link-resolution.service";
|
||||
import { PrismaService } from "../../prisma/prisma.service";
|
||||
import * as wikiLinkParser from "../utils/wiki-link-parser";
|
||||
|
||||
// Mock the wiki-link parser
|
||||
vi.mock("../utils/wiki-link-parser");
|
||||
const mockParseWikiLinks = vi.mocked(wikiLinkParser.parseWikiLinks);
|
||||
|
||||
describe("LinkSyncService", () => {
|
||||
let service: LinkSyncService;
|
||||
let prisma: PrismaService;
|
||||
let linkResolver: LinkResolutionService;
|
||||
|
||||
const mockWorkspaceId = "workspace-1";
|
||||
const mockEntryId = "entry-1";
|
||||
const mockTargetId = "entry-2";
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LinkSyncService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: {
|
||||
knowledgeLink: {
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn((fn) => fn(prisma)),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: LinkResolutionService,
|
||||
useValue: {
|
||||
resolveLink: vi.fn(),
|
||||
resolveLinks: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LinkSyncService>(LinkSyncService);
|
||||
prisma = module.get<PrismaService>(PrismaService);
|
||||
linkResolver = module.get<LinkResolutionService>(LinkResolutionService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("syncLinks", () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it("should parse wiki links from content", async () => {
|
||||
const content = "This is a [[Test Link]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Test Link]]",
|
||||
target: "Test Link",
|
||||
displayText: "Test Link",
|
||||
start: 10,
|
||||
end: 25,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(mockParseWikiLinks).toHaveBeenCalledWith(content);
|
||||
});
|
||||
|
||||
it("should create new links when parsing finds wiki links", async () => {
|
||||
const content = "This is a [[Test Link]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Test Link]]",
|
||||
target: "Test Link",
|
||||
displayText: "Test Link",
|
||||
start: 10,
|
||||
end: 25,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({
|
||||
id: "link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: mockTargetId,
|
||||
linkText: "Test Link",
|
||||
displayText: "Test Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 25,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
sourceId: mockEntryId,
|
||||
targetId: mockTargetId,
|
||||
linkText: "Test Link",
|
||||
displayText: "Test Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 25,
|
||||
resolved: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should create unresolved links when target cannot be found", async () => {
|
||||
const content = "This is a [[Nonexistent Link]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Nonexistent Link]]",
|
||||
target: "Nonexistent Link",
|
||||
displayText: "Nonexistent Link",
|
||||
start: 10,
|
||||
end: 32,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(null);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({
|
||||
id: "link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: null,
|
||||
linkText: "Nonexistent Link",
|
||||
displayText: "Nonexistent Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 32,
|
||||
resolved: false,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
sourceId: mockEntryId,
|
||||
targetId: null,
|
||||
linkText: "Nonexistent Link",
|
||||
displayText: "Nonexistent Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 32,
|
||||
resolved: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle custom display text in links", async () => {
|
||||
const content = "This is a [[Target|Custom Display]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Target|Custom Display]]",
|
||||
target: "Target",
|
||||
displayText: "Custom Display",
|
||||
start: 10,
|
||||
end: 35,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
sourceId: mockEntryId,
|
||||
targetId: mockTargetId,
|
||||
linkText: "Target",
|
||||
displayText: "Custom Display",
|
||||
positionStart: 10,
|
||||
positionEnd: 35,
|
||||
resolved: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete orphaned links not present in updated content", async () => {
|
||||
const content = "This is a [[New Link]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[New Link]]",
|
||||
target: "New Link",
|
||||
displayText: "New Link",
|
||||
start: 10,
|
||||
end: 22,
|
||||
},
|
||||
]);
|
||||
|
||||
// Mock existing link that should be removed
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([
|
||||
{
|
||||
id: "old-link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: "old-target",
|
||||
linkText: "Old Link",
|
||||
displayText: "Old Link",
|
||||
positionStart: 5,
|
||||
positionEnd: 17,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
] as any);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
|
||||
vi.spyOn(prisma.knowledgeLink, "deleteMany").mockResolvedValue({ count: 1 });
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
sourceId: mockEntryId,
|
||||
id: {
|
||||
in: ["old-link-1"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle empty content by removing all links", async () => {
|
||||
const content = "";
|
||||
mockParseWikiLinks.mockReturnValue([]);
|
||||
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([
|
||||
{
|
||||
id: "link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: mockTargetId,
|
||||
linkText: "Link",
|
||||
displayText: "Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 18,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
] as any);
|
||||
|
||||
vi.spyOn(prisma.knowledgeLink, "deleteMany").mockResolvedValue({ count: 1 });
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
sourceId: mockEntryId,
|
||||
id: {
|
||||
in: ["link-1"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle multiple links in content", async () => {
|
||||
const content = "Links: [[Link 1]] and [[Link 2]] and [[Link 3]]";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Link 1]]",
|
||||
target: "Link 1",
|
||||
displayText: "Link 1",
|
||||
start: 7,
|
||||
end: 17,
|
||||
},
|
||||
{
|
||||
raw: "[[Link 2]]",
|
||||
target: "Link 2",
|
||||
displayText: "Link 2",
|
||||
start: 22,
|
||||
end: 32,
|
||||
},
|
||||
{
|
||||
raw: "[[Link 3]]",
|
||||
target: "Link 3",
|
||||
displayText: "Link 3",
|
||||
start: 37,
|
||||
end: 47,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.create).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBacklinks", () => {
|
||||
it("should return all backlinks for an entry", async () => {
|
||||
const mockBacklinks = [
|
||||
{
|
||||
id: "link-1",
|
||||
sourceId: "source-1",
|
||||
targetId: mockEntryId,
|
||||
linkText: "Link Text",
|
||||
displayText: "Link Text",
|
||||
positionStart: 10,
|
||||
positionEnd: 25,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
source: {
|
||||
id: "source-1",
|
||||
title: "Source Entry",
|
||||
slug: "source-entry",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue(mockBacklinks as any);
|
||||
|
||||
const result = await service.getBacklinks(mockEntryId);
|
||||
|
||||
expect(prisma.knowledgeLink.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
targetId: mockEntryId,
|
||||
resolved: true,
|
||||
},
|
||||
include: {
|
||||
source: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockBacklinks);
|
||||
});
|
||||
|
||||
it("should return empty array when no backlinks exist", async () => {
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
|
||||
const result = await service.getBacklinks(mockEntryId);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUnresolvedLinks", () => {
|
||||
it("should return all unresolved links for a workspace", async () => {
|
||||
const mockUnresolvedLinks = [
|
||||
{
|
||||
id: "link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: null,
|
||||
linkText: "Unresolved Link",
|
||||
displayText: "Unresolved Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 29,
|
||||
resolved: false,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue(mockUnresolvedLinks as any);
|
||||
|
||||
const result = await service.getUnresolvedLinks(mockWorkspaceId);
|
||||
|
||||
expect(prisma.knowledgeLink.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
source: {
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
resolved: false,
|
||||
},
|
||||
include: {
|
||||
source: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockUnresolvedLinks);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user