import { describe, it, expect, vi, beforeEach } from "vitest"; import { chatFileService, deriveCategory } from "../services/chat-files.js"; // Mock @paperclipai/db module const mockReturning = vi.fn(); const mockThen = vi.fn(); const mockLimit = vi.fn(); const mockOrderBy = vi.fn(); const mockWhere = vi.fn(); const mockSet = vi.fn(); const mockValues = vi.fn(); const mockInsert = vi.fn(); const mockSelect = vi.fn(); const mockFrom = vi.fn(); const mockUpdate = vi.fn(); vi.mock("@paperclipai/db", () => ({ chatFiles: { id: "id", companyId: "company_id", conversationId: "conversation_id", messageId: "message_id" }, chatFileReferences: { id: "id", fileId: "file_id" }, })); function makeDb() { const returning = vi.fn(); const limit = vi.fn(); const orderBy = vi.fn(); const where = vi.fn(); const set = vi.fn(); const values = vi.fn(); const from = vi.fn(); // Chain setup const insertChain = { values: values.mockReturnValue({ returning }), }; const selectChain = { from: from.mockReturnValue({ where: where.mockReturnValue({ orderBy: orderBy.mockReturnValue({ limit }), then: undefined as unknown as typeof Promise.prototype.then }) }), }; const updateChain = { set: set.mockReturnValue({ where: where.mockReturnValue({ returning }) }), }; const db = { insert: vi.fn().mockReturnValue(insertChain), select: vi.fn().mockReturnValue(selectChain), update: vi.fn().mockReturnValue(updateChain), }; return { db, returning, limit, orderBy, where, set, values, from }; } describe("deriveCategory", () => { it("returns 'image' for image/* mime types", () => { expect(deriveCategory("image/png")).toBe("image"); expect(deriveCategory("image/jpeg")).toBe("image"); expect(deriveCategory("image/gif")).toBe("image"); expect(deriveCategory("image/webp")).toBe("image"); expect(deriveCategory("image/svg+xml")).toBe("image"); }); it("returns 'code' for code mime types", () => { expect(deriveCategory("text/javascript")).toBe("code"); expect(deriveCategory("text/typescript")).toBe("code"); expect(deriveCategory("text/css")).toBe("code"); expect(deriveCategory("text/html")).toBe("code"); expect(deriveCategory("application/json")).toBe("code"); expect(deriveCategory("text/x-python")).toBe("code"); expect(deriveCategory("text/x-java")).toBe("code"); }); it("returns 'document' for document mime types", () => { expect(deriveCategory("application/pdf")).toBe("document"); expect(deriveCategory("text/plain")).toBe("document"); expect(deriveCategory("text/markdown")).toBe("document"); expect(deriveCategory("text/csv")).toBe("document"); }); it("returns 'other' for unrecognized mime types", () => { expect(deriveCategory("application/octet-stream")).toBe("other"); expect(deriveCategory("video/mp4")).toBe("other"); expect(deriveCategory("audio/mpeg")).toBe("other"); expect(deriveCategory("application/zip")).toBe("other"); }); it("is case-insensitive", () => { expect(deriveCategory("IMAGE/PNG")).toBe("image"); expect(deriveCategory("TEXT/JAVASCRIPT")).toBe("code"); expect(deriveCategory("APPLICATION/PDF")).toBe("document"); }); }); describe("chatFileService", () => { describe("create", () => { it("inserts a row and returns the inserted record", async () => { const mockFile = { id: "file-1", companyId: "company-1", filename: "hello.txt", originalFilename: "hello.txt", mimeType: "text/plain", sizeBytes: 100, objectKey: "chat-files/hello.txt", sha256: "abc123", source: "user_upload", category: "document", conversationId: "conv-1", messageId: null, projectId: null, createdAt: new Date(), updatedAt: new Date(), }; const returningFn = vi.fn().mockResolvedValue([mockFile]); const valuesFn = vi.fn().mockReturnValue({ returning: returningFn }); const insertFn = vi.fn().mockReturnValue({ values: valuesFn }); const db = { insert: insertFn } as unknown as Parameters[0]; const svc = chatFileService(db); const result = await svc.create("company-1", { filename: "hello.txt", originalFilename: "hello.txt", mimeType: "text/plain", sizeBytes: 100, objectKey: "chat-files/hello.txt", sha256: "abc123", source: "user_upload", category: "document", conversationId: "conv-1", messageId: null, projectId: null, }); expect(result).toEqual(mockFile); expect(insertFn).toHaveBeenCalled(); expect(valuesFn).toHaveBeenCalledWith( expect.objectContaining({ companyId: "company-1", filename: "hello.txt" }), ); }); }); describe("getById", () => { it("returns null when no row found", async () => { const thenFn = vi.fn().mockImplementation((cb: (rows: unknown[]) => unknown) => Promise.resolve(cb([]))); const whereFn = vi.fn().mockReturnValue({ then: thenFn }); const fromFn = vi.fn().mockReturnValue({ where: whereFn }); const selectFn = vi.fn().mockReturnValue({ from: fromFn }); const db = { select: selectFn } as unknown as Parameters[0]; const svc = chatFileService(db); const result = await svc.getById("nonexistent-id"); expect(result).toBeNull(); }); it("returns the row when found", async () => { const mockFile = { id: "file-1", companyId: "company-1" }; const thenFn = vi.fn().mockImplementation((cb: (rows: unknown[]) => unknown) => Promise.resolve(cb([mockFile]))); const whereFn = vi.fn().mockReturnValue({ then: thenFn }); const fromFn = vi.fn().mockReturnValue({ where: whereFn }); const selectFn = vi.fn().mockReturnValue({ from: fromFn }); const db = { select: selectFn } as unknown as Parameters[0]; const svc = chatFileService(db); const result = await svc.getById("file-1"); expect(result).toEqual(mockFile); }); }); describe("listByConversation", () => { it("queries by conversationId with default limit 50", async () => { const mockFiles = [{ id: "file-1" }, { id: "file-2" }]; const limitFn = vi.fn().mockResolvedValue(mockFiles); const orderByFn = vi.fn().mockReturnValue({ limit: limitFn }); const whereFn = vi.fn().mockReturnValue({ orderBy: orderByFn }); const fromFn = vi.fn().mockReturnValue({ where: whereFn }); const selectFn = vi.fn().mockReturnValue({ from: fromFn }); const db = { select: selectFn } as unknown as Parameters[0]; const svc = chatFileService(db); const result = await svc.listByConversation("conv-1"); expect(result).toEqual(mockFiles); expect(limitFn).toHaveBeenCalledWith(50); }); it("respects custom limit", async () => { const limitFn = vi.fn().mockResolvedValue([]); const orderByFn = vi.fn().mockReturnValue({ limit: limitFn }); const whereFn = vi.fn().mockReturnValue({ orderBy: orderByFn }); const fromFn = vi.fn().mockReturnValue({ where: whereFn }); const selectFn = vi.fn().mockReturnValue({ from: fromFn }); const db = { select: selectFn } as unknown as Parameters[0]; const svc = chatFileService(db); await svc.listByConversation("conv-1", { limit: 10 }); expect(limitFn).toHaveBeenCalledWith(10); }); }); describe("attachToMessage", () => { it("updates messageId and returns updated row", async () => { const mockFile = { id: "file-1", messageId: "msg-1" }; const returningFn = vi.fn().mockResolvedValue([mockFile]); const whereFn = vi.fn().mockReturnValue({ returning: returningFn }); const setFn = vi.fn().mockReturnValue({ where: whereFn }); const updateFn = vi.fn().mockReturnValue({ set: setFn }); const db = { update: updateFn } as unknown as Parameters[0]; const svc = chatFileService(db); const result = await svc.attachToMessage("file-1", "msg-1"); expect(result).toEqual(mockFile); expect(setFn).toHaveBeenCalledWith( expect.objectContaining({ messageId: "msg-1" }), ); }); }); });