test(ts-sdk): expand sqlite path tests and fix non-sqlite config leak

Prevent default sqlite historyDbPath from leaking into non-sqlite
providers during config merging. Consolidate and expand test coverage
to 19 tests: config precedence, non-sqlite isolation, migration
warning, explicit dbPath, read-only CWD, and utility edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
utkarsh240799
2026-03-12 16:15:19 +05:30
parent 7a3bc22161
commit ec3eedbcb0
2 changed files with 233 additions and 67 deletions
+3 -3
View File
@@ -107,6 +107,7 @@ export class ConfigManager {
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 {
@@ -114,9 +115,8 @@ export class ConfigManager {
...userConfig.historyStore,
provider: historyProvider,
config: {
...defaultHistoryStore.config,
...(historyProvider.toLowerCase() === "sqlite" &&
userConfig.historyDbPath
...(isSqlite ? defaultHistoryStore.config : {}),
...(isSqlite && userConfig.historyDbPath
? { historyDbPath: userConfig.historyDbPath }
: {}),
...userConfig.historyStore?.config,
@@ -4,21 +4,109 @@ 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);
}
describe("SQLite path resolution", () => {
const originalCwd = process.cwd();
// ---------------------------------------------------------------------------
// Config merging – historyDbPath
// ---------------------------------------------------------------------------
afterEach(() => {
process.chdir(originalCwd);
jest.restoreAllMocks();
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("propagates top-level historyDbPath into the sqlite history store config", async () => {
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-"),
);
@@ -28,13 +116,6 @@ describe("SQLite path resolution", () => {
try {
const mergedConfig = ConfigManager.mergeConfig({ historyDbPath });
expect(mergedConfig.historyDbPath).toBe(historyDbPath);
expect(mergedConfig.historyStore?.provider).toBe("sqlite");
expect(mergedConfig.historyStore?.config.historyDbPath).toBe(
historyDbPath,
);
// Verify the DB is actually created at the configured path
manager = new SQLiteManager(
mergedConfig.historyStore!.config.historyDbPath!,
);
@@ -47,78 +128,163 @@ describe("SQLite path resolution", () => {
}
});
it("explicit historyStore.config.historyDbPath takes precedence over top-level historyDbPath", () => {
const mergedConfig = ConfigManager.mergeConfig({
historyDbPath: "/path/from/shorthand.db",
historyStore: {
provider: "sqlite",
config: { historyDbPath: "/path/from/explicit.db" },
},
});
it("works with :memory: without attempting directory creation", () => {
const manager = new SQLiteManager(":memory:");
expect(manager).toBeDefined();
manager.close();
});
});
expect(mergedConfig.historyStore?.config.historyDbPath).toBe(
"/path/from/explicit.db",
);
// ---------------------------------------------------------------------------
// MemoryVectorStore – path handling
// ---------------------------------------------------------------------------
describe("MemoryVectorStore – path handling", () => {
const originalCwd = process.cwd();
afterEach(() => {
process.chdir(originalCwd);
jest.restoreAllMocks();
});
it("uses default memory.db when neither historyDbPath nor historyStore is provided", () => {
const mergedConfig = ConfigManager.mergeConfig({});
expect(mergedConfig.historyStore?.provider).toBe("sqlite");
expect(mergedConfig.historyStore?.config.historyDbPath).toBe("memory.db");
});
it("defaults the memory vector store dbPath to ~/.mem0/vector_store.db", async () => {
const fakeHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-cwd-"));
const expectedDbPath = path.join(fakeHomeDir, ".mem0", "vector_store.db");
it("uses ~/.mem0/vector_store.db by default", () => {
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
try {
jest.spyOn(os, "homedir").mockReturnValue(fakeHomeDir);
process.chdir(tempCwd);
const store = new MemoryVectorStore({ dimension: 4 });
await store.insert(
[normalize([1, 0, 0, 0])],
["vector-1"],
[{ data: "hello" }],
);
expect(fs.existsSync(expectedDbPath)).toBe(true);
expect(fs.existsSync(path.join(tempCwd, "vector_store.db"))).toBe(false);
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(fakeHomeDir, { recursive: true, force: true });
fs.rmSync(tempCwd, { recursive: true, force: true });
fs.rmSync(fakeHome, { recursive: true, force: true });
}
});
it("does not depend on a writable current working directory for the default vector store db", async () => {
const fakeHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
const readOnlyCwd = fs.mkdtempSync(
path.join(os.tmpdir(), "mem0-readonly-cwd-"),
);
const expectedDbPath = path.join(fakeHomeDir, ".mem0", "vector_store.db");
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(fakeHomeDir);
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
process.chdir(readOnlyCwd);
const store = new MemoryVectorStore({ dimension: 4 });
await store.insert(
[normalize([0, 1, 0, 0])],
["vector-2"],
[{ data: "hello again" }],
["v2"],
[{ text: "works" }],
);
expect(fs.existsSync(expectedDbPath)).toBe(true);
expect(fs.existsSync(path.join(readOnlyCwd, "vector_store.db"))).toBe(
false,
);
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(fakeHomeDir, { recursive: true, force: true });
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"));
});
});