/// /** * Tests for embedding dimension auto-detection. * * Covers: * - ConfigManager: dimension resolution logic * - Memory class: probe-based auto-detection, lazy init gate, backward compat * - MemoryVectorStore: backward compat with explicit dimensions * - Explicit error messages on probe failure */ import { ConfigManager } from "../src/config/manager"; import { MemoryVectorStore } from "../src/vector_stores/memory"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; jest.setTimeout(15000); // ─────────────────────────────────────────────────────────────────────────── // 1. ConfigManager – dimension resolution // ─────────────────────────────────────────────────────────────────────────── describe("ConfigManager – dimension resolution", () => { const baseLlm = { provider: "openai", config: { apiKey: "k" } }; it("leaves dimension undefined when nothing explicit is set", () => { const cfg = ConfigManager.mergeConfig({ embedder: { provider: "openai", config: { apiKey: "k" } }, vectorStore: { provider: "memory", config: { collectionName: "t" } }, llm: baseLlm, }); expect(cfg.vectorStore.config.dimension).toBeUndefined(); }); it("uses embeddingDims from embedder config", () => { const cfg = ConfigManager.mergeConfig({ embedder: { provider: "ollama", config: { model: "nomic-embed-text", embeddingDims: 768 }, }, vectorStore: { provider: "qdrant", config: { collectionName: "t" } }, llm: baseLlm, }); expect(cfg.vectorStore.config.dimension).toBe(768); }); it("prefers explicit vectorStore.dimension over embeddingDims", () => { const cfg = ConfigManager.mergeConfig({ embedder: { provider: "ollama", config: { model: "nomic-embed-text", embeddingDims: 768 }, }, vectorStore: { provider: "qdrant", config: { collectionName: "t", dimension: 1024 }, }, llm: baseLlm, }); expect(cfg.vectorStore.config.dimension).toBe(1024); }); it("leaves dimension undefined for custom client without explicit dims", () => { const cfg = ConfigManager.mergeConfig({ embedder: { provider: "ollama", config: { model: "nomic-embed-text" } }, vectorStore: { provider: "qdrant", config: { collectionName: "t", client: {} }, }, llm: baseLlm, }); expect(cfg.vectorStore.config.dimension).toBeUndefined(); }); it("uses embeddingDims with a custom client", () => { const cfg = ConfigManager.mergeConfig({ embedder: { provider: "ollama", config: { model: "nomic-embed-text", embeddingDims: 768 }, }, vectorStore: { provider: "qdrant", config: { collectionName: "t", client: {} }, }, llm: baseLlm, }); expect(cfg.vectorStore.config.dimension).toBe(768); }); it("preserves all other vectorStore config fields", () => { const cfg = ConfigManager.mergeConfig({ embedder: { provider: "openai", config: { apiKey: "k" } }, vectorStore: { provider: "qdrant", config: { collectionName: "my-coll", host: "my-host", port: 6333, apiKey: "qdrant-key", }, }, llm: baseLlm, }); expect(cfg.vectorStore.config.collectionName).toBe("my-coll"); expect(cfg.vectorStore.config.host).toBe("my-host"); expect(cfg.vectorStore.config.port).toBe(6333); expect(cfg.vectorStore.config.apiKey).toBe("qdrant-key"); }); it("leaves dimension undefined with empty config", () => { const cfg = ConfigManager.mergeConfig({ embedder: { provider: "openai", config: {} }, vectorStore: { provider: "memory", config: {} }, llm: baseLlm, }); expect(cfg.vectorStore.config.dimension).toBeUndefined(); }); }); // ─────────────────────────────────────────────────────────────────────────── // 2. MemoryVectorStore – backward compat with explicit dimensions // ─────────────────────────────────────────────────────────────────────────── describe("MemoryVectorStore – backward compat", () => { let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-")); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it("defaults to dimension 1536 when not specified", async () => { const store = new MemoryVectorStore({ collectionName: "test", dbPath: path.join(tmpDir, "vs.db"), }); const vector = new Array(1536).fill(0.1); await store.insert([vector], ["id-1"], [{ data: "hello" }]); const result = await store.get("id-1"); expect(result).not.toBeNull(); }); it("explicit dimension=1536 still works", async () => { const store = new MemoryVectorStore({ collectionName: "test", dimension: 1536, dbPath: path.join(tmpDir, "vs.db"), }); const vector = new Array(1536).fill(0.1); await store.insert([vector], ["id-1"], [{ data: "hello" }]); const result = await store.get("id-1"); expect(result).not.toBeNull(); }); it("explicit dimension rejects mismatched vectors", async () => { const store = new MemoryVectorStore({ collectionName: "test", dimension: 1536, dbPath: path.join(tmpDir, "vs.db"), }); const wrongVector = new Array(768).fill(0.1); await expect( store.insert([wrongVector], ["id-1"], [{ data: "hello" }]), ).rejects.toThrow("Vector dimension mismatch"); }); it("search validates dimension", async () => { const store = new MemoryVectorStore({ collectionName: "test", dimension: 4, dbPath: path.join(tmpDir, "vs.db"), }); await expect(store.search([1, 2, 3], 1)).rejects.toThrow( "Query dimension mismatch", ); }); it("custom dimension=768 works end-to-end", async () => { const store = new MemoryVectorStore({ collectionName: "test", dimension: 768, dbPath: path.join(tmpDir, "vs.db"), }); await store.insert( [ [1, ...new Array(767).fill(0)], [0, 1, ...new Array(766).fill(0)], ], ["a", "b"], [{ data: "alpha" }, { data: "beta" }], ); const results = await store.search([1, ...new Array(767).fill(0)], 2); expect(results.length).toBe(2); expect(results[0].id).toBe("a"); }); it("getUserId and setUserId still work", async () => { const store = new MemoryVectorStore({ collectionName: "test", dbPath: path.join(tmpDir, "vs.db"), }); const userId = await store.getUserId(); expect(typeof userId).toBe("string"); expect(userId.length).toBeGreaterThan(0); await store.setUserId("custom-user"); const newUserId = await store.getUserId(); expect(newUserId).toBe("custom-user"); }); it("initialize() is idempotent", async () => { const store = new MemoryVectorStore({ collectionName: "test", dbPath: path.join(tmpDir, "vs.db"), }); await store.initialize(); await store.initialize(); await store.initialize(); }); }); // ─────────────────────────────────────────────────────────────────────────── // 3. Memory class – auto-init with probe, lazy gate, backward compat // ─────────────────────────────────────────────────────────────────────────── describe("Memory – auto-initialization", () => { let mockEmbedderFactory: any; let mockVectorStoreFactory: any; let mockLlmFactory: any; let mockHistoryFactory: any; let MemoryClass: any; function createMockEmbedder(dims: number) { return { embed: jest.fn().mockResolvedValue(new Array(dims).fill(0)), embedBatch: jest.fn().mockResolvedValue([new Array(dims).fill(0)]), }; } function createMockVectorStore() { return { insert: jest.fn().mockResolvedValue(undefined), search: jest.fn().mockResolvedValue([]), get: jest.fn().mockResolvedValue(null), update: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue(undefined), deleteCol: jest.fn().mockResolvedValue(undefined), list: jest.fn().mockResolvedValue([[], 0]), getUserId: jest.fn().mockResolvedValue("test-user-id"), setUserId: jest.fn().mockResolvedValue(undefined), initialize: jest.fn().mockResolvedValue(undefined), }; } beforeEach(() => { jest.resetModules(); const mockEmbedder = createMockEmbedder(768); const mockVStore = createMockVectorStore(); mockEmbedderFactory = { create: jest.fn().mockReturnValue(mockEmbedder) }; mockVectorStoreFactory = { create: jest.fn().mockReturnValue(mockVStore) }; mockLlmFactory = { create: jest.fn().mockReturnValue({ generateResponse: jest.fn().mockResolvedValue('{"facts":[]}'), }), }; mockHistoryFactory = { create: jest.fn().mockReturnValue({ addHistory: jest.fn().mockResolvedValue(undefined), getHistory: jest.fn().mockResolvedValue([]), reset: jest.fn().mockResolvedValue(undefined), }), }; jest.doMock("../src/utils/factory", () => ({ EmbedderFactory: mockEmbedderFactory, VectorStoreFactory: mockVectorStoreFactory, LLMFactory: mockLlmFactory, HistoryManagerFactory: mockHistoryFactory, })); jest.doMock("../src/utils/telemetry", () => ({ captureClientEvent: jest.fn().mockResolvedValue(undefined), })); MemoryClass = require("../src/memory").Memory; }); afterEach(() => { jest.restoreAllMocks(); jest.resetModules(); }); it("probes embedder to detect dimension when none set", async () => { const mockEmbedder = createMockEmbedder(768); const mockVStore = createMockVectorStore(); mockEmbedderFactory.create.mockReturnValue(mockEmbedder); mockVectorStoreFactory.create.mockReturnValue(mockVStore); const mem = new MemoryClass({ embedder: { provider: "ollama", config: { model: "nomic-embed-text" } }, vectorStore: { provider: "qdrant", config: { collectionName: "test" } }, llm: { provider: "openai", config: { apiKey: "k" } }, disableHistory: true, }); await mem.getAll({ userId: "u1" }); // Should have called embed("dimension probe") to detect dimension expect(mockEmbedder.embed).toHaveBeenCalledWith("dimension probe"); // VectorStoreFactory should have been called with detected dimension const vsCreateCall = mockVectorStoreFactory.create.mock.calls[0]; expect(vsCreateCall[1].dimension).toBe(768); }); it("skips probe when explicit dimension provided", async () => { const mockEmbedder = createMockEmbedder(1536); const mockVStore = createMockVectorStore(); mockEmbedderFactory.create.mockReturnValue(mockEmbedder); mockVectorStoreFactory.create.mockReturnValue(mockVStore); const mem = new MemoryClass({ embedder: { provider: "openai", config: { apiKey: "k" } }, vectorStore: { provider: "memory", config: { collectionName: "test", dimension: 1536 }, }, llm: { provider: "openai", config: { apiKey: "k" } }, disableHistory: true, }); await mem.getAll({ userId: "u1" }); // embed should NOT have been called for probing expect(mockEmbedder.embed).not.toHaveBeenCalledWith("dimension probe"); // VectorStoreFactory gets the explicit dimension const vsCreateCall = mockVectorStoreFactory.create.mock.calls[0]; expect(vsCreateCall[1].dimension).toBe(1536); }); it("skips probe when embeddingDims provided", async () => { const mockEmbedder = createMockEmbedder(768); const mockVStore = createMockVectorStore(); mockEmbedderFactory.create.mockReturnValue(mockEmbedder); mockVectorStoreFactory.create.mockReturnValue(mockVStore); const mem = new MemoryClass({ embedder: { provider: "ollama", config: { model: "nomic-embed-text", embeddingDims: 768 }, }, vectorStore: { provider: "qdrant", config: { collectionName: "test" } }, llm: { provider: "openai", config: { apiKey: "k" } }, disableHistory: true, }); await mem.getAll({ userId: "u1" }); // ConfigManager resolves dimension from embeddingDims → no probe needed expect(mockEmbedder.embed).not.toHaveBeenCalledWith("dimension probe"); }); it("all public methods wait for initialization", async () => { let resolveProbe: () => void; let probeCallCount = 0; const mockEmbedder = { embed: jest.fn().mockImplementation(() => { probeCallCount++; if (probeCallCount === 1) { // First call is the dimension probe — hang until manually resolved return new Promise((resolve) => { resolveProbe = () => resolve(new Array(768).fill(0)); }); } // Subsequent calls (from search, etc.) resolve immediately return Promise.resolve(new Array(768).fill(0)); }), embedBatch: jest.fn(), }; const mockVStore = createMockVectorStore(); mockEmbedderFactory.create.mockReturnValue(mockEmbedder); mockVectorStoreFactory.create.mockReturnValue(mockVStore); const mem = new MemoryClass({ embedder: { provider: "ollama", config: { model: "test" } }, vectorStore: { provider: "qdrant", config: { collectionName: "t" } }, llm: { provider: "openai", config: { apiKey: "k" } }, disableHistory: true, }); let getAllDone = false; let searchDone = false; let getDone = false; const getAllP = mem.getAll({ userId: "u" }).then(() => (getAllDone = true)); const searchP = mem .search("q", { userId: "u" }) .then(() => (searchDone = true)); const getP = mem.get("id").then(() => (getDone = true)); await new Promise((r) => setTimeout(r, 50)); expect(getAllDone).toBe(false); expect(searchDone).toBe(false); expect(getDone).toBe(false); // Resolve the probe — init completes — methods unblock resolveProbe!(); await Promise.all([getAllP, searchP, getP]); expect(getAllDone).toBe(true); expect(searchDone).toBe(true); expect(getDone).toBe(true); }); it("reset re-creates vector store with correct dimension", async () => { const mockEmbedder = createMockEmbedder(768); const mockVStore = createMockVectorStore(); mockEmbedderFactory.create.mockReturnValue(mockEmbedder); mockVectorStoreFactory.create.mockReturnValue(mockVStore); const mem = new MemoryClass({ embedder: { provider: "ollama", config: { model: "nomic-embed-text" } }, vectorStore: { provider: "qdrant", config: { collectionName: "test" } }, llm: { provider: "openai", config: { apiKey: "k" } }, disableHistory: true, }); await mem.getAll({ userId: "u1" }); expect(mockVectorStoreFactory.create).toHaveBeenCalledTimes(1); // Reset should re-create vector store const mockVStore2 = createMockVectorStore(); mockVectorStoreFactory.create.mockReturnValue(mockVStore2); await mem.reset(); expect(mockVectorStoreFactory.create).toHaveBeenCalledTimes(2); // Second creation should still have dimension=768 (cached from first probe) const secondCall = mockVectorStoreFactory.create.mock.calls[1]; expect(secondCall[1].dimension).toBe(768); }); it("backward compat: full explicit config works without probe", async () => { const mockEmbedder = createMockEmbedder(1536); const mockVStore = createMockVectorStore(); mockEmbedderFactory.create.mockReturnValue(mockEmbedder); mockVectorStoreFactory.create.mockReturnValue(mockVStore); const mem = new MemoryClass({ version: "v1.1", embedder: { provider: "openai", config: { apiKey: "sk-fake", model: "text-embedding-3-small" }, }, vectorStore: { provider: "memory", config: { collectionName: "test-memories", dimension: 1536 }, }, llm: { provider: "openai", config: { apiKey: "sk-fake", model: "gpt-4-turbo-preview" }, }, historyDbPath: ":memory:", disableHistory: true, }); await mem.getAll({ userId: "u1" }); expect(mockEmbedder.embed).not.toHaveBeenCalledWith("dimension probe"); }); it("throws explicit error when probe fails", async () => { const mockEmbedder = { embed: jest.fn().mockRejectedValue(new Error("Connection refused")), embedBatch: jest.fn(), }; mockEmbedderFactory.create.mockReturnValue(mockEmbedder); // Suppress console.error for this test const consoleSpy = jest .spyOn(console, "error") .mockImplementation(() => {}); const mem = new MemoryClass({ embedder: { provider: "ollama", config: { model: "nomic-embed-text" } }, vectorStore: { provider: "qdrant", config: { collectionName: "test" } }, llm: { provider: "openai", config: { apiKey: "k" } }, disableHistory: true, }); // getAll should reject with the init error await expect(mem.getAll({ userId: "u1" })).rejects.toThrow( "auto-detect embedding dimension", ); // Verify the error was logged and contains helpful information const errorCall = consoleSpy.mock.calls.find( (call) => call[0] instanceof Error && call[0].message.includes("auto-detect embedding dimension"), ); expect(errorCall).toBeDefined(); const errorMsg = (errorCall![0] as Error).message; expect(errorMsg).toContain("ollama"); expect(errorMsg).toContain("Connection refused"); expect(errorMsg).toContain("dimension"); expect(errorMsg).toContain("embeddingDims"); consoleSpy.mockRestore(); }); });