@@ -2809,6 +2809,71 @@ describe("QmdMemoryManager", () => {
|
2809 | 2809 | await manager.close(); |
2810 | 2810 | }); |
2811 | 2811 | |
| 2812 | +it("aborts the in-flight grouped qmd search subprocess when the caller signal aborts", async () => { |
| 2813 | +cfg = { |
| 2814 | + ...cfg, |
| 2815 | +memory: { |
| 2816 | +backend: "qmd", |
| 2817 | +qmd: { |
| 2818 | +includeDefaultMemory: false, |
| 2819 | +update: { interval: "0s", debounceMs: 60_000, onBoot: false }, |
| 2820 | +sessions: { enabled: true }, |
| 2821 | +paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }], |
| 2822 | +}, |
| 2823 | +}, |
| 2824 | +} as OpenClawConfig; |
| 2825 | + |
| 2826 | +// Mixed memory/session sources route the search through |
| 2827 | +// runQueryAcrossCollectionGroups. The first grouped search child never |
| 2828 | +// closes on its own, so the only way the search can settle is the |
| 2829 | +// caller-owned abort signal reaching the grouped subprocess and killing it. |
| 2830 | +let groupedChildKill: ReturnType<typeof vi.fn> | undefined; |
| 2831 | +let groupedChild: MockChild | undefined; |
| 2832 | +spawnMock.mockImplementation((_cmd: string, args: string[]) => { |
| 2833 | +if (args[0] === "--help") { |
| 2834 | +const child = createMockChild({ autoClose: false }); |
| 2835 | +emitAndClose( |
| 2836 | +child, |
| 2837 | +"stdout", |
| 2838 | +"-c, --collection <name> Filter by one or more collections", |
| 2839 | +); |
| 2840 | +return child; |
| 2841 | +} |
| 2842 | +if (args[0] === "search") { |
| 2843 | +const child = createMockChild({ autoClose: false }); |
| 2844 | +const kill = vi.fn(() => { |
| 2845 | +// Mirror a real child exiting after SIGKILL so the close handler runs. |
| 2846 | +queueMicrotask(() => child.emit("close", null)); |
| 2847 | +}); |
| 2848 | +Object.assign(child, { kill }); |
| 2849 | +if (!groupedChildKill) { |
| 2850 | +groupedChildKill = kill; |
| 2851 | +groupedChild = child; |
| 2852 | +} |
| 2853 | +return child; |
| 2854 | +} |
| 2855 | +return createMockChild(); |
| 2856 | +}); |
| 2857 | + |
| 2858 | +const { manager } = await createManager(); |
| 2859 | +const controller = new AbortController(); |
| 2860 | + |
| 2861 | +const searchPromise = manager.search("test", { |
| 2862 | +sessionKey: "agent:main:slack:dm:u123", |
| 2863 | +signal: controller.signal, |
| 2864 | +}); |
| 2865 | +searchPromise.catch(() => undefined); |
| 2866 | + |
| 2867 | +await waitUntil(() => groupedChildKill !== undefined); |
| 2868 | +expect(groupedChild).toBeDefined(); |
| 2869 | + |
| 2870 | +controller.abort(new Error("memory_search timed out after 15s")); |
| 2871 | + |
| 2872 | +await expect(searchPromise).rejects.toThrow("memory_search timed out after 15s"); |
| 2873 | +expect(groupedChildKill).toHaveBeenCalledWith("SIGKILL"); |
| 2874 | +await manager.close(); |
| 2875 | +}); |
| 2876 | + |
2812 | 2877 | it("does not pass --no-rerank to direct query fallback from search mode", async () => { |
2813 | 2878 | cfg = { |
2814 | 2879 | ...cfg, |
|