diff --git a/mem0-ts/src/client/tests/memoryClient.e2e.test.ts b/mem0-ts/src/client/tests/memoryClient.e2e.test.ts deleted file mode 100644 index 99d232a7b..000000000 --- a/mem0-ts/src/client/tests/memoryClient.e2e.test.ts +++ /dev/null @@ -1,501 +0,0 @@ -/** - * MemoryClient E2E integration tests. - * - * These tests exercise realistic usage patterns with mock HTTP responses. - * Skipped by default — run with MEM0_RUN_E2E=1 to enable. - * - * Run: MEM0_RUN_E2E=1 npx jest memoryClient.e2e.test.ts - */ -import { MemoryClient } from "../mem0"; -import type { - Memory, - AllUsers, - MemoryHistory, - User, - Messages, -} from "../mem0.types"; -import { - createMockFetch, - createMockMemory, - createMockMemoryHistory, - createMockUser, - createMockAllUsers, - TEST_API_KEY, - MOCK_PING_RESPONSE, -} from "./helpers"; - -const originalFetch = global.fetch; -const originalConsoleError = console.error; -const originalConsoleWarn = console.warn; - -beforeAll(() => { - jest.spyOn(console, "error").mockImplementation((...args: unknown[]) => { - if ( - String(args[0] ?? "").match( - /Telemetry|Failed to initialize|Failed to capture/, - ) - ) - return; - originalConsoleError(...args); - }); - jest.spyOn(console, "warn").mockImplementation((...args: unknown[]) => { - if (String(args[0] ?? "").match(/telemetry|Telemetry/)) return; - originalConsoleWarn(...args); - }); -}); - -afterAll(() => jest.restoreAllMocks()); -afterEach(() => { - global.fetch = originalFetch; -}); - -// Shared test data matching realistic API responses -const userId = "test_user_abc123"; -const memoryId = "mem_550e8400"; - -const mockMemory = createMockMemory({ - id: memoryId, - memory: "Alex is a vegetarian", - user_id: userId, - event: "ADD", - data: { memory: "Alex is a vegetarian" }, - categories: ["personal"], - metadata: null, - created_at: "2026-03-17T10:00:00Z", - updated_at: "2026-03-17T10:00:00Z", - score: 0.95, -}); - -function mockFetchForTest( - extraPatterns?: Record, -) { - const responses = new Map(); - responses.set("/v1/ping/", { status: 200, body: MOCK_PING_RESPONSE }); - responses.set("/v1/memories/search/", { status: 200, body: [mockMemory] }); - responses.set("/v2/memories/search/", { status: 200, body: [mockMemory] }); - responses.set("/history/", { - status: 200, - body: [ - createMockMemoryHistory({ - memory_id: memoryId, - user_id: userId, - event: "ADD", - old_memory: null, - new_memory: "Alex is a vegetarian", - }), - ], - }); - responses.set("/v1/entities/", { - status: 200, - body: createMockAllUsers([ - createMockUser({ id: "entity_1", name: userId, type: "user" }), - ]), - }); - // This must come last — it's a broad pattern that matches /v1/memories/:id/ and /v1/memories/ - responses.set("/v1/memories/", { status: 200, body: [mockMemory] }); - - if (extraPatterns) { - for (const [k, v] of Object.entries(extraPatterns)) { - responses.set(k, v); - } - } - - global.fetch = createMockFetch(responses); -} - -const describeOrSkip = process.env.MEM0_RUN_E2E ? describe : describe.skip; - -describeOrSkip("MemoryClient API (E2E)", () => { - beforeEach(() => mockFetchForTest()); - - const messages1 = [ - { role: "user" as const, content: "Hey, I am Alex. I'm now a vegetarian." }, - { role: "assistant" as const, content: "Hello Alex! Glad to hear!" }, - ]; - - describe("add messages", () => { - let res: Memory[]; - - beforeEach(async () => { - const client = new MemoryClient({ apiKey: TEST_API_KEY }); - res = await client.add(messages1, { user_id: userId }); - }); - - test("returns an array", () => { - expect(Array.isArray(res)).toBe(true); - }); - - test("first message has a string id", () => { - expect(typeof res[0].id).toBe("string"); - }); - - test("first message has a string data.memory", () => { - expect(typeof res[0].data?.memory).toBe("string"); - }); - - test("first message has a string event", () => { - expect(typeof res[0].event).toBe("string"); - }); - }); - - describe("retrieve specific memory by ID", () => { - let memory: Memory; - - beforeEach(async () => { - const client = new MemoryClient({ apiKey: TEST_API_KEY }); - const memories = await client.getAll({ user_id: userId }); - memory = Array.isArray(memories) ? memories[0] : memories; - }); - - test("returns string id", () => { - expect(typeof memory.id).toBe("string"); - }); - - test("returns string memory content", () => { - expect(typeof memory.memory).toBe("string"); - }); - - test("returns string user_id", () => { - expect(typeof memory.user_id).toBe("string"); - }); - - test("user_id matches the requested userId", () => { - expect(memory.user_id).toBe(userId); - }); - - test("metadata is null or an object", () => { - expect( - memory.metadata === null || typeof memory.metadata === "object", - ).toBe(true); - }); - - test("categories is an array or null", () => { - expect( - Array.isArray(memory.categories) || memory.categories === null, - ).toBe(true); - }); - - test("each category is a string", () => { - if (Array.isArray(memory.categories)) { - expect( - memory.categories.every((c: string) => typeof c === "string"), - ).toBe(true); - } - }); - - test("created_at is a valid date", () => { - expect(new Date(memory.created_at || "").toString()).not.toBe( - "Invalid Date", - ); - }); - - test("updated_at is a valid date", () => { - expect(new Date(memory.updated_at || "").toString()).not.toBe( - "Invalid Date", - ); - }); - }); - - describe("retrieve all users", () => { - let allUsers: AllUsers; - - beforeEach(async () => { - const client = new MemoryClient({ apiKey: TEST_API_KEY }); - allUsers = await client.users(); - }); - - test("count is a number", () => { - expect(typeof allUsers.count).toBe("number"); - }); - - test("first user has a string id", () => { - expect(typeof allUsers.results[0].id).toBe("string"); - }); - - test("first user has a string name", () => { - expect(typeof allUsers.results[0].name).toBe("string"); - }); - - test("first user has a string created_at", () => { - expect(typeof allUsers.results[0].created_at).toBe("string"); - }); - - test("first user has a string updated_at", () => { - expect(typeof allUsers.results[0].updated_at).toBe("string"); - }); - - test("first user has a number total_memories", () => { - expect(typeof allUsers.results[0].total_memories).toBe("number"); - }); - - test("first user has a string type", () => { - expect(typeof allUsers.results[0].type).toBe("string"); - }); - - test("results contain an entity matching userId", () => { - const entity = allUsers.results.find( - (user: User) => user.name === userId, - ); - expect(entity).not.toBeUndefined(); - }); - - test("matched entity has a string id", () => { - const entity = allUsers.results.find( - (user: User) => user.name === userId, - ); - expect(typeof entity?.id).toBe("string"); - }); - }); - - describe("retrieve all memories for the user", () => { - let memories: Memory[]; - let memory: Memory; - - beforeEach(async () => { - const client = new MemoryClient({ apiKey: TEST_API_KEY }); - memories = await client.getAll({ user_id: userId }); - memory = memories[0]; - }); - - test("returns an array", () => { - expect(Array.isArray(memories)).toBe(true); - }); - - test("first memory has a string id", () => { - expect(typeof memory.id).toBe("string"); - }); - - test("first memory has a string memory content", () => { - expect(typeof memory.memory).toBe("string"); - }); - - test("first memory has a string user_id", () => { - expect(typeof memory.user_id).toBe("string"); - }); - - test("first memory user_id matches the requested userId", () => { - expect(memory.user_id).toBe(userId); - }); - - test("first memory metadata is null or an object", () => { - expect( - memory.metadata === null || typeof memory.metadata === "object", - ).toBe(true); - }); - - test("first memory categories is an array or null", () => { - expect( - Array.isArray(memory.categories) || memory.categories === null, - ).toBe(true); - }); - - test("first memory created_at is a valid date", () => { - expect(new Date(memory.created_at || "").toString()).not.toBe( - "Invalid Date", - ); - }); - - test("first memory updated_at is a valid date", () => { - expect(new Date(memory.updated_at || "").toString()).not.toBe( - "Invalid Date", - ); - }); - }); - - describe("search with API version 2", () => { - let results: Memory[]; - let memory: Memory; - - beforeEach(async () => { - const client = new MemoryClient({ apiKey: TEST_API_KEY }); - results = await client.search("What do you know about me?", { - filters: { - OR: [{ user_id: userId }, { agent_id: "shopping-assistant" }], - }, - threshold: 0.1, - api_version: "v2", - }); - memory = results[0]; - }); - - test("returns an array", () => { - expect(Array.isArray(results)).toBe(true); - }); - - test("first result has a string id", () => { - expect(typeof memory.id).toBe("string"); - }); - - test("first result has a string memory content", () => { - expect(typeof memory.memory).toBe("string"); - }); - - test("first result metadata is null or an object", () => { - expect( - memory.metadata === null || typeof memory.metadata === "object", - ).toBe(true); - }); - - test("first result categories is an array or null", () => { - expect( - Array.isArray(memory.categories) || memory.categories === null, - ).toBe(true); - }); - - test("first result created_at is a valid date", () => { - expect(new Date(memory.created_at || "").toString()).not.toBe( - "Invalid Date", - ); - }); - - test("first result has a number score", () => { - expect(typeof memory.score).toBe("number"); - }); - }); - - describe("search with API version 1", () => { - let results: Memory[]; - let memory: Memory; - - beforeEach(async () => { - const client = new MemoryClient({ apiKey: TEST_API_KEY }); - results = await client.search("What is my name?", { - user_id: userId, - }); - memory = results[0]; - }); - - test("returns an array", () => { - expect(Array.isArray(results)).toBe(true); - }); - - test("first result has a string id", () => { - expect(typeof memory.id).toBe("string"); - }); - - test("first result has a string memory content", () => { - expect(typeof memory.memory).toBe("string"); - }); - - test("first result has a string user_id", () => { - expect(typeof memory.user_id).toBe("string"); - }); - - test("first result user_id matches the requested userId", () => { - expect(memory.user_id).toBe(userId); - }); - - test("first result has a number score", () => { - expect(typeof memory.score).toBe("number"); - }); - }); - - describe("retrieve history of a specific memory", () => { - let history: MemoryHistory[]; - let entry: MemoryHistory; - - beforeEach(async () => { - const client = new MemoryClient({ apiKey: TEST_API_KEY }); - history = await client.history(memoryId); - entry = history[0]; - }); - - test("returns an array", () => { - expect(Array.isArray(history)).toBe(true); - }); - - test("first entry has a string id", () => { - expect(typeof entry.id).toBe("string"); - }); - - test("first entry has a string memory_id", () => { - expect(typeof entry.memory_id).toBe("string"); - }); - - test("first entry has a string user_id", () => { - expect(typeof entry.user_id).toBe("string"); - }); - - test("first entry user_id matches the requested userId", () => { - expect(entry.user_id).toBe(userId); - }); - - test("old_memory is null or a string", () => { - expect( - entry.old_memory === null || typeof entry.old_memory === "string", - ).toBe(true); - }); - - test("new_memory is null or a string", () => { - expect( - entry.new_memory === null || typeof entry.new_memory === "string", - ).toBe(true); - }); - - test("created_at is a valid date", () => { - expect(new Date(entry.created_at).toString()).not.toBe("Invalid Date"); - }); - - test("updated_at is a valid date", () => { - expect(new Date(entry.updated_at).toString()).not.toBe("Invalid Date"); - }); - - test("event is one of ADD, UPDATE, DELETE, NOOP", () => { - expect(["ADD", "UPDATE", "DELETE", "NOOP"]).toContain(entry.event); - }); - - test("ADD event has null old_memory", () => { - expect(entry.old_memory).toBeNull(); - }); - - test("ADD event has non-null new_memory", () => { - expect(entry.new_memory).not.toBeNull(); - }); - - test("input is an array or null", () => { - expect(Array.isArray(entry.input) || entry.input === null).toBe(true); - }); - - test("each input item is an object", () => { - if (Array.isArray(entry.input)) { - expect(entry.input.every((i: Messages) => typeof i === "object")).toBe( - true, - ); - } - }); - - test("each input item has a string content", () => { - if (Array.isArray(entry.input)) { - expect( - entry.input.every((i: Messages) => typeof i.content === "string"), - ).toBe(true); - } - }); - - test("each input item has a valid role", () => { - if (Array.isArray(entry.input)) { - expect( - entry.input.every((i: Messages) => - ["user", "assistant"].includes(i.role), - ), - ).toBe(true); - } - }); - }); - - describe("delete user", () => { - test("returns success message", async () => { - const client = new MemoryClient({ - apiKey: TEST_API_KEY, - organizationId: "org_test", - projectId: "proj_test", - }); - client.client.delete = jest.fn().mockResolvedValue({ - data: { message: "Entity deleted successfully!" }, - }); - - const result = await client.deleteUsers({ user_id: userId }); - expect(result.message).toBe("Entity deleted successfully."); - }); - }); -}); diff --git a/mem0-ts/src/oss/tests/lmstudio-integration.test.ts b/mem0-ts/src/oss/tests/lmstudio-integration.test.ts deleted file mode 100644 index 839381434..000000000 --- a/mem0-ts/src/oss/tests/lmstudio-integration.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -/// -/** - * LM Studio integration tests against a real local server. - * Skipped by default. Enable with: LMSTUDIO_INTEGRATION=1 - * - * Prerequisites: - * 1. LM Studio installed with `lms` CLI - * 2. Server running: lms server start - * 3. Embedding model loaded: lms load text-embedding-nomic-embed-text-v1.5 - * 4. (Optional) Chat model loaded for LLM tests - */ - -import { LMStudioEmbedder } from "../src/embeddings/lmstudio"; -import { LMStudioLLM } from "../src/llms/lmstudio"; - -const LMSTUDIO_BASE_URL = - process.env.LMSTUDIO_BASE_URL || "http://localhost:1234/v1"; -const RUN_INTEGRATION = process.env.LMSTUDIO_INTEGRATION === "1"; -const describeIf = RUN_INTEGRATION ? describe : describe.skip; - -jest.setTimeout(120_000); - -async function listModels(): Promise<{ - embedding: string | null; - chat: string | null; -}> { - const res = await fetch(`${LMSTUDIO_BASE_URL}/models`); - const body = await res.json(); - const models: any[] = body.data || []; - const embedding = models.find( - (m) => m.id.includes("embed") || m.id.includes("nomic"), - ); - const chat = models.find( - (m) => !m.id.includes("embed") && !m.id.includes("nomic"), - ); - return { embedding: embedding?.id ?? null, chat: chat?.id ?? null }; -} - -function cosineSim(a: number[], b: number[]): number { - let dot = 0, - normA = 0, - normB = 0; - for (let i = 0; i < a.length; i++) { - dot += a[i] * b[i]; - normA += a[i] * a[i]; - normB += b[i] * b[i]; - } - const denom = Math.sqrt(normA) * Math.sqrt(normB); - return denom === 0 ? 0 : dot / denom; -} - -describeIf("LM Studio Integration", () => { - it("server is reachable and lists models", async () => { - const res = await fetch(`${LMSTUDIO_BASE_URL}/models`); - expect(res.ok).toBe(true); - const body = await res.json(); - expect(body.data).toBeDefined(); - console.log( - "Loaded models:", - body.data.map((m: any) => m.id), - ); - }); - - // ─── Embedder ──────────────────────────────────────────────────────── - describe("LMStudioEmbedder (real server)", () => { - let embedder: LMStudioEmbedder; - let modelId: string; - - beforeAll(async () => { - const models = await listModels(); - if (!models.embedding) throw new Error("No embedding model loaded"); - modelId = models.embedding; - embedder = new LMStudioEmbedder({ - baseURL: LMSTUDIO_BASE_URL, - model: modelId, - }); - }); - - it("embed() returns a numeric vector", async () => { - const vector = await embedder.embed("Hello world"); - expect(Array.isArray(vector)).toBe(true); - expect(vector.length).toBeGreaterThan(0); - vector.forEach((v) => expect(typeof v).toBe("number")); - console.log(` Model: ${modelId}, dimension: ${vector.length}`); - }); - - it("embed() produces identical output for newline-normalized text", async () => { - const v1 = await embedder.embed("hello world"); - const v2 = await embedder.embed("hello\nworld"); - expect(v1.length).toBe(v2.length); - const totalDiff = v1.reduce((s, val, i) => s + Math.abs(val - v2[i]), 0); - expect(totalDiff).toBeLessThan(0.001); - }); - - it("embedBatch() returns correct number of vectors", async () => { - const vectors = await embedder.embedBatch(["first", "second", "third"]); - expect(vectors).toHaveLength(3); - vectors.forEach((v) => { - expect(v.length).toBe(vectors[0].length); - v.forEach((val) => expect(typeof val).toBe("number")); - }); - }); - - it("semantically similar texts have higher cosine similarity", async () => { - const [v1, v2, v3] = await Promise.all([ - embedder.embed("I love hiking in the mountains"), - embedder.embed("I enjoy trekking through mountain trails"), - embedder.embed("The stock market crashed yesterday"), - ]); - const simSimilar = cosineSim(v1, v2); - const simDifferent = cosineSim(v1, v3); - console.log( - ` Similar: ${simSimilar.toFixed(4)}, Different: ${simDifferent.toFixed(4)}`, - ); - expect(Number.isFinite(simSimilar)).toBe(true); - expect(Number.isFinite(simDifferent)).toBe(true); - expect(simSimilar).toBeGreaterThan(simDifferent); - }); - - it("embed() handles empty string", async () => { - const vector = await embedder.embed(""); - expect(Array.isArray(vector)).toBe(true); - expect(vector.length).toBeGreaterThan(0); - }); - - it("embed() handles long text", async () => { - const longText = "This is a test sentence. ".repeat(200); - const vector = await embedder.embed(longText); - expect(Array.isArray(vector)).toBe(true); - expect(vector.length).toBeGreaterThan(0); - }); - }); - - // ─── LLM ───────────────────────────────────────────────────────────── - describe("LMStudioLLM (real server)", () => { - let llm: LMStudioLLM; - let chatModelId: string | null; - - beforeAll(async () => { - const models = await listModels(); - chatModelId = models.chat; - if (!chatModelId) { - console.warn("No chat model loaded — LLM tests will be skipped"); - return; - } - llm = new LMStudioLLM({ baseURL: LMSTUDIO_BASE_URL, model: chatModelId }); - }); - - it("generateResponse() returns a response", async () => { - if (!chatModelId) return; - const result = await llm.generateResponse([ - { role: "user", content: "Say hello in exactly 3 words." }, - ]); - if (typeof result === "string") { - expect(result.length).toBeGreaterThan(0); - console.log(` Response (string): ${result.slice(0, 100)}`); - } else { - expect(result).toHaveProperty("content"); - expect(result.content.length).toBeGreaterThan(0); - console.log(` Response (object): ${result.content.slice(0, 100)}`); - } - }); - - it("generateChat() returns LLMResponse with content and role", async () => { - if (!chatModelId) return; - const result = await llm.generateChat([ - { role: "user", content: "What is 2+2?" }, - ]); - expect(result).toHaveProperty("content"); - expect(result).toHaveProperty("role"); - expect(result.role).toBe("assistant"); - expect(result.content.length).toBeGreaterThan(0); - console.log(` Chat: ${result.content.slice(0, 100)}`); - }); - - it("generateChat() handles multi-turn conversation", async () => { - if (!chatModelId) return; - const result = await llm.generateChat([ - { role: "user", content: "My name is Alice." }, - { role: "assistant", content: "Hello Alice!" }, - { role: "user", content: "What is my name?" }, - ]); - expect(result.content.length).toBeGreaterThan(0); - console.log(` Multi-turn: ${result.content.slice(0, 100)}`); - }); - }); -}); diff --git a/mem0-ts/src/oss/tests/memory.e2e.test.ts b/mem0-ts/src/oss/tests/memory.e2e.test.ts deleted file mode 100644 index 2f0f6cd2d..000000000 --- a/mem0-ts/src/oss/tests/memory.e2e.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -/** - * OSS Memory E2E tests — exercises full add/get/search/update/delete flow with mocked LLM/embedder. - * Skipped by default. Run with: MEM0_RUN_E2E=1 npx jest memory.e2e.test.ts - */ -/// -import { Memory } from "../src/memory"; -import { MemoryItem, SearchResult } from "../src/types"; - -const describeOrSkip = process.env.MEM0_RUN_E2E ? describe : describe.skip; - -jest.setTimeout(30000); - -// Mock LLM and embedder so tests run without API keys. -// Content-based mock: system-prompt calls → facts, user-only calls → memory actions. -jest.mock("../src/embeddings/google", () => ({ GoogleEmbedder: jest.fn() })); -jest.mock("../src/llms/google", () => ({ GoogleLLM: jest.fn() })); - -jest.mock("../src/llms/openai", () => ({ - OpenAILLM: jest.fn().mockImplementation(() => ({ - generateResponse: jest - .fn() - .mockImplementation( - (messages: Array<{ role: string; content: string }>) => { - const hasSystemRole = messages.some((m) => m.role === "system"); - if (hasSystemRole) { - return JSON.stringify({ - facts: ["John is a software engineer"], - }); - } - return JSON.stringify({ - memory: [ - { - id: "new", - event: "ADD", - text: "John is a software engineer", - old_memory: "", - new_memory: "John is a software engineer", - }, - ], - }); - }, - ), - })), -})); - -jest.mock("../src/embeddings/openai", () => ({ - OpenAIEmbedder: jest.fn().mockImplementation(() => ({ - embed: jest.fn().mockResolvedValue(new Array(1536).fill(0.1)), - embeddingDims: 1536, - })), -})); - -describeOrSkip("Memory Class (E2E)", () => { - let memory: Memory; - const userId = - Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15); - - beforeEach(async () => { - memory = new Memory({ - version: "v1.1", - embedder: { - provider: "openai", - config: { apiKey: "test-key", model: "text-embedding-3-small" }, - }, - vectorStore: { - provider: "memory", - config: { collectionName: "test-memories", dimension: 1536 }, - }, - llm: { - provider: "openai", - config: { apiKey: "test-key", model: "gpt-4-turbo-preview" }, - }, - historyDbPath: ":memory:", - }); - await memory.reset(); - }); - - afterEach(async () => { - await memory.reset(); - }); - - describe("add() single memory", () => { - let result: SearchResult; - - beforeEach(async () => { - result = (await memory.add( - "Hi, my name is John and I am a software engineer.", - { userId }, - )) as SearchResult; - }); - - it("returns a defined result", () => { - expect(result).toBeDefined(); - }); - - it("returns results array", () => { - expect(Array.isArray(result.results)).toBe(true); - }); - - it("returns at least one result", () => { - expect(result.results.length).toBeGreaterThan(0); - }); - - it("returns result with an id", () => { - expect(result.results[0]?.id).toBeDefined(); - }); - }); - - describe("add() multiple messages", () => { - let result: SearchResult; - - beforeEach(async () => { - const messages = [ - { role: "user", content: "What is your favorite city?" }, - { role: "assistant", content: "I love Paris, it is my favorite city." }, - ]; - result = (await memory.add(messages, { userId })) as SearchResult; - }); - - it("returns results array", () => { - expect(Array.isArray(result.results)).toBe(true); - }); - - it("returns at least one result", () => { - expect(result.results.length).toBeGreaterThan(0); - }); - }); - - describe("get() single memory", () => { - let memoryItem: MemoryItem; - let memoryId: string; - - beforeEach(async () => { - const addResult = (await memory.add( - "I am a big advocate of using AI to make the world a better place", - { userId }, - )) as SearchResult; - memoryId = addResult.results[0].id; - memoryItem = (await memory.get(memoryId)) as MemoryItem; - }); - - it("returns the correct id", () => { - expect(memoryItem.id).toBe(memoryId); - }); - - it("returns a string memory", () => { - expect(typeof memoryItem.memory).toBe("string"); - }); - }); - - describe("update() memory", () => { - let memoryId: string; - - beforeEach(async () => { - const addResult = (await memory.add( - "I love speaking foreign languages especially Spanish", - { userId }, - )) as SearchResult; - memoryId = addResult.results[0].id; - }); - - it("returns success message", async () => { - const result = await memory.update(memoryId, "Updated content"); - expect(result.message).toBe("Memory updated successfully!"); - }); - - it("persists the updated content", async () => { - await memory.update(memoryId, "Updated content"); - const updated = (await memory.get(memoryId)) as MemoryItem; - expect(updated.memory).toBe("Updated content"); - }); - }); - - describe("getAll() memories for user", () => { - let result: SearchResult; - - beforeEach(async () => { - await memory.add("I love visiting new places in the winters", { userId }); - await memory.add("I like to rule the world", { userId }); - result = (await memory.getAll({ userId })) as SearchResult; - }); - - it("returns results array", () => { - expect(Array.isArray(result.results)).toBe(true); - }); - - it("returns at least two results", () => { - expect(result.results.length).toBeGreaterThanOrEqual(2); - }); - }); - - describe("search() memories", () => { - let result: SearchResult; - - beforeEach(async () => { - await memory.add("I love programming in Python", { userId }); - await memory.add("JavaScript is my favorite language", { userId }); - result = (await memory.search("What programming languages do I know?", { - userId, - })) as SearchResult; - }); - - it("returns results array", () => { - expect(Array.isArray(result.results)).toBe(true); - }); - - it("returns at least one result", () => { - expect(result.results.length).toBeGreaterThan(0); - }); - }); - - describe("history() of a memory", () => { - let history: unknown[]; - - beforeEach(async () => { - const addResult = (await memory.add("I like swimming in warm water", { - userId, - })) as SearchResult; - const memoryId = addResult.results[0].id; - await memory.update(memoryId, "Updated content"); - history = await memory.history(memoryId); - }); - - it("returns an array", () => { - expect(Array.isArray(history)).toBe(true); - }); - - it("returns at least one entry", () => { - expect(history.length).toBeGreaterThan(0); - }); - }); - - describe("delete() a memory", () => { - it("returns null after deletion", async () => { - const addResult = (await memory.add("I love to drink vodka in summers", { - userId, - })) as SearchResult; - const memoryId = addResult.results[0].id; - await memory.delete(memoryId); - const result = await memory.get(memoryId); - expect(result).toBeNull(); - }); - }); - - describe("Memory with Custom Configuration", () => { - let customMemory: Memory; - - beforeEach(() => { - customMemory = new Memory({ - version: "v1.1", - embedder: { - provider: "openai", - config: { apiKey: "test-key", model: "text-embedding-3-small" }, - }, - vectorStore: { - provider: "memory", - config: { collectionName: "test-memories", dimension: 1536 }, - }, - llm: { - provider: "openai", - config: { apiKey: "test-key", model: "gpt-4-turbo-preview" }, - }, - historyDbPath: ":memory:", - }); - }); - - afterEach(async () => { - await customMemory.reset(); - }); - - it("add() returns results with custom config", async () => { - const result = (await customMemory.add("I love programming in Python", { - userId, - })) as SearchResult; - expect(result.results.length).toBeGreaterThan(0); - }); - - it("search() returns results with custom config", async () => { - await customMemory.add("The weather in London is rainy today", { - userId, - }); - await customMemory.add("The temperature in Paris is 25 degrees", { - userId, - }); - const result = (await customMemory.search("What is the weather like?", { - userId, - })) as SearchResult; - expect(result.results.length).toBeGreaterThan(0); - }); - }); -}); diff --git a/mem0-ts/src/oss/tests/qdrant-e2e.test.ts b/mem0-ts/src/oss/tests/qdrant-e2e.test.ts deleted file mode 100644 index 40232232b..000000000 --- a/mem0-ts/src/oss/tests/qdrant-e2e.test.ts +++ /dev/null @@ -1,531 +0,0 @@ -/// -/** - * End-to-end tests for Qdrant dimension mismatch fix. - * - * Requires a running Qdrant instance at localhost:6333 (v1.13.x). - * These tests replicate the exact scenarios from issues #4212, #4173, #4056. - * - * Skipped automatically when Qdrant is not available. - * - * Run: npx jest --config jest.config.js src/oss/tests/qdrant-e2e.test.ts --forceExit - */ - -import { QdrantClient } from "@qdrant/js-client-rest"; -import { Qdrant } from "../src/vector_stores/qdrant"; -import { v4 as uuidv4 } from "uuid"; -jest.setTimeout(30000); - -const QDRANT_HOST = "localhost"; -const QDRANT_PORT = 6333; - -// Check if Qdrant is reachable synchronously at load time using -// a sync check via child_process so describe.skip works correctly. -function isQdrantAvailable(): boolean { - try { - const { execSync } = require("child_process"); - execSync( - `node -e "const s=require('net').createConnection({host:'${QDRANT_HOST}',port:${QDRANT_PORT}});s.on('connect',()=>{s.destroy();process.exit(0)});s.on('error',()=>process.exit(1));s.setTimeout(2000,()=>process.exit(1))"`, - { timeout: 3000, stdio: "ignore" }, - ); - return true; - } catch { - return false; - } -} - -const qdrantAvailable = isQdrantAvailable(); -if (!qdrantAvailable) { - console.warn("Qdrant not available at localhost:6333 — skipping e2e tests"); -} - -let qdrantClient: QdrantClient; - -beforeAll(async () => { - if (!qdrantAvailable) return; - qdrantClient = new QdrantClient({ host: QDRANT_HOST, port: QDRANT_PORT }); - const collections = await qdrantClient.getCollections(); - expect(collections).toBeDefined(); -}); - -// Helper: delete a collection if it exists -async function deleteCollectionIfExists(name: string) { - try { - await qdrantClient.deleteCollection(name); - } catch { - // Collection doesn't exist — fine - } -} - -// Helper: create a fake embedder that produces vectors of a given dimension -function createFakeEmbedder(dims: number) { - return { - embed: jest.fn().mockImplementation(async (_text: string) => { - const vec = new Array(dims).fill(0); - for (let i = 0; i < _text.length && i < dims; i++) { - vec[i] = _text.charCodeAt(i) / 255; - } - return vec; - }), - embedBatch: jest.fn().mockImplementation(async (texts: string[]) => { - return Promise.all( - texts.map(async (t) => { - const vec = new Array(dims).fill(0); - for (let i = 0; i < t.length && i < dims; i++) { - vec[i] = t.charCodeAt(i) / 255; - } - return vec; - }), - ); - }), - }; -} - -// Conditionally skip tests when Qdrant is unavailable -const describeIfQdrant = qdrantAvailable ? describe : describe.skip; - -afterAll(async () => { - await deleteCollectionIfExists("e2e_test_768"); - await deleteCollectionIfExists("e2e_test_1536"); - await deleteCollectionIfExists("e2e_test_race"); - await deleteCollectionIfExists("e2e_test_race2"); - await deleteCollectionIfExists("e2e_test_noexplicit"); - await deleteCollectionIfExists("e2e_test_explicit"); - await deleteCollectionIfExists("e2e_test_embdims"); - await deleteCollectionIfExists("e2e_test_autodetect"); - await deleteCollectionIfExists("memory_migrations"); -}); - -// ─────────────────────────────────────────────────────────────────────────── -// 1. Reproduce #4212 / #4173: dimension mismatch with 768-dim embedder -// ─────────────────────────────────────────────────────────────────────────── -describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => { - it("BEFORE FIX scenario: 768-dim vector into 1536-dim collection → Bad Request", async () => { - const collectionName = "e2e_test_1536"; - await deleteCollectionIfExists(collectionName); - await qdrantClient.createCollection(collectionName, { - vectors: { size: 1536, distance: "Cosine" }, - }); - - // Insert a 768-dim vector — this is what nomic-embed-text produces - const vector768 = new Array(768).fill(0.1); - try { - await qdrantClient.upsert(collectionName, { - points: [ - { id: "test-1", vector: vector768, payload: { data: "hello" } }, - ], - }); - fail("Expected Qdrant to reject 768-dim vector into 1536-dim collection"); - } catch (error: any) { - // This is the exact "Bad Request" error users were hitting - expect(error.status).toBe(400); - } - - await deleteCollectionIfExists(collectionName); - }); - - it("AFTER FIX: Qdrant store with dimension=768 works end-to-end", async () => { - const collectionName = "e2e_test_768"; - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - - // Create Qdrant store with correct dimension (what our auto-detect provides) - const store = new Qdrant({ - host: QDRANT_HOST, - port: QDRANT_PORT, - collectionName, - embeddingModelDims: 768, - dimension: 768, - }); - await store.initialize(); - - // Verify collection was created with 768 dims - const info = await qdrantClient.getCollection(collectionName); - expect(info.config?.params?.vectors?.size).toBe(768); - - // Insert 768-dim vectors (what nomic-embed-text produces) - const vec1 = new Array(768).fill(0); - vec1[0] = 1.0; - const vec2 = new Array(768).fill(0); - vec2[1] = 1.0; - - const id1 = uuidv4(); - const id2 = uuidv4(); - - await store.insert( - [vec1, vec2], - [id1, id2], - [ - { data: "hello", userId: "u1" }, - { data: "world", userId: "u1" }, - ], - ); - - // Search with 768-dim query — this USED TO fail with Bad Request - const results = await store.search(vec1, 2, { userId: "u1" }); - expect(results.length).toBe(2); - expect(results[0].id).toBe(id1); // Most similar to itself - expect(results[0].score).toBeGreaterThan(0.9); - - // Get by ID - const item = await store.get(id1); - expect(item).not.toBeNull(); - expect(item!.payload.data).toBe("hello"); - - // Update with 768-dim vector - const vec3 = new Array(768).fill(0); - vec3[2] = 1.0; - await store.update(id1, vec3, { data: "updated", userId: "u1" }); - const updated = await store.get(id1); - expect(updated!.payload.data).toBe("updated"); - - // Delete - await store.delete(id2); - const deleted = await store.get(id2); - expect(deleted).toBeNull(); - - // List - const [listed, count] = await store.list({ userId: "u1" }); - expect(count).toBe(1); - expect(listed[0].payload.data).toBe("updated"); - - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - }); - - it("AFTER FIX: Memory auto-detects 768 dims via probe (full integration)", async () => { - const collectionName = "e2e_test_autodetect"; - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - - const fakeEmbedder = createFakeEmbedder(768); - - // Mock only the non-Qdrant factories to avoid Google SDK import crash - jest.resetModules(); - jest.doMock("../src/utils/factory", () => { - // Import Qdrant directly (avoids loading Google embedder via factory) - const { Qdrant: QdrantStore } = require("../src/vector_stores/qdrant"); - return { - EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) }, - VectorStoreFactory: { - create: jest - .fn() - .mockImplementation((_provider: string, config: any) => { - return new QdrantStore(config); - }), - }, - LLMFactory: { - create: jest.fn().mockReturnValue({ - generateResponse: jest.fn().mockResolvedValue('{"facts":[]}'), - }), - }, - HistoryManagerFactory: { - create: jest.fn().mockReturnValue({ - addHistory: jest.fn().mockResolvedValue(undefined), - getHistory: jest.fn().mockResolvedValue([]), - reset: jest.fn().mockResolvedValue(undefined), - }), - }, - }; - }); - - jest.doMock("../src/utils/telemetry", () => ({ - captureClientEvent: jest.fn().mockResolvedValue(undefined), - })); - - const { Memory } = require("../src/memory"); - - // This is the EXACT config from issue #4212 — NO dimension specified - const mem = new Memory({ - embedder: { - provider: "ollama", - config: { model: "nomic-embed-text" }, - }, - vectorStore: { - provider: "qdrant", - config: { - host: QDRANT_HOST, - port: QDRANT_PORT, - collectionName, - }, - }, - llm: { provider: "openai", config: { apiKey: "fake" } }, - disableHistory: true, - }); - - // This triggers init — probe should detect 768 dims - await mem.getAll({ userId: "test-user" }); - - // Verify the probe was called - expect(fakeEmbedder.embed).toHaveBeenCalledWith("dimension probe"); - - // Verify Qdrant collection was created with auto-detected 768 dims - const collectionInfo = await qdrantClient.getCollection(collectionName); - expect(collectionInfo.config?.params?.vectors?.size).toBe(768); - - // Search should work (this used to throw Bad Request) - const searchResult = await mem.search("hello world", { - userId: "test-user", - }); - expect(searchResult).toBeDefined(); - - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - jest.resetModules(); - }); - - it("AFTER FIX: explicit dimension=768 skips probe (backward compat)", async () => { - const collectionName = "e2e_test_explicit"; - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - - const fakeEmbedder = createFakeEmbedder(768); - - jest.resetModules(); - jest.doMock("../src/utils/factory", () => { - const { Qdrant: QdrantStore } = require("../src/vector_stores/qdrant"); - return { - EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) }, - VectorStoreFactory: { - create: jest - .fn() - .mockImplementation((_provider: string, config: any) => { - return new QdrantStore(config); - }), - }, - LLMFactory: { - create: jest.fn().mockReturnValue({ - generateResponse: jest.fn().mockResolvedValue('{"facts":[]}'), - }), - }, - HistoryManagerFactory: { - create: jest.fn().mockReturnValue({ - addHistory: jest.fn().mockResolvedValue(undefined), - getHistory: jest.fn().mockResolvedValue([]), - reset: jest.fn().mockResolvedValue(undefined), - }), - }, - }; - }); - - jest.doMock("../src/utils/telemetry", () => ({ - captureClientEvent: jest.fn().mockResolvedValue(undefined), - })); - - const { Memory } = require("../src/memory"); - - // Workaround config from #4212 — explicit dimension - const mem = new Memory({ - embedder: { - provider: "ollama", - config: { model: "nomic-embed-text" }, - }, - vectorStore: { - provider: "qdrant", - config: { - host: QDRANT_HOST, - port: QDRANT_PORT, - collectionName, - dimension: 768, - }, - }, - llm: { provider: "openai", config: { apiKey: "fake" } }, - disableHistory: true, - }); - - await mem.getAll({ userId: "test-user" }); - - // Probe should NOT have been called - expect(fakeEmbedder.embed).not.toHaveBeenCalledWith("dimension probe"); - - const collectionInfo = await qdrantClient.getCollection(collectionName); - expect(collectionInfo.config?.params?.vectors?.size).toBe(768); - - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - jest.resetModules(); - }); - - it("AFTER FIX: embeddingDims in embedder config skips probe", async () => { - const collectionName = "e2e_test_embdims"; - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - - const fakeEmbedder = createFakeEmbedder(768); - - jest.resetModules(); - jest.doMock("../src/utils/factory", () => { - const { Qdrant: QdrantStore } = require("../src/vector_stores/qdrant"); - return { - EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) }, - VectorStoreFactory: { - create: jest - .fn() - .mockImplementation((_provider: string, config: any) => { - return new QdrantStore(config); - }), - }, - LLMFactory: { - create: jest.fn().mockReturnValue({ - generateResponse: jest.fn().mockResolvedValue('{"facts":[]}'), - }), - }, - HistoryManagerFactory: { - create: jest.fn().mockReturnValue({ - addHistory: jest.fn().mockResolvedValue(undefined), - getHistory: jest.fn().mockResolvedValue([]), - reset: jest.fn().mockResolvedValue(undefined), - }), - }, - }; - }); - - jest.doMock("../src/utils/telemetry", () => ({ - captureClientEvent: jest.fn().mockResolvedValue(undefined), - })); - - const { Memory } = require("../src/memory"); - - const mem = new Memory({ - embedder: { - provider: "ollama", - config: { model: "nomic-embed-text", embeddingDims: 768 }, - }, - vectorStore: { - provider: "qdrant", - config: { - host: QDRANT_HOST, - port: QDRANT_PORT, - collectionName, - }, - }, - llm: { provider: "openai", config: { apiKey: "fake" } }, - disableHistory: true, - }); - - await mem.getAll({ userId: "test-user" }); - - // Probe should NOT have been called — dimension inferred from embeddingDims - expect(fakeEmbedder.embed).not.toHaveBeenCalledWith("dimension probe"); - - const collectionInfo = await qdrantClient.getCollection(collectionName); - expect(collectionInfo.config?.params?.vectors?.size).toBe(768); - - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - jest.resetModules(); - }); -}); - -// ─────────────────────────────────────────────────────────────────────────── -// 2. Reproduce #4056 issue 1: Collection creation race condition -// ─────────────────────────────────────────────────────────────────────────── -describeIfQdrant("Issue #4056: Qdrant race condition", () => { - it("concurrent ensureCollection calls don't crash (no 409 error leak)", async () => { - const collectionName = "e2e_test_race"; - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - - // Create 5 Qdrant instances concurrently — this simulates the race - // that caused "Collection memory_migrations already exists!" in #4056 - const instances = Array.from( - { length: 5 }, - () => - new Qdrant({ - host: QDRANT_HOST, - port: QDRANT_PORT, - collectionName, - embeddingModelDims: 768, - dimension: 768, - }), - ); - - // All should initialize without throwing 409 Conflict - await Promise.all(instances.map((inst) => inst.initialize())); - - // Verify collection exists with correct dimension - const info = await qdrantClient.getCollection(collectionName); - expect(info.config?.params?.vectors?.size).toBe(768); - - // memory_migrations should also exist (created by initialize) - const migrInfo = await qdrantClient.getCollection("memory_migrations"); - expect(migrInfo.config?.params?.vectors?.size).toBe(1); - - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - }); - - it("getUserId works after concurrent initialization", async () => { - const collectionName = "e2e_test_race2"; - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - - const instance = new Qdrant({ - host: QDRANT_HOST, - port: QDRANT_PORT, - collectionName, - embeddingModelDims: 768, - dimension: 768, - }); - - await instance.initialize(); - - // getUserId should work without 409 crash - const userId = await instance.getUserId(); - expect(typeof userId).toBe("string"); - expect(userId.length).toBeGreaterThan(0); - - // setUserId + getUserId roundtrip - await instance.setUserId("custom-e2e-user"); - const updated = await instance.getUserId(); - expect(updated).toBe("custom-e2e-user"); - - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - }); -}); - -// ─────────────────────────────────────────────────────────────────────────── -// 3. Reproduce #4056 issue 2: memory_migrations dimension isolation -// ─────────────────────────────────────────────────────────────────────────── -describeIfQdrant("Issue #4056: memory_migrations dimension isolation", () => { - it("memory_migrations uses dim=1 independently of main collection dim=768", async () => { - const collectionName = "e2e_test_noexplicit"; - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - - const instance = new Qdrant({ - host: QDRANT_HOST, - port: QDRANT_PORT, - collectionName, - embeddingModelDims: 768, - dimension: 768, - }); - - await instance.initialize(); - - // Allow Qdrant a moment to fully commit collections - await new Promise((r) => setTimeout(r, 500)); - - // Main collection should be 768 - const mainInfo = await qdrantClient.getCollection(collectionName); - expect(mainInfo.config?.params?.vectors?.size).toBe(768); - - // memory_migrations should be 1 (NOT 768!) - // This was the bug in #4056 issue 2 — telemetry used wrong dimension - const migrationsInfo = - await qdrantClient.getCollection("memory_migrations"); - expect(migrationsInfo.config?.params?.vectors?.size).toBe(1); - - // getUserId should work — vector dim=1 in memory_migrations - const userId = await instance.getUserId(); - expect(typeof userId).toBe("string"); - - // setUserId should also work - await instance.setUserId("custom-test-user"); - const newUserId = await instance.getUserId(); - expect(newUserId).toBe("custom-test-user"); - - await deleteCollectionIfExists(collectionName); - await deleteCollectionIfExists("memory_migrations"); - }); -}); diff --git a/mem0-ts/src/oss/tests/redis-e2e.test.ts b/mem0-ts/src/oss/tests/redis-e2e.test.ts deleted file mode 100644 index 08afdfd85..000000000 --- a/mem0-ts/src/oss/tests/redis-e2e.test.ts +++ /dev/null @@ -1,427 +0,0 @@ -/// -/** - * End-to-end tests for Redis vector store with init guard fix. - * - * Requires a running Redis Stack instance at localhost:6379. - * Skipped automatically when Redis is not available. - * - * Run: npx jest --config jest.config.js src/oss/tests/redis-e2e.test.ts --forceExit - */ - -import { createClient } from "redis"; -import { RedisDB } from "../src/vector_stores/redis"; -import { v4 as uuidv4 } from "uuid"; -jest.setTimeout(30000); - -const REDIS_HOST = "localhost"; -const REDIS_PORT = 6379; -const REDIS_URL = `redis://${REDIS_HOST}:${REDIS_PORT}`; -const COLLECTION_NAME = "e2e_redis_test"; - -// Check if Redis is reachable synchronously at load time -function isRedisAvailable(): boolean { - try { - const { execSync } = require("child_process"); - execSync( - `node -e "const s=require('net').createConnection({host:'${REDIS_HOST}',port:${REDIS_PORT}});s.on('connect',()=>{s.destroy();process.exit(0)});s.on('error',()=>process.exit(1));s.setTimeout(2000,()=>process.exit(1))"`, - { timeout: 3000, stdio: "ignore" }, - ); - return true; - } catch { - return false; - } -} - -const redisAvailable = isRedisAvailable(); -if (!redisAvailable) { - console.warn("Redis not available at localhost:6379 — skipping e2e tests"); -} - -// Standalone client for cleanup -let cleanupClient: ReturnType; - -async function cleanupRedis() { - if (!redisAvailable) return; - try { - // Drop the index if it exists - await cleanupClient.ft.dropIndex(COLLECTION_NAME); - } catch { - // Index doesn't exist — fine - } - - // Delete all keys with our prefix - const keys = await cleanupClient.keys(`mem0:${COLLECTION_NAME}:*`); - if (keys.length > 0) { - await cleanupClient.del(keys); - } - - // Clean up memory_migrations key - await cleanupClient.del("memory_migrations:1"); -} - -beforeAll(async () => { - if (!redisAvailable) return; - - cleanupClient = createClient({ url: REDIS_URL }); - await cleanupClient.connect(); - - // Verify Redis Stack is running with search module - const modules = (await cleanupClient.moduleList()) as unknown as any[]; - const hasSearch = modules.some((mod: any[]) => { - const moduleMap = new Map(); - for (let i = 0; i < mod.length; i += 2) { - moduleMap.set(mod[i], mod[i + 1]); - } - return moduleMap.get("name")?.toLowerCase() === "search"; - }); - expect(hasSearch).toBe(true); -}); - -afterAll(async () => { - if (!redisAvailable) return; - await cleanupRedis(); - await cleanupClient.quit(); -}); - -// Conditionally skip tests when Redis is unavailable -const describeIfRedis = redisAvailable ? describe : describe.skip; - -// ─────────────────────────────────────────────────────────────────────────── -// 1. Basic initialization and idempotent init guard -// ─────────────────────────────────────────────────────────────────────────── -describeIfRedis("Redis: initialization", () => { - afterEach(async () => { - await cleanupRedis(); - }); - - it("initializes successfully and creates index", async () => { - const store = new RedisDB({ - redisUrl: REDIS_URL, - collectionName: COLLECTION_NAME, - embeddingModelDims: 128, - }); - await store.initialize(); - - // Verify the index was created by querying index info - const info = await cleanupClient.ft.info(COLLECTION_NAME); - expect(info).toBeDefined(); - expect(info.indexName).toBe(COLLECTION_NAME); - - await store.close(); - }); - - it("idempotent initialize() — multiple calls don't crash", async () => { - const store = new RedisDB({ - redisUrl: REDIS_URL, - collectionName: COLLECTION_NAME, - embeddingModelDims: 128, - }); - - // Call initialize multiple times concurrently - await Promise.all([ - store.initialize(), - store.initialize(), - store.initialize(), - ]); - - // Should still work fine - const info = await cleanupClient.ft.info(COLLECTION_NAME); - expect(info).toBeDefined(); - - await store.close(); - }); -}); - -// ─────────────────────────────────────────────────────────────────────────── -// 2. Full CRUD operations -// ─────────────────────────────────────────────────────────────────────────── -describeIfRedis("Redis: CRUD operations", () => { - let store: RedisDB; - - beforeEach(async () => { - await cleanupRedis(); - store = new RedisDB({ - redisUrl: REDIS_URL, - collectionName: COLLECTION_NAME, - embeddingModelDims: 4, // Small dims for testing - }); - await store.initialize(); - }); - - afterEach(async () => { - await store.close(); - await cleanupRedis(); - }); - - it("insert and search vectors", async () => { - const id1 = uuidv4(); - const id2 = uuidv4(); - const vec1 = [1.0, 0.0, 0.0, 0.0]; - const vec2 = [0.0, 1.0, 0.0, 0.0]; - - await store.insert( - [vec1, vec2], - [id1, id2], - [ - { - data: "hello world", - hash: "h1", - userId: "user1", - createdAt: new Date().toISOString(), - }, - { - data: "goodbye world", - hash: "h2", - userId: "user1", - createdAt: new Date().toISOString(), - }, - ], - ); - - // Search — vec1 should be most similar to itself - const results = await store.search(vec1, 2, { userId: "user1" }); - expect(results.length).toBe(2); - // The first result should be closest to the query - expect(results[0].id).toBe(id1); - expect(results[0].score).toBeDefined(); - expect(results[0].payload).toBeDefined(); - }); - - it("get vector by ID", async () => { - const id = uuidv4(); - const vec = [0.5, 0.5, 0.0, 0.0]; - - await store.insert( - [vec], - [id], - [ - { - data: "test memory", - hash: "h-test", - userId: "user1", - createdAt: new Date().toISOString(), - }, - ], - ); - - const result = await store.get(id); - expect(result).not.toBeNull(); - expect(result!.id).toBe(id); - expect(result!.payload.data).toBe("test memory"); - expect(result!.payload.hash).toBe("h-test"); - }); - - it("get non-existent vector returns null", async () => { - const result = await store.get("non-existent-id"); - expect(result).toBeNull(); - }); - - it("update vector and payload", async () => { - const id = uuidv4(); - const vec = [1.0, 0.0, 0.0, 0.0]; - - await store.insert( - [vec], - [id], - [ - { - data: "original", - hash: "h-orig", - userId: "user1", - createdAt: new Date().toISOString(), - }, - ], - ); - - // Update with new vector and payload - const newVec = [0.0, 0.0, 1.0, 0.0]; - await store.update(id, newVec, { - data: "updated memory", - hash: "h-updated", - userId: "user1", - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }); - - const result = await store.get(id); - expect(result).not.toBeNull(); - expect(result!.payload.data).toBe("updated memory"); - expect(result!.payload.hash).toBe("h-updated"); - }); - - it("delete vector", async () => { - const id = uuidv4(); - const vec = [0.0, 0.0, 0.0, 1.0]; - - await store.insert( - [vec], - [id], - [ - { - data: "to be deleted", - hash: "h-del", - userId: "user1", - createdAt: new Date().toISOString(), - }, - ], - ); - - // Verify it exists - const before = await store.get(id); - expect(before).not.toBeNull(); - - // Delete - await store.delete(id); - - // Verify it's gone - const after = await store.get(id); - expect(after).toBeNull(); - }); - - it("list vectors with filters", async () => { - const id1 = uuidv4(); - const id2 = uuidv4(); - const id3 = uuidv4(); - - await store.insert( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - ], - [id1, id2, id3], - [ - { - data: "mem1", - hash: "h1", - userId: "usera", - createdAt: new Date().toISOString(), - }, - { - data: "mem2", - hash: "h2", - userId: "usera", - createdAt: new Date().toISOString(), - }, - { - data: "mem3", - hash: "h3", - userId: "userb", - createdAt: new Date().toISOString(), - }, - ], - ); - - // List all - const [all, allCount] = await store.list(); - expect(allCount).toBe(3); - expect(all.length).toBe(3); - - // List with filter - const [filtered, filteredCount] = await store.list({ - userId: "usera", - }); - expect(filteredCount).toBe(2); - expect(filtered.length).toBe(2); - }); -}); - -// ─────────────────────────────────────────────────────────────────────────── -// 3. getUserId / setUserId -// ─────────────────────────────────────────────────────────────────────────── -describeIfRedis("Redis: getUserId / setUserId", () => { - let store: RedisDB; - - beforeEach(async () => { - await cleanupRedis(); - store = new RedisDB({ - redisUrl: REDIS_URL, - collectionName: COLLECTION_NAME, - embeddingModelDims: 4, - }); - await store.initialize(); - }); - - afterEach(async () => { - await store.close(); - await cleanupRedis(); - }); - - it("getUserId generates random ID if none exists", async () => { - const userId = await store.getUserId(); - expect(typeof userId).toBe("string"); - expect(userId.length).toBeGreaterThan(0); - }); - - it("setUserId + getUserId roundtrip", async () => { - await store.setUserId("custom-redis-user"); - const retrieved = await store.getUserId(); - expect(retrieved).toBe("custom-redis-user"); - }); - - it("getUserId returns same value on subsequent calls", async () => { - const first = await store.getUserId(); - const second = await store.getUserId(); - expect(first).toBe(second); - }); -}); - -// ─────────────────────────────────────────────────────────────────────────── -// 4. Dimension handling (our fix ensures correct dims from Memory) -// ─────────────────────────────────────────────────────────────────────────── -describeIfRedis("Redis: dimension handling", () => { - afterEach(async () => { - await cleanupRedis(); - }); - - it("creates index with correct dimensions from config", async () => { - const store = new RedisDB({ - redisUrl: REDIS_URL, - collectionName: COLLECTION_NAME, - embeddingModelDims: 768, - }); - await store.initialize(); - - // Verify the index has the right dimension in its schema - const info = await cleanupClient.ft.info(COLLECTION_NAME); - // Check that the vector field has DIM=768 - const attributes = info.attributes as any[]; - const vectorAttr = attributes.find( - (a: any) => a.identifier === "embedding" || a.attribute === "embedding", - ); - expect(vectorAttr).toBeDefined(); - - await store.close(); - }); - - it("insert with matching dimension succeeds", async () => { - const dims = 128; - const store = new RedisDB({ - redisUrl: REDIS_URL, - collectionName: COLLECTION_NAME, - embeddingModelDims: dims, - }); - await store.initialize(); - - const id = uuidv4(); - const vec = new Array(dims).fill(0.1); - - await store.insert( - [vec], - [id], - [ - { - data: "test", - hash: "h1", - createdAt: new Date().toISOString(), - }, - ], - ); - - const result = await store.get(id); - expect(result).not.toBeNull(); - expect(result!.id).toBe(id); - - await store.close(); - }); -});