fix(memory): stabilize qmd collection scoping

This commit is contained in:
Altay 2026-03-30 22:41:21 +03:00
parent 9c25544e6c
commit 910134b702
4 changed files with 58 additions and 15 deletions

View File

@ -267,7 +267,7 @@ describe("QmdMemoryManager slugified path resolution", () => {
"stdout",
JSON.stringify([
{
file: "qmd://vault-main/topics/sub-category/topic-name.md",
file: "qmd://vault/topics/sub-category/topic-name.md",
score: 0.81,
snippet: "@@ -1,1\nvault memory",
},
@ -281,7 +281,7 @@ describe("QmdMemoryManager slugified path resolution", () => {
const { manager } = await createManager({ cfg });
installIndexedPathStub({
manager,
collection: "vault-main",
collection: "vault",
normalizedPath: "topics/sub-category/topic-name.md",
actualPath: actualRelative,
});
@ -291,7 +291,7 @@ describe("QmdMemoryManager slugified path resolution", () => {
});
expect(results).toEqual([
{
path: `qmd/vault-main/${actualRelative}`,
path: `qmd/vault/${actualRelative}`,
startLine: 1,
endLine: 1,
score: 0.81,
@ -301,7 +301,7 @@ describe("QmdMemoryManager slugified path resolution", () => {
]);
await expect(manager.readFile({ relPath: results[0]!.path })).resolves.toEqual({
path: `qmd/vault-main/${actualRelative}`,
path: `qmd/vault/${actualRelative}`,
text: "vault memory",
});
});

View File

@ -1742,7 +1742,7 @@ describe("QmdMemoryManager", () => {
.filter((args: string[]) => args[0] === "search");
expect(searchCalls).toEqual([
["search", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"],
["search", "test", "--json", "-n", String(maxResults), "-c", "notes"],
["search", "test", "--json", "-n", String(maxResults), "-c", "notes-main"],
]);
await manager.close();
});
@ -1828,7 +1828,7 @@ describe("QmdMemoryManager", () => {
.filter((args: string[]) => args[0] === "query");
expect(queryCalls).toEqual([
["query", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"],
["query", "test", "--json", "-n", String(maxResults), "-c", "notes"],
["query", "test", "--json", "-n", String(maxResults), "-c", "notes-main"],
]);
await manager.close();
});
@ -1880,7 +1880,7 @@ describe("QmdMemoryManager", () => {
expect(searchAndQueryCalls).toEqual([
["search", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"],
["query", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"],
["query", "test", "--json", "-n", String(maxResults), "-c", "notes"],
["query", "test", "--json", "-n", String(maxResults), "-c", "notes-main"],
]);
await manager.close();
});
@ -2217,7 +2217,7 @@ describe("QmdMemoryManager", () => {
await manager.search("hello", { sessionKey: "agent:main:slack:dm:u123" });
expect(selectors).toEqual(["qmd.hybrid_search", "qmd.hybrid_search"]);
expect(collections).toEqual(["workspace-a", "workspace-b"]);
expect(collections).toEqual(["workspace-a-main", "workspace-b-main"]);
await manager.close();
});
@ -3326,7 +3326,7 @@ describe("QmdMemoryManager", () => {
);
return child;
}
if (args[0] === "search" && args.includes("notes")) {
if (args[0] === "search" && args.includes("notes-main")) {
const child = createMockChild({ autoClose: false });
emitAndClose(child, "stdout", "[]");
return child;
@ -3542,14 +3542,14 @@ describe("QmdMemoryManager", () => {
);
return child;
}
if (args[0] === "search" && args.includes("notes")) {
if (args[0] === "search" && args.includes("notes-main")) {
const child = createMockChild({ autoClose: false });
emitAndClose(
child,
"stdout",
JSON.stringify([
{
file: "qmd://notes/guide.md",
file: "qmd://notes-main/guide.md",
score: 0.7,
snippet: "@@ -1,1\nnotes guide",
},

View File

@ -173,6 +173,39 @@ describe("resolveMemoryBackendConfig", () => {
}
});
it("keeps unresolved child paths under a symlinked workspace agent-scoped", async () => {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qmd-backend-config-"));
const realRootDir = path.join(tmpRoot, "real-root");
const aliasRootDir = path.join(tmpRoot, "alias-root");
const workspaceDir = path.join(realRootDir, "workspace");
const workspaceAliasDir = path.join(aliasRootDir, "workspace");
try {
await fs.mkdir(workspaceDir, { recursive: true });
await fs.symlink(realRootDir, aliasRootDir);
const cfg = {
agents: {
defaults: { workspace: workspaceDir },
list: [{ id: "main", default: true, workspace: workspaceDir }],
},
memory: {
backend: "qmd",
qmd: {
includeDefaultMemory: false,
paths: [
{ path: path.join(workspaceAliasDir, "notes"), name: "notes", pattern: "**/*.md" },
],
},
},
} as OpenClawConfig;
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
const names = new Set((resolved.qmd?.collections ?? []).map((collection) => collection.name));
expect(names.has("notes-main")).toBe(true);
expect(names.has("notes")).toBe(false);
} finally {
await fs.rm(tmpRoot, { recursive: true, force: true });
}
});
it("resolves qmd update timeout overrides", () => {
const cfg = {
agents: { defaults: { workspace: "/tmp/memory-test" } },

View File

@ -118,10 +118,20 @@ function scopeCollectionBase(base: string, agentId: string): string {
function canonicalizePathForContainment(rawPath: string): string {
const resolved = path.resolve(rawPath);
try {
return path.normalize(fs.realpathSync.native(resolved));
} catch {
return path.normalize(resolved);
let current = resolved;
const suffix: string[] = [];
while (true) {
try {
const canonical = path.normalize(fs.realpathSync.native(current));
return path.normalize(path.join(canonical, ...suffix));
} catch {
const parent = path.dirname(current);
if (parent === current) {
return path.normalize(resolved);
}
suffix.unshift(path.basename(current));
current = parent;
}
}
}