mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 21:14:31 +00:00
fix(memory): harden qmd collection recovery
This commit is contained in:
@@ -496,6 +496,65 @@ describe("QmdMemoryManager", () => {
|
||||
expect(legacyCollections.has("memory-dir")).toBe(false);
|
||||
});
|
||||
|
||||
it("migrates unscoped legacy collections from plain-text collection list output", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: true,
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
paths: [],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const removeCalls: string[] = [];
|
||||
const addCalls: string[] = [];
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "collection" && args[1] === "list") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(
|
||||
child,
|
||||
"stdout",
|
||||
[
|
||||
"Collections (3):",
|
||||
"",
|
||||
"memory-root (qmd://memory-root/)",
|
||||
" Pattern: MEMORY.md",
|
||||
"",
|
||||
"memory-alt (qmd://memory-alt/)",
|
||||
" Pattern: memory.md",
|
||||
"",
|
||||
"memory-dir (qmd://memory-dir/)",
|
||||
" Pattern: **/*.md",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
return child;
|
||||
}
|
||||
if (args[0] === "collection" && args[1] === "remove") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
removeCalls.push(args[2] ?? "");
|
||||
queueMicrotask(() => child.closeWith(0));
|
||||
return child;
|
||||
}
|
||||
if (args[0] === "collection" && args[1] === "add") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
addCalls.push(args[args.indexOf("--name") + 1] ?? "");
|
||||
queueMicrotask(() => child.closeWith(0));
|
||||
return child;
|
||||
}
|
||||
return createMockChild();
|
||||
});
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
await manager.close();
|
||||
|
||||
expect(removeCalls).toEqual(["memory-root", "memory-alt", "memory-dir"]);
|
||||
expect(addCalls).toEqual(["memory-root-main", "memory-alt-main", "memory-dir-main"]);
|
||||
});
|
||||
|
||||
it("does not migrate unscoped collections when listed metadata differs", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
@@ -729,6 +788,96 @@ describe("QmdMemoryManager", () => {
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("repairs missing managed collections and retries search once", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: true,
|
||||
searchMode: "search",
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
paths: [],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const expectedDocId = "abc123";
|
||||
let missingCollectionSeen = false;
|
||||
let addCallsAfterMissing = 0;
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "collection" && args[1] === "list") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
}
|
||||
if (args[0] === "collection" && args[1] === "add") {
|
||||
if (missingCollectionSeen) {
|
||||
addCallsAfterMissing += 1;
|
||||
}
|
||||
return createMockChild();
|
||||
}
|
||||
if (args[0] === "search") {
|
||||
const collectionFlagIndex = args.indexOf("-c");
|
||||
const collection = collectionFlagIndex >= 0 ? args[collectionFlagIndex + 1] : "";
|
||||
if (collection === "memory-root-main" && !missingCollectionSeen) {
|
||||
missingCollectionSeen = true;
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(child, "stderr", "Collection not found: memory-root-main", 1);
|
||||
return child;
|
||||
}
|
||||
if (collection === "memory-root-main") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(
|
||||
child,
|
||||
"stdout",
|
||||
JSON.stringify([{ docid: expectedDocId, score: 1, snippet: "@@ -1,1\nremember this" }]),
|
||||
);
|
||||
return child;
|
||||
}
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
}
|
||||
return createMockChild();
|
||||
});
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
const inner = manager as unknown as {
|
||||
db: { prepare: (query: string) => { all: (arg: unknown) => unknown }; close: () => void };
|
||||
};
|
||||
inner.db = {
|
||||
prepare: (_query: string) => ({
|
||||
all: (arg: unknown) => {
|
||||
if (typeof arg === "string" && arg.startsWith(expectedDocId)) {
|
||||
return [{ collection: "memory-root-main", path: "MEMORY.md" }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
close: () => {},
|
||||
};
|
||||
|
||||
await expect(
|
||||
manager.search("remember", { sessionKey: "agent:main:slack:dm:u123" }),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 1,
|
||||
snippet: "@@ -1,1\nremember this",
|
||||
source: "memory",
|
||||
},
|
||||
]);
|
||||
expect(addCallsAfterMissing).toBeGreaterThan(0);
|
||||
expect(logWarnMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("repairing collections and retrying once"),
|
||||
);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("uses qmd.cmd on Windows when qmd command is bare", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user