Files
mem0/mem0-ts/src/oss/tests/vector-stores-compat.test.ts
T

1139 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// <reference types="jest" />
/**
* Backward-compatibility tests for ALL vector store implementations.
*
* Verifies that:
* 1. Every store implements the full VectorStore interface
* 2. initialize() is idempotent (safe to call multiple times)
* 3. Constructor + explicit initialize() doesn't break (the double-call pattern)
* 4. All CRUD methods work correctly after initialization
* 5. getUserId / setUserId work correctly
* 6. The Memory class works with each store via mocked factories
*/
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
jest.setTimeout(15000);
// ───────────────────────────────────────────────────────────────────────────
// 1. MemoryVectorStore — full CRUD, no external dependencies
// ───────────────────────────────────────────────────────────────────────────
describe("MemoryVectorStore – full backward compat", () => {
const { MemoryVectorStore } = require("../src/vector_stores/memory");
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-vs-compat-"));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("implements full VectorStore interface", () => {
const store = new MemoryVectorStore({
collectionName: "test",
dbPath: path.join(tmpDir, "vs.db"),
});
expect(typeof store.insert).toBe("function");
expect(typeof store.search).toBe("function");
expect(typeof store.get).toBe("function");
expect(typeof store.update).toBe("function");
expect(typeof store.delete).toBe("function");
expect(typeof store.deleteCol).toBe("function");
expect(typeof store.list).toBe("function");
expect(typeof store.getUserId).toBe("function");
expect(typeof store.setUserId).toBe("function");
expect(typeof store.initialize).toBe("function");
});
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();
// Insert should still work after multiple initializations
const vec = new Array(1536).fill(0.1);
await store.insert([vec], ["id-1"], [{ data: "test" }]);
const result = await store.get("id-1");
expect(result).not.toBeNull();
});
it("full CRUD cycle with default dimension 1536", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dbPath: path.join(tmpDir, "vs.db"),
});
const vec1 = new Array(1536).fill(0);
vec1[0] = 1.0;
const vec2 = new Array(1536).fill(0);
vec2[1] = 1.0;
// Insert
await store.insert(
[vec1, vec2],
["id-1", "id-2"],
[
{ data: "alpha", userId: "u1" },
{ data: "beta", userId: "u1" },
],
);
// Get
const item = await store.get("id-1");
expect(item).not.toBeNull();
expect(item!.payload.data).toBe("alpha");
// Search
const results = await store.search(vec1, 2);
expect(results.length).toBe(2);
expect(results[0].id).toBe("id-1");
// Search with filters (camelCase in payload is normalized to snake_case)
const filtered = await store.search(vec1, 2, { user_id: "u1" });
expect(filtered.length).toBe(2);
// Update
const vec3 = new Array(1536).fill(0);
vec3[2] = 1.0;
await store.update("id-1", vec3, { data: "updated", user_id: "u1" });
const updated = await store.get("id-1");
expect(updated!.payload.data).toBe("updated");
// List
const [listed, count] = await store.list({ user_id: "u1" });
expect(count).toBe(2);
// List with limit
const [limitedList] = await store.list(undefined, 1);
expect(limitedList.length).toBe(1);
// Delete
await store.delete("id-2");
const deleted = await store.get("id-2");
expect(deleted).toBeNull();
// DeleteCol + re-init
await store.deleteCol();
const [afterDelete] = await store.list();
expect(afterDelete.length).toBe(0);
});
it("full CRUD cycle with custom dimension 768", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dimension: 768,
dbPath: path.join(tmpDir, "vs.db"),
});
const vec = new Array(768).fill(0.1);
await store.insert([vec], ["id-1"], [{ data: "test" }]);
const result = await store.get("id-1");
expect(result!.payload.data).toBe("test");
const searchResults = await store.search(vec, 1);
expect(searchResults.length).toBe(1);
});
it("rejects dimension mismatch on insert", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dimension: 1536,
dbPath: path.join(tmpDir, "vs.db"),
});
await expect(
store.insert([new Array(768).fill(0)], ["id-1"], [{}]),
).rejects.toThrow("Vector dimension mismatch");
});
it("rejects dimension mismatch on search", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dimension: 1536,
dbPath: path.join(tmpDir, "vs.db"),
});
await expect(store.search(new Array(768).fill(0), 1)).rejects.toThrow(
"Query dimension mismatch",
);
});
it("rejects dimension mismatch on update", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dimension: 1536,
dbPath: path.join(tmpDir, "vs.db"),
});
await expect(
store.update("id-1", new Array(768).fill(0), {}),
).rejects.toThrow("Vector dimension mismatch");
});
it("getUserId and setUserId roundtrip", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dbPath: path.join(tmpDir, "vs.db"),
});
const auto = await store.getUserId();
expect(typeof auto).toBe("string");
expect(auto.length).toBeGreaterThan(0);
await store.setUserId("custom-user");
expect(await store.getUserId()).toBe("custom-user");
// Overwrite
await store.setUserId("another-user");
expect(await store.getUserId()).toBe("another-user");
});
it("get returns null for non-existent ID", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dbPath: path.join(tmpDir, "vs.db"),
});
const result = await store.get("non-existent");
expect(result).toBeNull();
});
});
// ───────────────────────────────────────────────────────────────────────────
// 2. Qdrant — mock QdrantClient, test interface + idempotent init
// ───────────────────────────────────────────────────────────────────────────
describe("Qdrant – backward compat with mocked client", () => {
function createMockQdrantClient() {
const collections = new Map<string, number>();
const points = new Map<
string,
{ id: string; vector: number[]; payload: any }
>();
return {
_collections: collections,
_points: points,
createCollection: jest
.fn()
.mockImplementation(async (name: string, opts: any) => {
if (collections.has(name)) {
const err: any = new Error("Collection already exists");
err.status = 409;
throw err;
}
collections.set(name, opts.vectors.size);
}),
getCollection: jest.fn().mockImplementation(async (name: string) => {
if (!collections.has(name)) {
const err: any = new Error("Not found");
err.status = 404;
throw err;
}
return {
config: { params: { vectors: { size: collections.get(name) } } },
};
}),
getCollections: jest.fn().mockResolvedValue({
collections: [],
}),
upsert: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
for (const pt of opts.points) {
points.set(`${collName}:${pt.id}`, {
id: pt.id,
vector: pt.vector,
payload: pt.payload,
});
}
}),
retrieve: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
const results = [];
for (const id of opts.ids) {
const pt = points.get(`${collName}:${id}`);
if (pt) results.push({ id: pt.id, payload: pt.payload });
}
return results;
}),
search: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
const results: any[] = [];
points.forEach((pt, key) => {
if (key.startsWith(`${collName}:`)) {
results.push({ id: pt.id, payload: pt.payload, score: 0.9 });
}
});
return results.slice(0, opts.limit);
}),
scroll: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
const results: any[] = [];
points.forEach((pt, key) => {
if (key.startsWith(`${collName}:`)) {
results.push({ id: pt.id, payload: pt.payload });
}
});
return { points: results.slice(0, opts.limit) };
}),
delete: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
for (const id of opts.points) {
points.delete(`${collName}:${id}`);
}
}),
deleteCollection: jest.fn().mockImplementation(async (name: string) => {
collections.delete(name);
}),
};
}
it("implements full VectorStore interface", () => {
const { Qdrant } = require("../src/vector_stores/qdrant");
const store = new Qdrant({
client: createMockQdrantClient(),
collectionName: "test",
embeddingModelDims: 768,
dimension: 768,
});
expect(typeof store.insert).toBe("function");
expect(typeof store.search).toBe("function");
expect(typeof store.get).toBe("function");
expect(typeof store.update).toBe("function");
expect(typeof store.delete).toBe("function");
expect(typeof store.deleteCol).toBe("function");
expect(typeof store.list).toBe("function");
expect(typeof store.getUserId).toBe("function");
expect(typeof store.setUserId).toBe("function");
expect(typeof store.initialize).toBe("function");
});
it("initialize() is idempotent (same promise returned)", async () => {
const { Qdrant } = require("../src/vector_stores/qdrant");
const mockClient = createMockQdrantClient();
const store = new Qdrant({
client: mockClient,
collectionName: "test",
embeddingModelDims: 768,
dimension: 768,
});
// Constructor already fires initialize()
const p1 = store.initialize();
const p2 = store.initialize();
const p3 = store.initialize();
await Promise.all([p1, p2, p3]);
// createCollection called only once per collection despite multiple initialize() calls
expect(mockClient.createCollection).toHaveBeenCalledTimes(2); // test + memory_migrations
});
it("full CRUD cycle", async () => {
const { Qdrant } = require("../src/vector_stores/qdrant");
const mockClient = createMockQdrantClient();
const store = new Qdrant({
client: mockClient,
collectionName: "test",
embeddingModelDims: 768,
dimension: 768,
});
await store.initialize();
// Insert
await store.insert(
[
[1, 2, 3],
[4, 5, 6],
],
["id-1", "id-2"],
[{ data: "alpha" }, { data: "beta" }],
);
expect(mockClient.upsert).toHaveBeenCalled();
// Get
const item = await store.get("id-1");
expect(item).not.toBeNull();
expect(item!.payload.data).toBe("alpha");
// Search
const results = await store.search([1, 2, 3], 2);
expect(results.length).toBeGreaterThan(0);
// Update
await store.update("id-1", [7, 8, 9], { data: "updated" });
// List
const [listed, count] = await store.list();
expect(listed.length).toBeGreaterThan(0);
// Delete
await store.delete("id-2");
// DeleteCol
await store.deleteCol();
expect(mockClient.deleteCollection).toHaveBeenCalledWith("test");
});
it("getUserId and setUserId roundtrip", async () => {
const { Qdrant } = require("../src/vector_stores/qdrant");
const mockClient = createMockQdrantClient();
const store = new Qdrant({
client: mockClient,
collectionName: "test",
embeddingModelDims: 768,
dimension: 768,
});
await store.initialize();
const userId = await store.getUserId();
expect(typeof userId).toBe("string");
expect(userId.length).toBeGreaterThan(0);
await store.setUserId("custom-user");
const updated = await store.getUserId();
expect(updated).toBe("custom-user");
});
});
// ───────────────────────────────────────────────────────────────────────────
// 3. Redis — mock redis client, test interface + idempotent init
// ───────────────────────────────────────────────────────────────────────────
describe("Redis – backward compat with mocked client", () => {
let RedisDB: any;
beforeEach(() => {
jest.resetModules();
// Mock redis createClient
jest.doMock("redis", () => {
const store = new Map<string, any>();
const mockClient = {
connect: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
isOpen: false,
moduleList: jest
.fn()
.mockResolvedValue([["name", "search", "ver", 20000]]),
ft: {
dropIndex: jest.fn().mockResolvedValue(undefined),
create: jest.fn().mockResolvedValue(undefined),
search: jest.fn().mockResolvedValue({ total: 0, documents: [] }),
},
hSet: jest.fn().mockImplementation(async (key: string, obj: any) => {
store.set(key, obj);
}),
hGetAll: jest.fn().mockImplementation(async (key: string) => {
return store.get(key) || {};
}),
del: jest.fn().mockImplementation(async (key: string) => {
store.delete(key);
}),
keys: jest.fn().mockResolvedValue([]),
quit: jest.fn().mockResolvedValue(undefined),
};
// Track connect calls for assertion
mockClient.connect.mockImplementation(async () => {
mockClient.isOpen = true;
});
return {
createClient: jest.fn().mockReturnValue(mockClient),
SchemaFieldTypes: {
VECTOR: "VECTOR",
TAG: "TAG",
TEXT: "TEXT",
NUMERIC: "NUMERIC",
},
VectorAlgorithms: {
FLAT: "FLAT",
HNSW: "HNSW",
},
__mockClient: mockClient,
};
});
RedisDB = require("../src/vector_stores/redis").RedisDB;
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});
it("implements full VectorStore interface", () => {
const store = new RedisDB({
collectionName: "test",
embeddingModelDims: 768,
redisUrl: "redis://localhost:6379",
});
expect(typeof store.insert).toBe("function");
expect(typeof store.search).toBe("function");
expect(typeof store.get).toBe("function");
expect(typeof store.update).toBe("function");
expect(typeof store.delete).toBe("function");
expect(typeof store.deleteCol).toBe("function");
expect(typeof store.list).toBe("function");
expect(typeof store.getUserId).toBe("function");
expect(typeof store.setUserId).toBe("function");
expect(typeof store.initialize).toBe("function");
});
it("initialize() is idempotent (same promise returned)", async () => {
const redis = require("redis");
const mockClient = redis.__mockClient;
const store = new RedisDB({
collectionName: "test",
embeddingModelDims: 768,
redisUrl: "redis://localhost:6379",
});
// Constructor already fires initialize()
const p1 = store.initialize();
const p2 = store.initialize();
const p3 = store.initialize();
await Promise.all([p1, p2, p3]);
// connect() called only once despite multiple initialize() calls
expect(mockClient.connect).toHaveBeenCalledTimes(1);
});
it("constructor + explicit initialize() doesn't double-connect", async () => {
const redis = require("redis");
const mockClient = redis.__mockClient;
const store = new RedisDB({
collectionName: "test",
embeddingModelDims: 768,
redisUrl: "redis://localhost:6379",
});
// Explicitly awaiting initialize (what Memory._autoInitialize does)
await store.initialize();
// Should only have connected once
expect(mockClient.connect).toHaveBeenCalledTimes(1);
});
});
// ───────────────────────────────────────────────────────────────────────────
// 4. Supabase — mock Supabase client, test idempotent init
// ───────────────────────────────────────────────────────────────────────────
describe("Supabase – backward compat with mocked client", () => {
let SupabaseDB: any;
beforeEach(() => {
jest.resetModules();
jest.doMock("@supabase/supabase-js", () => {
const mockClient = {
from: jest.fn().mockReturnValue({
insert: jest.fn().mockReturnValue({
select: jest.fn().mockReturnValue({ error: null }),
}),
select: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({ data: [], error: null }),
}),
delete: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({ error: null }),
}),
update: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({ error: null }),
}),
upsert: jest.fn().mockReturnValue({ error: null }),
}),
rpc: jest.fn().mockResolvedValue({ data: [], error: null }),
};
return {
createClient: jest.fn().mockReturnValue(mockClient),
__mockClient: mockClient,
};
});
SupabaseDB = require("../src/vector_stores/supabase").SupabaseDB;
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});
it("implements full VectorStore interface", () => {
const store = new SupabaseDB({
supabaseUrl: "https://example.supabase.co",
supabaseKey: "fake-key",
tableName: "memories",
collectionName: "test",
});
expect(typeof store.insert).toBe("function");
expect(typeof store.search).toBe("function");
expect(typeof store.get).toBe("function");
expect(typeof store.update).toBe("function");
expect(typeof store.delete).toBe("function");
expect(typeof store.deleteCol).toBe("function");
expect(typeof store.list).toBe("function");
expect(typeof store.getUserId).toBe("function");
expect(typeof store.setUserId).toBe("function");
expect(typeof store.initialize).toBe("function");
});
it("initialize() is idempotent (same promise returned)", async () => {
const store = new SupabaseDB({
supabaseUrl: "https://example.supabase.co",
supabaseKey: "fake-key",
tableName: "memories",
collectionName: "test",
});
const p1 = store.initialize();
const p2 = store.initialize();
await Promise.all([p1, p2]);
// No crash = idempotent (Supabase init runs test insert only once)
});
});
// ───────────────────────────────────────────────────────────────────────────
// 5. AzureAISearch — mock Azure clients, test idempotent init
// ───────────────────────────────────────────────────────────────────────────
describe("AzureAISearch – backward compat with mocked client", () => {
let AzureAISearch: any;
beforeEach(() => {
jest.resetModules();
jest.doMock("@azure/search-documents", () => ({
SearchClient: jest.fn().mockImplementation(() => ({
search: jest.fn().mockReturnValue({
[Symbol.asyncIterator]: () => ({ next: () => ({ done: true }) }),
}),
getDocument: jest.fn().mockResolvedValue(null),
mergeOrUploadDocuments: jest.fn().mockResolvedValue({}),
deleteDocuments: jest.fn().mockResolvedValue({}),
})),
SearchIndexClient: jest.fn().mockImplementation(() => ({
listIndexes: jest.fn().mockReturnValue({
[Symbol.asyncIterator]: () => ({ next: () => ({ done: true }) }),
}),
createOrUpdateIndex: jest.fn().mockResolvedValue({}),
deleteIndex: jest.fn().mockResolvedValue({}),
})),
AzureKeyCredential: jest
.fn()
.mockImplementation((key: string) => ({ key })),
}));
jest.doMock("@azure/identity", () => ({
DefaultAzureCredential: jest.fn(),
}));
AzureAISearch =
require("../src/vector_stores/azure_ai_search").AzureAISearch;
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});
it("implements full VectorStore interface", () => {
const store = new AzureAISearch({
serviceName: "test-service",
collectionName: "test-index",
apiKey: "fake-key",
embeddingModelDims: 768,
});
expect(typeof store.insert).toBe("function");
expect(typeof store.search).toBe("function");
expect(typeof store.get).toBe("function");
expect(typeof store.update).toBe("function");
expect(typeof store.delete).toBe("function");
expect(typeof store.deleteCol).toBe("function");
expect(typeof store.list).toBe("function");
expect(typeof store.getUserId).toBe("function");
expect(typeof store.setUserId).toBe("function");
expect(typeof store.initialize).toBe("function");
});
it("initialize() is idempotent (same promise returned)", async () => {
const store = new AzureAISearch({
serviceName: "test-service",
collectionName: "test-index",
apiKey: "fake-key",
embeddingModelDims: 768,
});
const p1 = store.initialize();
const p2 = store.initialize();
const p3 = store.initialize();
await Promise.all([p1, p2, p3]);
// No crash = idempotent
});
});
// ───────────────────────────────────────────────────────────────────────────
// 6. Vectorize — mock Cloudflare client, test idempotent init
// ───────────────────────────────────────────────────────────────────────────
describe("Vectorize – backward compat with mocked client", () => {
let VectorizeDB: any;
beforeEach(() => {
jest.resetModules();
jest.doMock("cloudflare", () => {
const mockIndexes = {
list: jest.fn().mockReturnValue({
[Symbol.asyncIterator]: () => ({
next: async () => ({ done: true }),
}),
}),
create: jest.fn().mockResolvedValue({}),
delete: jest.fn().mockResolvedValue({}),
query: jest.fn().mockResolvedValue({ matches: [] }),
getByIds: jest.fn().mockResolvedValue([]),
metadataIndex: {
list: jest.fn().mockResolvedValue({ metadataIndexes: [] }),
create: jest.fn().mockResolvedValue({}),
},
};
return {
__esModule: true,
default: jest.fn().mockImplementation(() => ({
apiToken: "fake-token",
vectorize: { indexes: mockIndexes },
__mockIndexes: mockIndexes,
})),
};
});
VectorizeDB = require("../src/vector_stores/vectorize").VectorizeDB;
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});
it("implements full VectorStore interface", () => {
const store = new VectorizeDB({
apiKey: "fake-token",
indexName: "test-index",
accountId: "test-account",
dimension: 768,
});
expect(typeof store.insert).toBe("function");
expect(typeof store.search).toBe("function");
expect(typeof store.get).toBe("function");
expect(typeof store.update).toBe("function");
expect(typeof store.delete).toBe("function");
expect(typeof store.deleteCol).toBe("function");
expect(typeof store.list).toBe("function");
expect(typeof store.getUserId).toBe("function");
expect(typeof store.setUserId).toBe("function");
expect(typeof store.initialize).toBe("function");
});
it("initialize() is idempotent (same promise returned)", async () => {
const store = new VectorizeDB({
apiKey: "fake-token",
indexName: "test-index",
accountId: "test-account",
dimension: 768,
});
const p1 = store.initialize();
const p2 = store.initialize();
await Promise.all([p1, p2]);
// No crash = idempotent
});
});
// ───────────────────────────────────────────────────────────────────────────
// 7. LangchainVectorStore — mock Langchain client, verify no-op init
// ───────────────────────────────────────────────────────────────────────────
describe("LangchainVectorStore – backward compat", () => {
it("implements full VectorStore interface", () => {
const { LangchainVectorStore } = require("../src/vector_stores/langchain");
const mockLcStore = {
addVectors: jest.fn().mockResolvedValue(undefined),
similaritySearchVectorWithScore: jest.fn().mockResolvedValue([]),
delete: jest.fn().mockResolvedValue(undefined),
};
const store = new LangchainVectorStore({
client: mockLcStore,
collectionName: "test",
dimension: 768,
});
expect(typeof store.insert).toBe("function");
expect(typeof store.search).toBe("function");
expect(typeof store.get).toBe("function");
expect(typeof store.update).toBe("function");
expect(typeof store.delete).toBe("function");
expect(typeof store.deleteCol).toBe("function");
expect(typeof store.list).toBe("function");
expect(typeof store.getUserId).toBe("function");
expect(typeof store.setUserId).toBe("function");
expect(typeof store.initialize).toBe("function");
});
it("initialize() is a no-op and safe to call multiple times", async () => {
const { LangchainVectorStore } = require("../src/vector_stores/langchain");
const mockLcStore = {
addVectors: jest.fn().mockResolvedValue(undefined),
similaritySearchVectorWithScore: jest.fn().mockResolvedValue([]),
};
const store = new LangchainVectorStore({
client: mockLcStore,
collectionName: "test",
});
await store.initialize();
await store.initialize();
await store.initialize();
});
it("insert and search work with mock Langchain client", async () => {
const { LangchainVectorStore } = require("../src/vector_stores/langchain");
const mockLcStore = {
addVectors: jest.fn().mockResolvedValue(undefined),
similaritySearchVectorWithScore: jest
.fn()
.mockResolvedValue([
[
{ metadata: { _mem0_id: "id-1", data: "test" }, pageContent: "" },
0.95,
],
]),
};
const store = new LangchainVectorStore({
client: mockLcStore,
collectionName: "test",
dimension: 4,
});
await store.insert([[1, 2, 3, 4]], ["id-1"], [{ data: "test" }]);
expect(mockLcStore.addVectors).toHaveBeenCalled();
const results = await store.search([1, 2, 3, 4], 1);
expect(results.length).toBe(1);
expect(results[0].id).toBe("id-1");
expect(results[0].score).toBe(0.95);
});
it("getUserId and setUserId work (in-memory)", async () => {
const { LangchainVectorStore } = require("../src/vector_stores/langchain");
const mockLcStore = {
addVectors: jest.fn(),
similaritySearchVectorWithScore: jest.fn(),
};
const store = new LangchainVectorStore({
client: mockLcStore,
collectionName: "test",
});
const defaultId = await store.getUserId();
expect(defaultId).toBe("anonymous-langchain-user");
await store.setUserId("custom-user");
expect(await store.getUserId()).toBe("custom-user");
});
it("rejects vector dimension mismatch on insert", async () => {
const { LangchainVectorStore } = require("../src/vector_stores/langchain");
const mockLcStore = {
addVectors: jest.fn(),
similaritySearchVectorWithScore: jest.fn(),
};
const store = new LangchainVectorStore({
client: mockLcStore,
collectionName: "test",
dimension: 4,
});
await expect(store.insert([[1, 2, 3]], ["id-1"], [{}])).rejects.toThrow(
"Vector dimension mismatch",
);
});
});
// ───────────────────────────────────────────────────────────────────────────
// 8. Memory class — ensure it works with each provider via mocked factories
// ───────────────────────────────────────────────────────────────────────────
describe("Memory class – backward compat with all providers", () => {
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),
};
}
let MemoryClass: any;
let mockEmbedderFactory: any;
let mockVectorStoreFactory: any;
beforeEach(() => {
jest.resetModules();
const mockEmbedder = createMockEmbedder(1536);
const mockVStore = createMockVectorStore();
mockEmbedderFactory = { create: jest.fn().mockReturnValue(mockEmbedder) };
mockVectorStoreFactory = { create: jest.fn().mockReturnValue(mockVStore) };
jest.doMock("../src/utils/factory", () => ({
EmbedderFactory: mockEmbedderFactory,
VectorStoreFactory: mockVectorStoreFactory,
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),
isTelemetryEnabled: jest.fn(() => false),
}));
MemoryClass = require("../src/memory").Memory;
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});
it("works with explicit dimension (no probe)", async () => {
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({ filters: { user_id: "u1" } });
const embedder = mockEmbedderFactory.create.mock.results[0].value;
expect(embedder.embed).not.toHaveBeenCalledWith("dimension probe");
const vsCreateCall = mockVectorStoreFactory.create.mock.calls[0];
expect(vsCreateCall[1].dimension).toBe(1536);
});
it("works with embeddingDims (no probe)", async () => {
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,
});
const mockEmbedder768 = createMockEmbedder(768);
mockEmbedderFactory.create.mockReturnValue(mockEmbedder768);
await mem.getAll({ filters: { user_id: "u1" } });
expect(mockEmbedder768.embed).not.toHaveBeenCalledWith("dimension probe");
});
it("probes when no dimension provided", async () => {
const mockEmbedder768 = createMockEmbedder(768);
mockEmbedderFactory.create.mockReturnValue(mockEmbedder768);
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({ filters: { user_id: "u1" } });
expect(mockEmbedder768.embed).toHaveBeenCalledWith("dimension probe");
const vsCreateCall = mockVectorStoreFactory.create.mock.calls[0];
expect(vsCreateCall[1].dimension).toBe(768);
});
it("calls vectorStore.initialize() after creation", async () => {
const mockVStore = createMockVectorStore();
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({ filters: { user_id: "u1" } });
expect(mockVStore.initialize).toHaveBeenCalled();
});
it("all public methods work after initialization", async () => {
const mockVStore = createMockVectorStore();
mockVStore.search.mockResolvedValue([
{ id: "id-1", payload: { memory: "test", hash: "h" }, score: 0.9 },
]);
mockVStore.get.mockResolvedValue({
id: "id-1",
payload: {
memory: "test",
hash: "h",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
});
mockVStore.list.mockResolvedValue([
[
{
id: "id-1",
payload: {
memory: "test",
hash: "h",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
},
],
1,
]);
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,
});
// getAll
const all = await mem.getAll({ filters: { user_id: "u1" } });
expect(all).toBeDefined();
// search
const searchResult = await mem.search("query", {
filters: { user_id: "u1" },
});
expect(searchResult).toBeDefined();
// get
const item = await mem.get("id-1");
expect(item).toBeDefined();
// update
const updateResult = await mem.update("id-1", "new data");
expect(updateResult.message).toBe("Memory updated successfully!");
// delete
const deleteResult = await mem.delete("id-1");
expect(deleteResult.message).toBe("Memory deleted successfully!");
// deleteAll
const deleteAllResult = await mem.deleteAll({ userId: "u1" });
expect(deleteAllResult.message).toBe("Memories deleted successfully!");
// history
const history = await mem.history("id-1");
expect(Array.isArray(history)).toBe(true);
});
it("reset re-creates vector store correctly", async () => {
const mockVStore1 = createMockVectorStore();
const mockVStore2 = createMockVectorStore();
mockVectorStoreFactory.create
.mockReturnValueOnce(mockVStore1)
.mockReturnValueOnce(mockVStore2);
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({ filters: { user_id: "u1" } });
expect(mockVectorStoreFactory.create).toHaveBeenCalledTimes(1);
await mem.reset();
expect(mockVectorStoreFactory.create).toHaveBeenCalledTimes(2);
// Second store should also have initialize called
expect(mockVStore2.initialize).toHaveBeenCalled();
});
it("propagates init error to public methods", async () => {
const failingEmbedder = {
embed: jest.fn().mockRejectedValue(new Error("Embedder unreachable")),
embedBatch: jest.fn(),
};
mockEmbedderFactory.create.mockReturnValue(failingEmbedder);
const consoleSpy = jest
.spyOn(console, "error")
.mockImplementation(() => {});
const mem = new MemoryClass({
embedder: { provider: "ollama", config: { model: "test" } },
vectorStore: { provider: "qdrant", config: { collectionName: "t" } },
llm: { provider: "openai", config: { apiKey: "k" } },
disableHistory: true,
});
await expect(mem.getAll({ filters: { user_id: "u1" } })).rejects.toThrow(
"auto-detect embedding dimension",
);
await expect(
mem.search("q", { filters: { user_id: "u1" } }),
).rejects.toThrow("auto-detect embedding dimension");
await expect(mem.get("id")).rejects.toThrow(
"auto-detect embedding dimension",
);
consoleSpy.mockRestore();
});
});