diff --git a/src/memory/search-manager.test.ts b/src/memory/search-manager.test.ts index 991dbe619c..0b352bff20 100644 --- a/src/memory/search-manager.test.ts +++ b/src/memory/search-manager.test.ts @@ -22,6 +22,37 @@ const mockPrimary = { close: vi.fn(async () => {}), }; +const fallbackSearch = vi.fn(async () => [ + { + path: "MEMORY.md", + startLine: 1, + endLine: 1, + score: 1, + snippet: "fallback", + source: "memory" as const, + }, +]); + +const fallbackManager = { + search: fallbackSearch, + readFile: vi.fn(async () => ({ text: "", path: "MEMORY.md" })), + status: vi.fn(() => ({ + backend: "builtin" as const, + provider: "openai", + model: "text-embedding-3-small", + requestedProvider: "openai", + files: 0, + chunks: 0, + dirty: false, + workspaceDir: "/tmp", + dbPath: "/tmp/index.sqlite", + })), + sync: vi.fn(async () => {}), + probeEmbeddingAvailability: vi.fn(async () => ({ ok: true })), + probeVectorAvailability: vi.fn(async () => true), + close: vi.fn(async () => {}), +}; + vi.mock("./qmd-manager.js", () => ({ QmdMemoryManager: { create: vi.fn(async () => mockPrimary), @@ -30,34 +61,7 @@ vi.mock("./qmd-manager.js", () => ({ vi.mock("./manager.js", () => ({ MemoryIndexManager: { - get: vi.fn(async () => ({ - search: vi.fn(async () => [ - { - path: "MEMORY.md", - startLine: 1, - endLine: 1, - score: 1, - snippet: "fallback", - source: "memory", - }, - ]), - readFile: vi.fn(async () => ({ text: "", path: "MEMORY.md" })), - status: vi.fn(() => ({ - backend: "builtin" as const, - provider: "openai", - model: "text-embedding-3-small", - requestedProvider: "openai", - files: 0, - chunks: 0, - dirty: false, - workspaceDir: "/tmp", - dbPath: "/tmp/index.sqlite", - })), - sync: vi.fn(async () => {}), - probeEmbeddingAvailability: vi.fn(async () => ({ ok: true })), - probeVectorAvailability: vi.fn(async () => true), - close: vi.fn(async () => {}), - })), + get: vi.fn(async () => fallbackManager), }, })); @@ -72,6 +76,13 @@ beforeEach(() => { mockPrimary.probeEmbeddingAvailability.mockClear(); mockPrimary.probeVectorAvailability.mockClear(); mockPrimary.close.mockClear(); + fallbackSearch.mockClear(); + fallbackManager.readFile.mockClear(); + fallbackManager.status.mockClear(); + fallbackManager.sync.mockClear(); + fallbackManager.probeEmbeddingAvailability.mockClear(); + fallbackManager.probeVectorAvailability.mockClear(); + fallbackManager.close.mockClear(); QmdMemoryManager.create.mockClear(); }); @@ -145,4 +156,27 @@ describe("getMemorySearchManager caching", () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(QmdMemoryManager.create).toHaveBeenCalledTimes(2); }); + + it("falls back to builtin search when qmd fails with sqlite busy", async () => { + const retryAgentId = "retry-agent-busy"; + const cfg = { + memory: { backend: "qmd", qmd: {} }, + agents: { list: [{ id: retryAgentId, default: true, workspace: "/tmp/workspace" }] }, + } as const; + + mockPrimary.search.mockRejectedValueOnce( + new Error("qmd index busy while reading results: SQLITE_BUSY: database is locked"), + ); + + const first = await getMemorySearchManager({ cfg, agentId: retryAgentId }); + expect(first.manager).toBeTruthy(); + if (!first.manager) { + throw new Error("manager missing"); + } + + const results = await first.manager.search("hello"); + expect(results).toHaveLength(1); + expect(results[0]?.path).toBe("MEMORY.md"); + expect(fallbackSearch).toHaveBeenCalledTimes(1); + }); });