import { beforeAll, beforeEach, describe, expect, it, afterAll, vi } from "vitest"; import { randomUUID as uuid } from "crypto"; import { Test, TestingModule } from "@nestjs/testing"; import { ConflictException } from "@nestjs/common"; import { PrismaClient, Prisma } from "@prisma/client"; import { EMBEDDING_DIMENSION } from "@mosaic/shared"; import { ConversationArchiveService } from "./conversation-archive.service"; import { PrismaService } from "../prisma/prisma.service"; import { EmbeddingService } from "../knowledge/services/embedding.service"; const shouldRunDbIntegrationTests = process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL); const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip; function vector(value: number): number[] { return Array.from({ length: EMBEDDING_DIMENSION }, () => value); } function toVectorLiteral(input: number[]): string { return `[${input.join(",")}]`; } describeFn("ConversationArchiveService Integration", () => { let moduleRef: TestingModule; let prisma: PrismaClient; let service: ConversationArchiveService; let workspaceId: string; let ownerId: string; let setupComplete = false; const embeddingServiceMock = { isConfigured: vi.fn(), generateEmbedding: vi.fn(), }; beforeAll(async () => { prisma = new PrismaClient(); await prisma.$connect(); const workspace = await prisma.workspace.create({ data: { name: `Conversation Archive Integration ${Date.now()}`, owner: { create: { email: `conversation-archive-integration-${Date.now()}@example.com`, name: "Conversation Archive Integration Owner", }, }, }, }); workspaceId = workspace.id; ownerId = workspace.ownerId; moduleRef = await Test.createTestingModule({ providers: [ ConversationArchiveService, { provide: PrismaService, useValue: prisma, }, { provide: EmbeddingService, useValue: embeddingServiceMock, }, ], }).compile(); service = moduleRef.get(ConversationArchiveService); setupComplete = true; }); beforeEach(async () => { vi.clearAllMocks(); embeddingServiceMock.isConfigured.mockReturnValue(false); if (!setupComplete) { return; } await prisma.conversationArchive.deleteMany({ where: { workspaceId } }); }); afterAll(async () => { if (!prisma) { return; } if (workspaceId) { await prisma.conversationArchive.deleteMany({ where: { workspaceId } }); await prisma.workspace.deleteMany({ where: { id: workspaceId } }); } if (ownerId) { await prisma.user.deleteMany({ where: { id: ownerId } }); } if (moduleRef) { await moduleRef.close(); } await prisma.$disconnect(); }); it("ingests a conversation log", async () => { if (!setupComplete) { return; } const sessionId = `session-${uuid()}`; const result = await service.ingest(workspaceId, { sessionId, agentId: "agent-conversation-ingest", messages: [ { role: "user", content: "Can you summarize deployment issues?" }, { role: "assistant", content: "Yes, three retries timed out in staging." }, ], summary: "Deployment retry failures discussed", startedAt: "2026-02-28T21:00:00.000Z", endedAt: "2026-02-28T21:05:00.000Z", metadata: { source: "integration-test" }, }); expect(result.id).toBeDefined(); const stored = await prisma.conversationArchive.findUnique({ where: { id: result.id, }, }); expect(stored).toBeTruthy(); expect(stored?.workspaceId).toBe(workspaceId); expect(stored?.sessionId).toBe(sessionId); expect(stored?.messageCount).toBe(2); expect(stored?.summary).toBe("Deployment retry failures discussed"); }); it("rejects duplicate session ingest per workspace", async () => { if (!setupComplete) { return; } const sessionId = `session-${uuid()}`; const dto = { sessionId, agentId: "agent-conversation-duplicate", messages: [{ role: "user", content: "hello" }], summary: "simple conversation", startedAt: "2026-02-28T22:00:00.000Z", }; await service.ingest(workspaceId, dto); await expect(service.ingest(workspaceId, dto)).rejects.toThrow(ConflictException); }); it("rejects semantic search when embeddings are disabled", async () => { if (!setupComplete) { return; } embeddingServiceMock.isConfigured.mockReturnValue(false); await expect( service.search(workspaceId, { query: "deployment retries", }) ).rejects.toThrow(ConflictException); }); it("searches archived conversations by vector similarity", async () => { if (!setupComplete) { return; } const near = vector(0.02); const far = vector(0.85); const matching = await prisma.conversationArchive.create({ data: { workspaceId, sessionId: `session-search-${uuid()}`, agentId: "agent-conversation-search-a", messages: [ { role: "user", content: "What caused deployment retries?" }, { role: "assistant", content: "A connection pool timeout." }, ] as unknown as Prisma.InputJsonValue, messageCount: 2, summary: "Deployment retries caused by connection pool timeout", startedAt: new Date("2026-02-28T23:00:00.000Z"), metadata: { channel: "cli" } as Prisma.InputJsonValue, }, }); const nonMatching = await prisma.conversationArchive.create({ data: { workspaceId, sessionId: `session-search-${uuid()}`, agentId: "agent-conversation-search-b", messages: [ { role: "user", content: "How is billing configured?" }, ] as unknown as Prisma.InputJsonValue, messageCount: 1, summary: "Billing and quotas conversation", startedAt: new Date("2026-02-28T23:10:00.000Z"), metadata: { channel: "cli" } as Prisma.InputJsonValue, }, }); await prisma.$executeRaw` UPDATE conversation_archives SET embedding = ${toVectorLiteral(near)}::vector(${EMBEDDING_DIMENSION}) WHERE id = ${matching.id}::uuid `; await prisma.$executeRaw` UPDATE conversation_archives SET embedding = ${toVectorLiteral(far)}::vector(${EMBEDDING_DIMENSION}) WHERE id = ${nonMatching.id}::uuid `; embeddingServiceMock.isConfigured.mockReturnValue(true); embeddingServiceMock.generateEmbedding.mockResolvedValue(near); const result = await service.search(workspaceId, { query: "deployment retries timeout", agentId: "agent-conversation-search-a", similarityThreshold: 0, limit: 10, }); const rows = result.data as Array<{ id: string; agent_id: string; similarity: number }>; expect(result.pagination.total).toBe(1); expect(rows).toHaveLength(1); expect(rows[0]?.id).toBe(matching.id); expect(rows[0]?.agent_id).toBe("agent-conversation-search-a"); expect(rows[0]?.similarity).toBeGreaterThan(0); }); });