Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 124c59bbe6 | |||
| ec3eedbcb0 | |||
| 7a3bc22161 |
@@ -95,16 +95,34 @@ export class ConfigManager {
|
||||
})(),
|
||||
},
|
||||
historyDbPath:
|
||||
userConfig.historyDbPath || DEFAULT_MEMORY_CONFIG.historyDbPath,
|
||||
userConfig.historyDbPath ||
|
||||
userConfig.historyStore?.config?.historyDbPath ||
|
||||
DEFAULT_MEMORY_CONFIG.historyStore?.config?.historyDbPath,
|
||||
customPrompt: userConfig.customPrompt,
|
||||
graphStore: {
|
||||
...DEFAULT_MEMORY_CONFIG.graphStore,
|
||||
...userConfig.graphStore,
|
||||
},
|
||||
historyStore: {
|
||||
...DEFAULT_MEMORY_CONFIG.historyStore,
|
||||
...userConfig.historyStore,
|
||||
},
|
||||
historyStore: (() => {
|
||||
const defaultHistoryStore = DEFAULT_MEMORY_CONFIG.historyStore!;
|
||||
const historyProvider =
|
||||
userConfig.historyStore?.provider || defaultHistoryStore.provider;
|
||||
const isSqlite = historyProvider.toLowerCase() === "sqlite";
|
||||
|
||||
// Precedence: explicit historyStore.config > top-level historyDbPath > default
|
||||
return {
|
||||
...defaultHistoryStore,
|
||||
...userConfig.historyStore,
|
||||
provider: historyProvider,
|
||||
config: {
|
||||
...(isSqlite ? defaultHistoryStore.config : {}),
|
||||
...(isSqlite && userConfig.historyDbPath
|
||||
? { historyDbPath: userConfig.historyDbPath }
|
||||
: {}),
|
||||
...userConfig.historyStore?.config,
|
||||
},
|
||||
};
|
||||
})(),
|
||||
disableHistory:
|
||||
userConfig.disableHistory || DEFAULT_MEMORY_CONFIG.disableHistory,
|
||||
enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph,
|
||||
|
||||
@@ -70,20 +70,10 @@ export class Memory {
|
||||
if (this.config.disableHistory) {
|
||||
this.db = new DummyHistoryManager();
|
||||
} else {
|
||||
const defaultConfig = {
|
||||
provider: "sqlite",
|
||||
config: {
|
||||
historyDbPath: this.config.historyDbPath || ":memory:",
|
||||
},
|
||||
};
|
||||
|
||||
this.db =
|
||||
this.config.historyStore && !this.config.disableHistory
|
||||
? HistoryManagerFactory.create(
|
||||
this.config.historyStore.provider,
|
||||
this.config.historyStore,
|
||||
)
|
||||
: HistoryManagerFactory.create("sqlite", defaultConfig);
|
||||
this.db = HistoryManagerFactory.create(
|
||||
this.config.historyStore!.provider,
|
||||
this.config.historyStore!,
|
||||
);
|
||||
}
|
||||
|
||||
this.collectionName = this.config.vectorStore.config.collectionName;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { HistoryManager } from "./base";
|
||||
import { ensureSQLiteDirectory } from "../utils/sqlite";
|
||||
|
||||
export class SQLiteManager implements HistoryManager {
|
||||
private db: Database.Database;
|
||||
@@ -7,6 +8,7 @@ export class SQLiteManager implements HistoryManager {
|
||||
private stmtSelect!: Database.Statement;
|
||||
|
||||
constructor(dbPath: string) {
|
||||
ensureSQLiteDirectory(dbPath);
|
||||
this.db = new Database(dbPath);
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* Backward-compatibility tests for SQLite path handling changes.
|
||||
*
|
||||
* These tests verify that every documented and common usage pattern
|
||||
* from before the fix continues to work identically after the change.
|
||||
*/
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { ConfigManager } from "../config/manager";
|
||||
import { SQLiteManager } from "../storage/SQLiteManager";
|
||||
import { MemoryVectorStore } from "../vector_stores/memory";
|
||||
import {
|
||||
ensureSQLiteDirectory,
|
||||
getDefaultVectorStoreDbPath,
|
||||
} from "../utils/sqlite";
|
||||
|
||||
function normalize(vector: number[]): number[] {
|
||||
const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
|
||||
return vector.map((value) => value / norm);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Config merging – existing patterns must keep working
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("backward compat: ConfigManager.mergeConfig", () => {
|
||||
it("empty config returns all expected defaults", () => {
|
||||
const cfg = ConfigManager.mergeConfig({});
|
||||
|
||||
expect(cfg.version).toBe("v1.1");
|
||||
expect(cfg.embedder.provider).toBe("openai");
|
||||
expect(cfg.vectorStore.provider).toBe("memory");
|
||||
expect(cfg.vectorStore.config.collectionName).toBe("memories");
|
||||
expect(cfg.vectorStore.config.dimension).toBe(1536);
|
||||
expect(cfg.llm.provider).toBe("openai");
|
||||
expect(cfg.historyStore).toBeDefined();
|
||||
expect(cfg.historyStore!.provider).toBe("sqlite");
|
||||
expect(cfg.historyStore!.config.historyDbPath).toBe("memory.db");
|
||||
expect(cfg.disableHistory).toBe(false);
|
||||
expect(cfg.enableGraph).toBe(false);
|
||||
});
|
||||
|
||||
it("workaround: explicit historyStore still works (existing user pattern)", () => {
|
||||
// This is the documented workaround from all three issues
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
historyStore: {
|
||||
provider: "sqlite",
|
||||
config: { historyDbPath: "/tmp/workaround.db" },
|
||||
},
|
||||
});
|
||||
expect(cfg.historyStore!.provider).toBe("sqlite");
|
||||
expect(cfg.historyStore!.config.historyDbPath).toBe("/tmp/workaround.db");
|
||||
});
|
||||
|
||||
it("disableHistory: true still works", () => {
|
||||
const cfg = ConfigManager.mergeConfig({ disableHistory: true });
|
||||
expect(cfg.disableHistory).toBe(true);
|
||||
});
|
||||
|
||||
it("supabase historyStore config is preserved", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
historyStore: {
|
||||
provider: "supabase",
|
||||
config: {
|
||||
supabaseUrl: "https://abc.supabase.co",
|
||||
supabaseKey: "secret-key",
|
||||
tableName: "custom_history",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(cfg.historyStore!.provider).toBe("supabase");
|
||||
expect(cfg.historyStore!.config.supabaseUrl).toBe(
|
||||
"https://abc.supabase.co",
|
||||
);
|
||||
expect(cfg.historyStore!.config.supabaseKey).toBe("secret-key");
|
||||
expect(cfg.historyStore!.config.tableName).toBe("custom_history");
|
||||
});
|
||||
|
||||
it("custom embedder, llm, vectorStore configs pass through unchanged", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
embedder: {
|
||||
provider: "ollama",
|
||||
config: { model: "nomic-embed-text", url: "http://localhost:11434" },
|
||||
},
|
||||
llm: {
|
||||
provider: "ollama",
|
||||
config: { model: "llama3.1:8b" },
|
||||
},
|
||||
vectorStore: {
|
||||
provider: "qdrant",
|
||||
config: {
|
||||
collectionName: "test",
|
||||
dimension: 768,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(cfg.embedder.provider).toBe("ollama");
|
||||
expect(cfg.embedder.config.model).toBe("nomic-embed-text");
|
||||
expect(cfg.llm.provider).toBe("ollama");
|
||||
expect(cfg.llm.config.model).toBe("llama3.1:8b");
|
||||
expect(cfg.vectorStore.provider).toBe("qdrant");
|
||||
expect(cfg.vectorStore.config.collectionName).toBe("test");
|
||||
expect(cfg.vectorStore.config.dimension).toBe(768);
|
||||
});
|
||||
|
||||
it("graphStore config passes through unchanged", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
enableGraph: true,
|
||||
graphStore: {
|
||||
provider: "neo4j",
|
||||
config: {
|
||||
url: "neo4j://custom:7687",
|
||||
username: "admin",
|
||||
password: "pass",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(cfg.enableGraph).toBe(true);
|
||||
expect(cfg.graphStore!.config.url).toBe("neo4j://custom:7687");
|
||||
});
|
||||
|
||||
it("customPrompt passes through unchanged", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
customPrompt: "You are a helpful assistant",
|
||||
});
|
||||
expect(cfg.customPrompt).toBe("You are a helpful assistant");
|
||||
});
|
||||
|
||||
it("version override passes through unchanged", () => {
|
||||
const cfg = ConfigManager.mergeConfig({ version: "v1.0" });
|
||||
expect(cfg.version).toBe("v1.0");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. SQLiteManager – existing behavior preserved
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("backward compat: SQLiteManager", () => {
|
||||
it("relative path still works (resolves from CWD)", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-"));
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
process.chdir(tempDir);
|
||||
const manager = new SQLiteManager("memory.db");
|
||||
await manager.addHistory("m1", null, "value", "ADD");
|
||||
const history = await manager.getHistory("m1");
|
||||
|
||||
expect(history).toHaveLength(1);
|
||||
expect(fs.existsSync(path.join(tempDir, "memory.db"))).toBe(true);
|
||||
manager.close();
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("absolute path still works", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-"));
|
||||
const dbPath = path.join(tempDir, "history.db");
|
||||
|
||||
try {
|
||||
const manager = new SQLiteManager(dbPath);
|
||||
await manager.addHistory("m1", null, "value", "ADD");
|
||||
expect(fs.existsSync(dbPath)).toBe(true);
|
||||
manager.close();
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it(":memory: still works", async () => {
|
||||
const manager = new SQLiteManager(":memory:");
|
||||
await manager.addHistory("m1", null, "value", "ADD");
|
||||
const history = await manager.getHistory("m1");
|
||||
expect(history).toHaveLength(1);
|
||||
manager.close();
|
||||
});
|
||||
|
||||
it("reset clears history and allows re-use", async () => {
|
||||
const manager = new SQLiteManager(":memory:");
|
||||
await manager.addHistory("m1", null, "val", "ADD");
|
||||
await manager.reset();
|
||||
const history = await manager.getHistory("m1");
|
||||
expect(history).toHaveLength(0);
|
||||
await manager.addHistory("m2", null, "new-val", "ADD");
|
||||
const history2 = await manager.getHistory("m2");
|
||||
expect(history2).toHaveLength(1);
|
||||
manager.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. MemoryVectorStore – existing API preserved
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("backward compat: MemoryVectorStore", () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("explicit dbPath still works (the existing config.dbPath feature)", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
|
||||
const dbPath = path.join(tempDir, "my_vectors.db");
|
||||
|
||||
try {
|
||||
const store = new MemoryVectorStore({ dimension: 3, dbPath });
|
||||
await store.insert(
|
||||
[normalize([1, 0, 0])],
|
||||
["id1"],
|
||||
[{ text: "hello" }],
|
||||
);
|
||||
|
||||
expect(fs.existsSync(dbPath)).toBe(true);
|
||||
|
||||
const result = await store.get("id1");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.payload.text).toBe("hello");
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("insert, search, get, update, delete, list all work", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
|
||||
const dbPath = path.join(tempDir, "test.db");
|
||||
|
||||
try {
|
||||
const store = new MemoryVectorStore({ dimension: 3, dbPath });
|
||||
const v1 = normalize([1, 0, 0]);
|
||||
const v2 = normalize([0, 1, 0]);
|
||||
|
||||
// insert
|
||||
await store.insert([v1, v2], ["a", "b"], [{ t: "a" }, { t: "b" }]);
|
||||
|
||||
// get
|
||||
const a = await store.get("a");
|
||||
expect(a!.payload.t).toBe("a");
|
||||
|
||||
// search
|
||||
const results = await store.search(v1, 2);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].id).toBe("a"); // closest to v1
|
||||
|
||||
// update
|
||||
await store.update("a", v2, { t: "updated" });
|
||||
const updated = await store.get("a");
|
||||
expect(updated!.payload.t).toBe("updated");
|
||||
|
||||
// list
|
||||
const [listed, count] = await store.list();
|
||||
expect(count).toBe(2);
|
||||
expect(listed).toHaveLength(2);
|
||||
|
||||
// delete
|
||||
await store.delete("a");
|
||||
const deleted = await store.get("a");
|
||||
expect(deleted).toBeNull();
|
||||
|
||||
// deleteCol
|
||||
await store.deleteCol();
|
||||
const [afterDrop] = await store.list();
|
||||
expect(afterDrop).toHaveLength(0);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("dimension mismatch on insert still throws", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
|
||||
const dbPath = path.join(tempDir, "test.db");
|
||||
|
||||
try {
|
||||
const store = new MemoryVectorStore({ dimension: 3, dbPath });
|
||||
await expect(
|
||||
store.insert([[1, 0]], ["id1"], [{ t: "x" }]),
|
||||
).rejects.toThrow("Vector dimension mismatch");
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("dimension mismatch on search still throws", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
|
||||
const dbPath = path.join(tempDir, "test.db");
|
||||
|
||||
try {
|
||||
const store = new MemoryVectorStore({ dimension: 3, dbPath });
|
||||
await expect(store.search([1, 0], 1)).rejects.toThrow(
|
||||
"Query dimension mismatch",
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("default dimension is 1536 when not specified", () => {
|
||||
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
|
||||
try {
|
||||
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
|
||||
const store = new MemoryVectorStore({});
|
||||
// Verify by trying to insert a 1536-dim vector
|
||||
const vec = new Array(1536).fill(0);
|
||||
vec[0] = 1;
|
||||
expect(store.insert([vec], ["id1"], [{ t: "x" }])).resolves.not.toThrow();
|
||||
} finally {
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("search with filters still works", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
|
||||
const dbPath = path.join(tempDir, "test.db");
|
||||
|
||||
try {
|
||||
const store = new MemoryVectorStore({ dimension: 3, dbPath });
|
||||
await store.insert(
|
||||
[normalize([1, 0, 0]), normalize([0, 1, 0])],
|
||||
["a", "b"],
|
||||
[
|
||||
{ text: "hello", userId: "user1" },
|
||||
{ text: "world", userId: "user2" },
|
||||
],
|
||||
);
|
||||
|
||||
const results = await store.search(normalize([1, 0, 0]), 10, {
|
||||
userId: "user2",
|
||||
});
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe("b");
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. VectorStoreConfig type – dbPath is optional, existing configs work
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("backward compat: VectorStoreConfig type", () => {
|
||||
it("config without dbPath still works (no required field breakage)", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
vectorStore: {
|
||||
provider: "memory",
|
||||
config: { collectionName: "test", dimension: 512 },
|
||||
},
|
||||
});
|
||||
expect(cfg.vectorStore.config.dbPath).toBeUndefined();
|
||||
expect(cfg.vectorStore.config.collectionName).toBe("test");
|
||||
expect(cfg.vectorStore.config.dimension).toBe(512);
|
||||
});
|
||||
|
||||
it("config with client instance passes through unchanged", () => {
|
||||
const fakeClient = { connect: () => {} };
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
vectorStore: {
|
||||
provider: "qdrant",
|
||||
config: { client: fakeClient, dimension: 768 },
|
||||
},
|
||||
});
|
||||
expect(cfg.vectorStore.config.client).toBe(fakeClient);
|
||||
expect(cfg.vectorStore.config.dimension).toBe(768);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. ensureSQLiteDirectory – does not break existing paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("backward compat: ensureSQLiteDirectory", () => {
|
||||
it("no-ops for already existing directory", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-existing-"));
|
||||
try {
|
||||
// Should not throw even though directory already exists
|
||||
expect(() =>
|
||||
ensureSQLiteDirectory(path.join(tempDir, "test.db")),
|
||||
).not.toThrow();
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("handles path with trailing slash gracefully", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-trailing-"));
|
||||
try {
|
||||
// path.dirname of "dir/sub/" is "dir/sub", mkdirSync should handle it
|
||||
expect(() =>
|
||||
ensureSQLiteDirectory(path.join(tempDir, "sub", "test.db")),
|
||||
).not.toThrow();
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,290 @@
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { ConfigManager } from "../config/manager";
|
||||
import { SQLiteManager } from "../storage/SQLiteManager";
|
||||
import { MemoryVectorStore } from "../vector_stores/memory";
|
||||
import {
|
||||
ensureSQLiteDirectory,
|
||||
getDefaultVectorStoreDbPath,
|
||||
} from "../utils/sqlite";
|
||||
|
||||
function normalize(vector: number[]): number[] {
|
||||
const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
|
||||
return vector.map((value) => value / norm);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config merging – historyDbPath
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("ConfigManager.mergeConfig – historyDbPath handling", () => {
|
||||
it("propagates top-level historyDbPath into historyStore.config", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
historyDbPath: "/tmp/custom/history.db",
|
||||
});
|
||||
expect(cfg.historyDbPath).toBe("/tmp/custom/history.db");
|
||||
expect(cfg.historyStore?.provider).toBe("sqlite");
|
||||
expect(cfg.historyStore?.config.historyDbPath).toBe(
|
||||
"/tmp/custom/history.db",
|
||||
);
|
||||
});
|
||||
|
||||
it("explicit historyStore.config.historyDbPath takes precedence over top-level", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
historyDbPath: "/tmp/shorthand.db",
|
||||
historyStore: {
|
||||
provider: "sqlite",
|
||||
config: { historyDbPath: "/tmp/explicit.db" },
|
||||
},
|
||||
});
|
||||
expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/explicit.db");
|
||||
});
|
||||
|
||||
it("preserves default memory.db when nothing is provided", () => {
|
||||
const cfg = ConfigManager.mergeConfig({});
|
||||
expect(cfg.historyStore?.provider).toBe("sqlite");
|
||||
expect(cfg.historyStore?.config.historyDbPath).toBe("memory.db");
|
||||
});
|
||||
|
||||
it("respects only historyStore.config when top-level is absent", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
historyStore: {
|
||||
provider: "sqlite",
|
||||
config: { historyDbPath: "/tmp/nested-only.db" },
|
||||
},
|
||||
});
|
||||
expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/nested-only.db");
|
||||
});
|
||||
|
||||
it("does not leak historyDbPath into non-sqlite providers", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
historyDbPath: "/tmp/should-not-apply.db",
|
||||
historyStore: {
|
||||
provider: "supabase",
|
||||
config: {
|
||||
supabaseUrl: "https://x.supabase.co",
|
||||
supabaseKey: "key",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(cfg.historyStore?.provider).toBe("supabase");
|
||||
expect(cfg.historyStore?.config.historyDbPath).toBeUndefined();
|
||||
});
|
||||
|
||||
it("disableHistory does not prevent historyStore config from merging", () => {
|
||||
const cfg = ConfigManager.mergeConfig({
|
||||
disableHistory: true,
|
||||
historyDbPath: "/tmp/disabled.db",
|
||||
});
|
||||
expect(cfg.disableHistory).toBe(true);
|
||||
expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/disabled.db");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SQLiteManager – directory creation & DB operations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("SQLiteManager – directory auto-creation", () => {
|
||||
it("creates nested parent directories and writes to the DB", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-sqlite-"));
|
||||
const dbPath = path.join(tempDir, "a", "b", "c", "history.db");
|
||||
let manager: SQLiteManager | undefined;
|
||||
|
||||
try {
|
||||
manager = new SQLiteManager(dbPath);
|
||||
await manager.addHistory("mem-1", null, "test value", "ADD");
|
||||
const history = await manager.getHistory("mem-1");
|
||||
|
||||
expect(fs.existsSync(dbPath)).toBe(true);
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0].new_value).toBe("test value");
|
||||
} finally {
|
||||
manager?.close();
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("end-to-end: mergeConfig + SQLiteManager at configured path", async () => {
|
||||
const tempDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "mem0-history-path-"),
|
||||
);
|
||||
const historyDbPath = path.join(tempDir, "nested", "history.db");
|
||||
let manager: SQLiteManager | undefined;
|
||||
|
||||
try {
|
||||
const mergedConfig = ConfigManager.mergeConfig({ historyDbPath });
|
||||
|
||||
manager = new SQLiteManager(
|
||||
mergedConfig.historyStore!.config.historyDbPath!,
|
||||
);
|
||||
await manager.addHistory("memory-1", null, "remember me", "ADD");
|
||||
|
||||
expect(fs.existsSync(historyDbPath)).toBe(true);
|
||||
} finally {
|
||||
manager?.close();
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("works with :memory: without attempting directory creation", () => {
|
||||
const manager = new SQLiteManager(":memory:");
|
||||
expect(manager).toBeDefined();
|
||||
manager.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MemoryVectorStore – path handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("MemoryVectorStore – path handling", () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("uses ~/.mem0/vector_store.db by default", () => {
|
||||
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
|
||||
try {
|
||||
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
|
||||
new MemoryVectorStore({ dimension: 4 });
|
||||
expect(
|
||||
fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("respects explicit dbPath config", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-vs-"));
|
||||
const dbPath = path.join(tempDir, "custom", "vectors.db");
|
||||
|
||||
try {
|
||||
const store = new MemoryVectorStore({ dimension: 4, dbPath });
|
||||
await store.insert(
|
||||
[normalize([1, 0, 0, 0])],
|
||||
["v1"],
|
||||
[{ text: "hello" }],
|
||||
);
|
||||
|
||||
expect(fs.existsSync(dbPath)).toBe(true);
|
||||
const results = await store.search(normalize([1, 0, 0, 0]), 1);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].payload.text).toBe("hello");
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("works when CWD is read-only", async () => {
|
||||
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
|
||||
const readOnlyCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-ro-"));
|
||||
|
||||
try {
|
||||
fs.chmodSync(readOnlyCwd, 0o555);
|
||||
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
|
||||
process.chdir(readOnlyCwd);
|
||||
|
||||
const store = new MemoryVectorStore({ dimension: 4 });
|
||||
await store.insert(
|
||||
[normalize([0, 1, 0, 0])],
|
||||
["v2"],
|
||||
[{ text: "works" }],
|
||||
);
|
||||
|
||||
expect(
|
||||
fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(readOnlyCwd, "vector_store.db")),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
fs.chmodSync(readOnlyCwd, 0o755);
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
fs.rmSync(readOnlyCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("emits migration warning when old CWD-based vector_store.db exists", () => {
|
||||
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
|
||||
const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-cwd-"));
|
||||
|
||||
try {
|
||||
fs.writeFileSync(path.join(tempCwd, "vector_store.db"), "");
|
||||
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
|
||||
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
process.chdir(tempCwd);
|
||||
|
||||
new MemoryVectorStore({ dimension: 4 });
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Default vector_store.db location changed"),
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||
fs.rmSync(tempCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does NOT emit migration warning when dbPath is explicitly set", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-vs-"));
|
||||
const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-cwd-"));
|
||||
|
||||
try {
|
||||
fs.writeFileSync(path.join(tempCwd, "vector_store.db"), "");
|
||||
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
process.chdir(tempCwd);
|
||||
|
||||
new MemoryVectorStore({
|
||||
dimension: 4,
|
||||
dbPath: path.join(tempDir, "explicit.db"),
|
||||
});
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
fs.rmSync(tempCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utils
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("ensureSQLiteDirectory", () => {
|
||||
it("creates nested directories", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-ensure-"));
|
||||
const target = path.join(tempDir, "x", "y", "z", "test.db");
|
||||
try {
|
||||
ensureSQLiteDirectory(target);
|
||||
expect(fs.existsSync(path.join(tempDir, "x", "y", "z"))).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("skips :memory:", () => {
|
||||
expect(() => ensureSQLiteDirectory(":memory:")).not.toThrow();
|
||||
});
|
||||
|
||||
it("skips file: URIs", () => {
|
||||
expect(() => ensureSQLiteDirectory("file::memory:")).not.toThrow();
|
||||
});
|
||||
|
||||
it("skips empty string", () => {
|
||||
expect(() => ensureSQLiteDirectory("")).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultVectorStoreDbPath", () => {
|
||||
it("returns path under homedir/.mem0", () => {
|
||||
const result = getDefaultVectorStoreDbPath();
|
||||
expect(result).toBe(path.join(os.homedir(), ".mem0", "vector_store.db"));
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,7 @@ export interface EmbeddingConfig {
|
||||
export interface VectorStoreConfig {
|
||||
collectionName?: string;
|
||||
dimension?: number;
|
||||
dbPath?: string;
|
||||
client?: any;
|
||||
instance?: any;
|
||||
[key: string]: any;
|
||||
@@ -129,6 +130,7 @@ export const MemoryConfigSchema = z.object({
|
||||
.object({
|
||||
collectionName: z.string().optional(),
|
||||
dimension: z.number().optional(),
|
||||
dbPath: z.string().optional(),
|
||||
client: z.any().optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
export function getDefaultVectorStoreDbPath(): string {
|
||||
return path.join(os.homedir(), ".mem0", "vector_store.db");
|
||||
}
|
||||
|
||||
export function ensureSQLiteDirectory(dbPath: string): void {
|
||||
if (!dbPath || dbPath === ":memory:" || dbPath.startsWith("file:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
import Database from "better-sqlite3";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
ensureSQLiteDirectory,
|
||||
getDefaultVectorStoreDbPath,
|
||||
} from "../utils/sqlite";
|
||||
|
||||
interface MemoryVector {
|
||||
id: string;
|
||||
@@ -16,10 +21,19 @@ export class MemoryVectorStore implements VectorStore {
|
||||
|
||||
constructor(config: VectorStoreConfig) {
|
||||
this.dimension = config.dimension || 1536; // Default OpenAI dimension
|
||||
this.dbPath = path.join(process.cwd(), "vector_store.db");
|
||||
if (config.dbPath) {
|
||||
this.dbPath = config.dbPath;
|
||||
this.dbPath = config.dbPath || getDefaultVectorStoreDbPath();
|
||||
|
||||
if (!config.dbPath) {
|
||||
const oldDefault = path.join(process.cwd(), "vector_store.db");
|
||||
if (fs.existsSync(oldDefault) && oldDefault !== this.dbPath) {
|
||||
console.warn(
|
||||
`[mem0] Default vector_store.db location changed from ${oldDefault} to ${this.dbPath}. ` +
|
||||
`Move your existing file or set vectorStore.config.dbPath explicitly.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ensureSQLiteDirectory(this.dbPath);
|
||||
this.db = new Database(this.dbPath);
|
||||
this.init();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user