mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 06:14:34 +00:00
feat(memory): allow QMD searches via mcporter keep-alive (openclaw#19617) thanks @vignesh07
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -148,6 +148,8 @@ describe("QmdMemoryManager", () => {
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
delete (globalThis as Record<string, unknown>).__openclawMcporterDaemonStart;
|
||||
delete (globalThis as Record<string, unknown>).__openclawMcporterColdStartWarned;
|
||||
});
|
||||
|
||||
it("debounces back-to-back sync calls", async () => {
|
||||
@@ -910,6 +912,170 @@ describe("QmdMemoryManager", () => {
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("runs qmd searches via mcporter and warns when startDaemon=false", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
mcporter: { enabled: true, serverName: "qmd", startDaemon: false },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
spawnMock.mockImplementation((cmd: string, args: string[]) => {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
if (cmd === "mcporter" && args[0] === "call") {
|
||||
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
|
||||
logWarnMock.mockClear();
|
||||
await expect(
|
||||
manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" }),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
const mcporterCalls = spawnMock.mock.calls.filter((call: unknown[]) => call[0] === "mcporter");
|
||||
expect(mcporterCalls.length).toBeGreaterThan(0);
|
||||
expect(mcporterCalls.some((call: unknown[]) => (call[1] as string[])[0] === "daemon")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(logWarnMock).toHaveBeenCalledWith(expect.stringContaining("cold-start"));
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("passes manager-scoped XDG env to mcporter commands", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
mcporter: { enabled: true, serverName: "qmd", startDaemon: false },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
spawnMock.mockImplementation((cmd: string, args: string[]) => {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
if (cmd === "mcporter" && args[0] === "call") {
|
||||
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
await manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
|
||||
const mcporterCall = spawnMock.mock.calls.find(
|
||||
(call: unknown[]) => call[0] === "mcporter" && (call[1] as string[])[0] === "call",
|
||||
);
|
||||
expect(mcporterCall).toBeDefined();
|
||||
const spawnOpts = mcporterCall?.[2] as { env?: NodeJS.ProcessEnv } | undefined;
|
||||
expect(spawnOpts?.env?.XDG_CONFIG_HOME).toContain("/agents/main/qmd/xdg-config");
|
||||
expect(spawnOpts?.env?.XDG_CACHE_HOME).toContain("/agents/main/qmd/xdg-cache");
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("retries mcporter daemon start after a failure", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
mcporter: { enabled: true, serverName: "qmd", startDaemon: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
let daemonAttempts = 0;
|
||||
spawnMock.mockImplementation((cmd: string, args: string[]) => {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
if (cmd === "mcporter" && args[0] === "daemon") {
|
||||
daemonAttempts += 1;
|
||||
if (daemonAttempts === 1) {
|
||||
emitAndClose(child, "stderr", "failed", 1);
|
||||
} else {
|
||||
emitAndClose(child, "stdout", "");
|
||||
}
|
||||
return child;
|
||||
}
|
||||
if (cmd === "mcporter" && args[0] === "call") {
|
||||
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
|
||||
await manager.search("one", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
await manager.search("two", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
|
||||
expect(daemonAttempts).toBe(2);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("starts the mcporter daemon only once when enabled", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
mcporter: { enabled: true, serverName: "qmd", startDaemon: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
spawnMock.mockImplementation((cmd: string, args: string[]) => {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
if (cmd === "mcporter" && args[0] === "daemon") {
|
||||
emitAndClose(child, "stdout", "");
|
||||
return child;
|
||||
}
|
||||
if (cmd === "mcporter" && args[0] === "call") {
|
||||
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
|
||||
return child;
|
||||
}
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
|
||||
await manager.search("one", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
await manager.search("two", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
|
||||
const daemonStarts = spawnMock.mock.calls.filter(
|
||||
(call: unknown[]) => call[0] === "mcporter" && (call[1] as string[])[0] === "daemon",
|
||||
);
|
||||
expect(daemonStarts).toHaveLength(1);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("fails closed when no managed collections are configured", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
|
||||
Reference in New Issue
Block a user