///
/**
* 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
const filtered = await store.search(vec1, 2, { userId: "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", userId: "u1" });
const updated = await store.get("id-1");
expect(updated!.payload.data).toBe("updated");
// List
const [listed, count] = await store.list({ userId: "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();
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();
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),
}));
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({ userId: "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({ userId: "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({ userId: "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({ userId: "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({ userId: "u1" });
expect(all).toBeDefined();
// search
const searchResult = await mem.search("query", { userId: "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({ userId: "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({ userId: "u1" })).rejects.toThrow(
"auto-detect embedding dimension",
);
await expect(mem.search("q", { userId: "u1" })).rejects.toThrow(
"auto-detect embedding dimension",
);
await expect(mem.get("id")).rejects.toThrow(
"auto-detect embedding dimension",
);
consoleSpy.mockRestore();
});
});