import type { SupabaseClient } from "@supabase/supabase-js"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { SupabaseStore } from "./supabase-store.js"; import type { Tag } from "./types.js"; function createMockSupabaseClient() { const mockSingle = vi.fn(); const mockSelect = vi.fn(() => ({ single: mockSingle })); const mockEq = vi.fn(() => ({ eq: mockEq, select: mockSelect, single: mockSingle, })); const mockContains = vi.fn(() => ({ data: [], error: null })); const mockDelete = vi.fn(() => ({ eq: mockEq })); const mockUpdate = vi.fn(() => ({ eq: mockEq })); const mockInsert = vi.fn(() => ({ select: mockSelect })); const mockFrom = vi.fn(() => ({ insert: mockInsert, select: mockSelect, update: mockUpdate, delete: mockDelete, eq: mockEq, contains: mockContains, })); const mockRpc = vi.fn(); return { from: mockFrom, rpc: mockRpc, _mocks: { from: mockFrom, insert: mockInsert, select: mockSelect, update: mockUpdate, delete: mockDelete, eq: mockEq, contains: mockContains, single: mockSingle, rpc: mockRpc, }, }; } describe("SupabaseStore", () => { const testUserId = "user-123"; let mockClient: ReturnType; let store: SupabaseStore; beforeEach(() => { vi.clearAllMocks(); mockClient = createMockSupabaseClient(); store = new SupabaseStore( mockClient as unknown as SupabaseClient, testUserId, ); }); describe("create", () => { it("builds correct insert query with required fields", async () => { const mockMemoryRow = { id: "memory-1", user_id: testUserId, project_id: "project-1", folder_id: null, content: "Test content", tags: [], metadata: {}, embedding: null, embedding_dimensions: null, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-01T00:00:00Z", }; mockClient._mocks.single.mockResolvedValue({ data: mockMemoryRow, error: null, }); await store.create({ content: "Test content", projectId: "project-1", }); expect(mockClient.from).toHaveBeenCalledWith("memories"); expect(mockClient._mocks.insert).toHaveBeenCalledWith({ user_id: testUserId, project_id: "project-1", folder_id: null, content: "Test content", tags: [], metadata: {}, }); }); it("builds correct insert query with all optional fields", async () => { const testTags: Tag[] = [{ id: "tag-1", name: "important" }]; const testMetadata = { source: "test" }; const testEmbedding = [0.1, 0.2, 0.3]; const mockMemoryRow = { id: "memory-1", user_id: testUserId, project_id: "project-1", folder_id: "folder-1", content: "Test content", tags: testTags, metadata: testMetadata, embedding: testEmbedding, embedding_dimensions: 3, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-01T00:00:00Z", }; mockClient._mocks.single.mockResolvedValue({ data: mockMemoryRow, error: null, }); await store.create({ content: "Test content", projectId: "project-1", folderId: "folder-1", tags: testTags, metadata: testMetadata, embedding: testEmbedding, }); expect(mockClient._mocks.insert).toHaveBeenCalledWith({ user_id: testUserId, project_id: "project-1", folder_id: "folder-1", content: "Test content", tags: testTags, metadata: testMetadata, embedding: testEmbedding, embedding_dimensions: 3, }); }); it("uses custom embedding dimensions when provided", async () => { const testEmbedding = [0.1, 0.2, 0.3]; const mockMemoryRow = { id: "memory-1", user_id: testUserId, project_id: "project-1", folder_id: null, content: "Test content", tags: [], metadata: {}, embedding: testEmbedding, embedding_dimensions: 768, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-01T00:00:00Z", }; mockClient._mocks.single.mockResolvedValue({ data: mockMemoryRow, error: null, }); await store.create({ content: "Test content", projectId: "project-1", embedding: testEmbedding, embeddingDimensions: 768, }); expect(mockClient._mocks.insert).toHaveBeenCalledWith({ user_id: testUserId, project_id: "project-1", folder_id: null, content: "Test content", tags: [], metadata: {}, embedding: testEmbedding, embedding_dimensions: 768, }); }); it("returns the created memory with correct field mapping", async () => { const mockMemoryRow = { id: "memory-1", user_id: testUserId, project_id: "project-1", folder_id: "folder-1", content: "Test content", tags: [{ id: "tag-1", name: "important" }], metadata: { source: "test" }, embedding: null, embedding_dimensions: null, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-02T00:00:00Z", }; mockClient._mocks.single.mockResolvedValue({ data: mockMemoryRow, error: null, }); const result = await store.create({ content: "Test content", projectId: "project-1", }); expect(result.id).toBe("memory-1"); expect(result.content).toBe("Test content"); expect(result.projectId).toBe("project-1"); expect(result.folderId).toBe("folder-1"); expect(result.tags).toEqual([{ id: "tag-1", name: "important" }]); expect(result.metadata).toEqual({ source: "test" }); expect(result.createdAt).toBeInstanceOf(Date); expect(result.updatedAt).toBeInstanceOf(Date); }); it("throws error when insert fails", async () => { mockClient._mocks.single.mockResolvedValue({ data: null, error: { message: "Database error" }, }); await expect( store.create({ content: "Test content", projectId: "project-1", }), ).rejects.toThrow("Failed to create memory: Database error"); }); }); describe("read", () => { it("queries by id and user_id", async () => { const mockMemoryRow = { id: "memory-1", user_id: testUserId, project_id: "project-1", folder_id: null, content: "Test content", tags: [], metadata: {}, embedding: null, embedding_dimensions: null, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-01T00:00:00Z", }; const mockSingleResult = vi.fn().mockResolvedValue({ data: mockMemoryRow, error: null, }); const mockSelectResult = vi.fn().mockReturnValue({ single: mockSingleResult, }); const mockEqUserId = vi.fn().mockReturnValue({ single: mockSingleResult, select: mockSelectResult, }); const mockEqId = vi.fn().mockReturnValue({ eq: mockEqUserId, }); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); const result = await store.read("memory-1"); expect(mockClient.from).toHaveBeenCalledWith("memories"); expect(mockSelect).toHaveBeenCalled(); expect(mockEqId).toHaveBeenCalledWith("id", "memory-1"); expect(mockEqUserId).toHaveBeenCalledWith("user_id", testUserId); expect(result.success).toBe(true); if (result.success) { expect(result.value.id).toBe("memory-1"); } }); it("returns failure when memory not found", async () => { const mockSingleResult = vi.fn().mockResolvedValue({ data: null, error: { message: "Not found" }, }); const mockEqUserId = vi.fn().mockReturnValue({ single: mockSingleResult, }); const mockEqId = vi.fn().mockReturnValue({ eq: mockEqUserId, }); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); const result = await store.read("non-existent-id"); expect(result.success).toBe(false); if (!result.success) { expect(result.error.type).toBe("MEMORY_NOT_FOUND"); expect(result.error.memoryId).toBe("non-existent-id"); } }); }); describe("update", () => { it("builds correct update query with content", async () => { const mockMemoryRow = { id: "memory-1", user_id: testUserId, project_id: "project-1", folder_id: null, content: "Updated content", tags: [], metadata: {}, embedding: null, embedding_dimensions: null, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-02T00:00:00Z", }; const mockSingleResult = vi.fn().mockResolvedValue({ data: mockMemoryRow, error: null, }); const mockSelectResult = vi.fn().mockReturnValue({ single: mockSingleResult, }); const mockEqUserId = vi.fn().mockReturnValue({ select: mockSelectResult, }); const mockEqId = vi.fn().mockReturnValue({ eq: mockEqUserId, }); const mockUpdate = vi.fn().mockReturnValue({ eq: mockEqId, }); mockClient.from = vi.fn().mockReturnValue({ update: mockUpdate, }); await store.update("memory-1", { content: "Updated content", }); expect(mockClient.from).toHaveBeenCalledWith("memories"); expect(mockUpdate).toHaveBeenCalledWith({ content: "Updated content" }); expect(mockEqId).toHaveBeenCalledWith("id", "memory-1"); expect(mockEqUserId).toHaveBeenCalledWith("user_id", testUserId); }); it("builds correct update query with all fields", async () => { const testTags: Tag[] = [{ id: "tag-1", name: "updated" }]; const testMetadata = { updated: true }; const testEmbedding = [0.4, 0.5, 0.6]; const mockMemoryRow = { id: "memory-1", user_id: testUserId, project_id: "project-1", folder_id: "folder-2", content: "Updated content", tags: testTags, metadata: testMetadata, embedding: testEmbedding, embedding_dimensions: 3, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-02T00:00:00Z", }; const mockSingleResult = vi.fn().mockResolvedValue({ data: mockMemoryRow, error: null, }); const mockSelectResult = vi.fn().mockReturnValue({ single: mockSingleResult, }); const mockEqUserId = vi.fn().mockReturnValue({ select: mockSelectResult, }); const mockEqId = vi.fn().mockReturnValue({ eq: mockEqUserId, }); const mockUpdate = vi.fn().mockReturnValue({ eq: mockEqId, }); mockClient.from = vi.fn().mockReturnValue({ update: mockUpdate, }); await store.update("memory-1", { content: "Updated content", folderId: "folder-2", tags: testTags, metadata: testMetadata, embedding: testEmbedding, embeddingDimensions: 768, }); expect(mockUpdate).toHaveBeenCalledWith({ content: "Updated content", folder_id: "folder-2", tags: testTags, metadata: testMetadata, embedding: testEmbedding, embedding_dimensions: 768, }); }); it("returns failure when memory not found", async () => { const mockSingleResult = vi.fn().mockResolvedValue({ data: null, error: { message: "Not found" }, }); const mockSelectResult = vi.fn().mockReturnValue({ single: mockSingleResult, }); const mockEqUserId = vi.fn().mockReturnValue({ select: mockSelectResult, }); const mockEqId = vi.fn().mockReturnValue({ eq: mockEqUserId, }); const mockUpdate = vi.fn().mockReturnValue({ eq: mockEqId, }); mockClient.from = vi.fn().mockReturnValue({ update: mockUpdate, }); const result = await store.update("non-existent-id", { content: "Updated content", }); expect(result.success).toBe(false); if (!result.success) { expect(result.error.type).toBe("MEMORY_NOT_FOUND"); expect(result.error.memoryId).toBe("non-existent-id"); } }); }); describe("delete", () => { it("builds correct delete query", async () => { const mockEqUserId = vi.fn().mockResolvedValue({ error: null, count: 1, }); const mockEqId = vi.fn().mockReturnValue({ eq: mockEqUserId, }); const mockDelete = vi.fn().mockReturnValue({ eq: mockEqId, }); mockClient.from = vi.fn().mockReturnValue({ delete: mockDelete, }); const result = await store.delete("memory-1"); expect(mockClient.from).toHaveBeenCalledWith("memories"); expect(mockDelete).toHaveBeenCalledWith({ count: "exact" }); expect(mockEqId).toHaveBeenCalledWith("id", "memory-1"); expect(mockEqUserId).toHaveBeenCalledWith("user_id", testUserId); expect(result.success).toBe(true); }); it("returns failure when memory not found", async () => { const mockEqUserId = vi.fn().mockResolvedValue({ error: null, count: 0, }); const mockEqId = vi.fn().mockReturnValue({ eq: mockEqUserId, }); const mockDelete = vi.fn().mockReturnValue({ eq: mockEqId, }); mockClient.from = vi.fn().mockReturnValue({ delete: mockDelete, }); const result = await store.delete("non-existent-id"); expect(result.success).toBe(false); if (!result.success) { expect(result.error.type).toBe("MEMORY_NOT_FOUND"); expect(result.error.memoryId).toBe("non-existent-id"); } }); it("returns failure when delete encounters error", async () => { const mockEqUserId = vi.fn().mockResolvedValue({ error: { message: "Database error" }, count: null, }); const mockEqId = vi.fn().mockReturnValue({ eq: mockEqUserId, }); const mockDelete = vi.fn().mockReturnValue({ eq: mockEqId, }); mockClient.from = vi.fn().mockReturnValue({ delete: mockDelete, }); const result = await store.delete("memory-1"); expect(result.success).toBe(false); if (!result.success) { expect(result.error.type).toBe("MEMORY_NOT_FOUND"); } }); }); describe("list", () => { it("queries with user_id filter only when no filter provided", async () => { const mockQueryResult = Promise.resolve({ data: [], error: null }); const mockEqUserId = vi.fn().mockReturnValue(mockQueryResult); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqUserId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); await store.list(); expect(mockClient.from).toHaveBeenCalledWith("memories"); expect(mockSelect).toHaveBeenCalled(); expect(mockEqUserId).toHaveBeenCalledWith("user_id", testUserId); }); it("adds projectId filter when provided", async () => { const mockQueryResult = Promise.resolve({ data: [], error: null }); const mockEqProjectId = vi.fn().mockReturnValue(mockQueryResult); const mockEqUserId = vi.fn().mockReturnValue({ eq: mockEqProjectId, }); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqUserId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); await store.list({ projectId: "project-1" }); expect(mockEqProjectId).toHaveBeenCalledWith("project_id", "project-1"); }); it("adds folderId filter when provided", async () => { const mockQueryResult = Promise.resolve({ data: [], error: null }); const mockEqFolderId = vi.fn().mockReturnValue(mockQueryResult); const mockEqUserId = vi.fn().mockReturnValue({ eq: mockEqFolderId, }); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqUserId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); await store.list({ folderId: "folder-1" }); expect(mockEqFolderId).toHaveBeenCalledWith("folder_id", "folder-1"); }); it("adds tags filter with contains when provided", async () => { const mockQueryResult = Promise.resolve({ data: [], error: null }); const mockContains = vi.fn().mockReturnValue(mockQueryResult); const mockEqUserId = vi.fn().mockReturnValue({ contains: mockContains, }); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqUserId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); await store.list({ tags: ["tag-1", "tag-2"] }); expect(mockContains).toHaveBeenCalledWith("tags", [ { id: "tag-1" }, { id: "tag-2" }, ]); }); it("applies multiple filters together", async () => { const mockQueryResult = Promise.resolve({ data: [], error: null }); const mockContains = vi.fn().mockReturnValue(mockQueryResult); const mockEqFolderId = vi.fn().mockReturnValue({ contains: mockContains, }); const mockEqProjectId = vi.fn().mockReturnValue({ eq: mockEqFolderId, }); const mockEqUserId = vi.fn().mockReturnValue({ eq: mockEqProjectId, }); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqUserId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); await store.list({ projectId: "project-1", folderId: "folder-1", tags: ["tag-1"], }); expect(mockEqProjectId).toHaveBeenCalledWith("project_id", "project-1"); expect(mockEqFolderId).toHaveBeenCalledWith("folder_id", "folder-1"); expect(mockContains).toHaveBeenCalledWith("tags", [{ id: "tag-1" }]); }); it("returns mapped memories from query result", async () => { const mockMemoryRows = [ { id: "memory-1", user_id: testUserId, project_id: "project-1", folder_id: null, content: "Content 1", tags: [], metadata: {}, embedding: null, embedding_dimensions: null, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-01T00:00:00Z", }, { id: "memory-2", user_id: testUserId, project_id: "project-1", folder_id: "folder-1", content: "Content 2", tags: [{ id: "tag-1", name: "important" }], metadata: { source: "test" }, embedding: null, embedding_dimensions: null, created_at: "2024-01-02T00:00:00Z", updated_at: "2024-01-02T00:00:00Z", }, ]; const mockQueryResult = Promise.resolve({ data: mockMemoryRows, error: null, }); const mockEqUserId = vi.fn().mockReturnValue(mockQueryResult); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqUserId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); const result = await store.list(); expect(result).toHaveLength(2); expect(result[0]?.id).toBe("memory-1"); expect(result[0]?.projectId).toBe("project-1"); expect(result[1]?.id).toBe("memory-2"); expect(result[1]?.folderId).toBe("folder-1"); expect(result[1]?.tags).toEqual([{ id: "tag-1", name: "important" }]); }); it("throws error when list fails", async () => { const mockQueryResult = Promise.resolve({ data: null, error: { message: "Database error" }, }); const mockEqUserId = vi.fn().mockReturnValue(mockQueryResult); const mockSelect = vi.fn().mockReturnValue({ eq: mockEqUserId, }); mockClient.from = vi.fn().mockReturnValue({ select: mockSelect, }); await expect(store.list()).rejects.toThrow( "Failed to list memories: Database error", ); }); }); describe("search", () => { it("calls rpc with correct parameters", async () => { const testEmbedding = [0.1, 0.2, 0.3]; mockClient.rpc.mockResolvedValue({ data: [], error: null, }); await store.search(testEmbedding); expect(mockClient.rpc).toHaveBeenCalledWith("search_memories", { query_embedding: testEmbedding, match_threshold: 0.7, match_count: 10, filter_project_id: null, filter_folder_id: null, }); }); it("passes custom threshold when provided", async () => { const testEmbedding = [0.1, 0.2, 0.3]; mockClient.rpc.mockResolvedValue({ data: [], error: null, }); await store.search(testEmbedding, { threshold: 0.8 }); expect(mockClient.rpc).toHaveBeenCalledWith("search_memories", { query_embedding: testEmbedding, match_threshold: 0.8, match_count: 10, filter_project_id: null, filter_folder_id: null, }); }); it("passes custom limit when provided", async () => { const testEmbedding = [0.1, 0.2, 0.3]; mockClient.rpc.mockResolvedValue({ data: [], error: null, }); await store.search(testEmbedding, { limit: 5 }); expect(mockClient.rpc).toHaveBeenCalledWith("search_memories", { query_embedding: testEmbedding, match_threshold: 0.7, match_count: 5, filter_project_id: null, filter_folder_id: null, }); }); it("passes projectId filter when provided", async () => { const testEmbedding = [0.1, 0.2, 0.3]; mockClient.rpc.mockResolvedValue({ data: [], error: null, }); await store.search(testEmbedding, { projectId: "project-1" }); expect(mockClient.rpc).toHaveBeenCalledWith("search_memories", { query_embedding: testEmbedding, match_threshold: 0.7, match_count: 10, filter_project_id: "project-1", filter_folder_id: null, }); }); it("passes folderId filter when provided", async () => { const testEmbedding = [0.1, 0.2, 0.3]; mockClient.rpc.mockResolvedValue({ data: [], error: null, }); await store.search(testEmbedding, { folderId: "folder-1" }); expect(mockClient.rpc).toHaveBeenCalledWith("search_memories", { query_embedding: testEmbedding, match_threshold: 0.7, match_count: 10, filter_project_id: null, filter_folder_id: "folder-1", }); }); it("passes all options when provided", async () => { const testEmbedding = [0.1, 0.2, 0.3]; mockClient.rpc.mockResolvedValue({ data: [], error: null, }); await store.search(testEmbedding, { threshold: 0.9, limit: 20, projectId: "project-1", folderId: "folder-1", }); expect(mockClient.rpc).toHaveBeenCalledWith("search_memories", { query_embedding: testEmbedding, match_threshold: 0.9, match_count: 20, filter_project_id: "project-1", filter_folder_id: "folder-1", }); }); it("returns mapped search results", async () => { const testEmbedding = [0.1, 0.2, 0.3]; const mockSearchResultRows = [ { id: "memory-1", content: "Test content 1", project_id: "project-1", folder_id: null, tags: [], metadata: {}, similarity: 0.95, created_at: "2024-01-01T00:00:00Z", updated_at: "2024-01-01T00:00:00Z", }, { id: "memory-2", content: "Test content 2", project_id: "project-1", folder_id: "folder-1", tags: [{ id: "tag-1", name: "important" }], metadata: { source: "test" }, similarity: 0.85, created_at: "2024-01-02T00:00:00Z", updated_at: "2024-01-02T00:00:00Z", }, ]; mockClient.rpc.mockResolvedValue({ data: mockSearchResultRows, error: null, }); const result = await store.search(testEmbedding); expect(result).toHaveLength(2); expect(result[0]?.id).toBe("memory-1"); expect(result[0]?.content).toBe("Test content 1"); expect(result[0]?.similarity).toBe(0.95); expect(result[0]?.projectId).toBe("project-1"); expect(result[0]?.folderId).toBeNull(); expect(result[0]?.createdAt).toBeInstanceOf(Date); expect(result[1]?.id).toBe("memory-2"); expect(result[1]?.similarity).toBe(0.85); expect(result[1]?.folderId).toBe("folder-1"); expect(result[1]?.tags).toEqual([{ id: "tag-1", name: "important" }]); }); it("throws error when search fails", async () => { const testEmbedding = [0.1, 0.2, 0.3]; mockClient.rpc.mockResolvedValue({ data: null, error: { message: "RPC error" }, }); await expect(store.search(testEmbedding)).rejects.toThrow( "Failed to search memories: RPC error", ); }); }); });