From 110cc5d79113c8703ae5af0214d1a03963b4251b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 22:36:13 +0000 Subject: [PATCH] perf(test): speed up memory index suite --- src/memory/index.test.ts | 150 +++++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 68 deletions(-) diff --git a/src/memory/index.test.ts b/src/memory/index.test.ts index f0cca4781d1..ba31067a2de 100644 --- a/src/memory/index.test.ts +++ b/src/memory/index.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; let embedBatchCalls = 0; @@ -43,16 +43,33 @@ vi.mock("./embeddings.js", () => { describe("memory index", () => { let fixtureRoot = ""; - let fixtureCount = 0; - let workspaceDir: string; - let indexPath: string; - let manager: MemoryIndexManager | null = null; + let workspaceDir = ""; + let memoryDir = ""; + let extraDir = ""; + let indexBasicPath = ""; + let indexCachePath = ""; + let indexHybridPath = ""; + let indexVectorPath = ""; + let indexExtraPath = ""; + + const persistentManagers = new Set(); beforeAll(async () => { fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-fixtures-")); + workspaceDir = path.join(fixtureRoot, "workspace"); + memoryDir = path.join(workspaceDir, "memory"); + extraDir = path.join(workspaceDir, "extra"); + indexBasicPath = path.join(workspaceDir, "index-basic.sqlite"); + indexCachePath = path.join(workspaceDir, "index-cache.sqlite"); + indexHybridPath = path.join(workspaceDir, "index-hybrid.sqlite"); + indexVectorPath = path.join(workspaceDir, "index-vector.sqlite"); + indexExtraPath = path.join(workspaceDir, "index-extra.sqlite"); + + await fs.mkdir(memoryDir, { recursive: true }); }); afterAll(async () => { + await Promise.all(Array.from(persistentManagers).map((manager) => manager.close())); await fs.rm(fixtureRoot, { recursive: true, force: true }); }); @@ -61,23 +78,46 @@ describe("memory index", () => { // Keep atomic reindex tests on the safe path. vi.stubEnv("OPENCLAW_TEST_MEMORY_UNSAFE_REINDEX", "1"); embedBatchCalls = 0; - workspaceDir = path.join(fixtureRoot, `case-${fixtureCount++}`); - await fs.mkdir(workspaceDir, { recursive: true }); - indexPath = path.join(workspaceDir, "index.sqlite"); - await fs.mkdir(path.join(workspaceDir, "memory")); + + // Keep the workspace stable to allow manager reuse across tests. + await fs.mkdir(memoryDir, { recursive: true }); await fs.writeFile( - path.join(workspaceDir, "memory", "2026-01-12.md"), + path.join(memoryDir, "2026-01-12.md"), "# Log\nAlpha memory line.\nZebra memory line.", ); - }); - afterEach(async () => { - if (manager) { - await manager.close(); - manager = null; + // Clean additional paths that may have been created by earlier cases. + await fs.rm(extraDir, { recursive: true, force: true }); + + for (const manager of persistentManagers) { + resetManagerForTest(manager); } }); + function resetManagerForTest(manager: MemoryIndexManager) { + // These tests reuse managers for performance. Clear the index + embedding + // cache to keep each test fully isolated. + (manager as unknown as { resetIndex: () => void }).resetIndex(); + (manager as unknown as { db: { exec: (sql: string) => void } }).db.exec( + "DELETE FROM embedding_cache", + ); + (manager as unknown as { dirty: boolean }).dirty = true; + (manager as unknown as { sessionsDirty: boolean }).sessionsDirty = false; + } + + type TestCfg = Parameters[0]["cfg"]; + + async function getPersistentManager(cfg: TestCfg): Promise { + const result = await getMemorySearchManager({ cfg, agentId: "main" }); + expect(result.manager).not.toBeNull(); + if (!result.manager) { + throw new Error("manager missing"); + } + const manager = result.manager as MemoryIndexManager; + persistentManagers.add(manager); + return manager; + } + it("indexes memory files and searches by vector", async () => { const cfg = { agents: { @@ -86,7 +126,7 @@ describe("memory index", () => { memorySearch: { provider: "openai", model: "mock-embed", - store: { path: indexPath, vector: { enabled: false } }, + store: { path: indexBasicPath, vector: { enabled: false } }, sync: { watch: false, onSessionStart: false, onSearch: true }, query: { minScore: 0, hybrid: { enabled: false } }, }, @@ -94,17 +134,12 @@ describe("memory index", () => { list: [{ id: "main", default: true }], }, }; - const result = await getMemorySearchManager({ cfg, agentId: "main" }); - expect(result.manager).not.toBeNull(); - if (!result.manager) { - throw new Error("manager missing"); - } - manager = result.manager; - await result.manager.sync({ reason: "test" }); - const results = await result.manager.search("alpha"); + const manager = await getPersistentManager(cfg); + await manager.sync({ reason: "test" }); + const results = await manager.search("alpha"); expect(results.length).toBeGreaterThan(0); expect(results[0]?.path).toContain("memory/2026-01-12.md"); - const status = result.manager.status(); + const status = manager.status(); expect(status.sourceCounts).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -117,13 +152,18 @@ describe("memory index", () => { }); it("reindexes when the embedding model changes", async () => { + const indexModelPath = path.join(workspaceDir, "index-model-change.sqlite"); + await fs.rm(indexModelPath, { force: true }); + await fs.rm(`${indexModelPath}-shm`, { force: true }); + await fs.rm(`${indexModelPath}-wal`, { force: true }); + const base = { agents: { defaults: { workspace: workspaceDir, memorySearch: { provider: "openai", - store: { path: indexPath }, + store: { path: indexModelPath }, sync: { watch: false, onSessionStart: false, onSearch: true }, query: { minScore: 0, hybrid: { enabled: false } }, }, @@ -176,11 +216,11 @@ describe("memory index", () => { if (!second.manager) { throw new Error("manager missing"); } - manager = second.manager; await second.manager.sync({ reason: "test" }); expect(embedBatchCalls).toBeGreaterThan(callsAfterFirstSync); const status = second.manager.status(); expect(status.files).toBeGreaterThan(0); + await second.manager.close(); }); it("reuses cached embeddings on forced reindex", async () => { @@ -191,7 +231,7 @@ describe("memory index", () => { memorySearch: { provider: "openai", model: "mock-embed", - store: { path: indexPath, vector: { enabled: false } }, + store: { path: indexCachePath, vector: { enabled: false } }, sync: { watch: false, onSessionStart: false, onSearch: false }, query: { minScore: 0, hybrid: { enabled: false } }, cache: { enabled: true }, @@ -200,12 +240,7 @@ describe("memory index", () => { list: [{ id: "main", default: true }], }, }; - const result = await getMemorySearchManager({ cfg, agentId: "main" }); - expect(result.manager).not.toBeNull(); - if (!result.manager) { - throw new Error("manager missing"); - } - manager = result.manager; + const manager = await getPersistentManager(cfg); await manager.sync({ force: true }); const afterFirst = embedBatchCalls; expect(afterFirst).toBeGreaterThan(0); @@ -222,7 +257,7 @@ describe("memory index", () => { memorySearch: { provider: "openai", model: "mock-embed", - store: { path: indexPath, vector: { enabled: false } }, + store: { path: indexHybridPath, vector: { enabled: false } }, sync: { watch: false, onSessionStart: false, onSearch: true }, query: { minScore: 0, @@ -233,12 +268,7 @@ describe("memory index", () => { list: [{ id: "main", default: true }], }, }; - const result = await getMemorySearchManager({ cfg, agentId: "main" }); - expect(result.manager).not.toBeNull(); - if (!result.manager) { - throw new Error("manager missing"); - } - manager = result.manager; + const manager = await getPersistentManager(cfg); const status = manager.status(); if (!status.fts?.available) { @@ -259,21 +289,16 @@ describe("memory index", () => { memorySearch: { provider: "openai", model: "mock-embed", - store: { path: indexPath }, + store: { path: indexVectorPath, vector: { enabled: true } }, sync: { watch: false, onSessionStart: false, onSearch: false }, }, }, list: [{ id: "main", default: true }], }, }; - const result = await getMemorySearchManager({ cfg, agentId: "main" }); - expect(result.manager).not.toBeNull(); - if (!result.manager) { - throw new Error("manager missing"); - } - manager = result.manager; - const available = await result.manager.probeVectorAvailability(); - const status = result.manager.status(); + const manager = await getPersistentManager(cfg); + const available = await manager.probeVectorAvailability(); + const status = manager.status(); expect(status.vector?.enabled).toBe(true); expect(typeof status.vector?.available).toBe("boolean"); expect(status.vector?.available).toBe(available); @@ -287,7 +312,7 @@ describe("memory index", () => { memorySearch: { provider: "openai", model: "mock-embed", - store: { path: indexPath, vector: { enabled: false } }, + store: { path: indexBasicPath, vector: { enabled: false } }, sync: { watch: false, onSessionStart: false, onSearch: true }, query: { minScore: 0, hybrid: { enabled: false } }, }, @@ -295,17 +320,11 @@ describe("memory index", () => { list: [{ id: "main", default: true }], }, }; - const result = await getMemorySearchManager({ cfg, agentId: "main" }); - expect(result.manager).not.toBeNull(); - if (!result.manager) { - throw new Error("manager missing"); - } - manager = result.manager; - await expect(result.manager.readFile({ relPath: "NOTES.md" })).rejects.toThrow("path required"); + const manager = await getPersistentManager(cfg); + await expect(manager.readFile({ relPath: "NOTES.md" })).rejects.toThrow("path required"); }); it("allows reading from additional memory paths and blocks symlinks", async () => { - const extraDir = path.join(workspaceDir, "extra"); await fs.mkdir(extraDir, { recursive: true }); await fs.writeFile(path.join(extraDir, "extra.md"), "Extra content."); @@ -316,7 +335,7 @@ describe("memory index", () => { memorySearch: { provider: "openai", model: "mock-embed", - store: { path: indexPath, vector: { enabled: false } }, + store: { path: indexExtraPath, vector: { enabled: false } }, sync: { watch: false, onSessionStart: false, onSearch: true }, query: { minScore: 0, hybrid: { enabled: false } }, extraPaths: [extraDir], @@ -325,13 +344,8 @@ describe("memory index", () => { list: [{ id: "main", default: true }], }, }; - const result = await getMemorySearchManager({ cfg, agentId: "main" }); - expect(result.manager).not.toBeNull(); - if (!result.manager) { - throw new Error("manager missing"); - } - manager = result.manager; - await expect(result.manager.readFile({ relPath: "extra/extra.md" })).resolves.toEqual({ + const manager = await getPersistentManager(cfg); + await expect(manager.readFile({ relPath: "extra/extra.md" })).resolves.toEqual({ path: "extra/extra.md", text: "Extra content.", }); @@ -349,7 +363,7 @@ describe("memory index", () => { } } if (symlinkOk) { - await expect(result.manager.readFile({ relPath: "extra/linked.md" })).rejects.toThrow( + await expect(manager.readFile({ relPath: "extra/linked.md" })).rejects.toThrow( "path required", ); }