From 3d1c3b78ec2be947f43f9f91d94cefd27149071f Mon Sep 17 00:00:00 2001 From: Benjamin Jesuiter Date: Mon, 2 Feb 2026 20:53:02 +0100 Subject: [PATCH] Tests: cover QMD scope, reads, and citation clamp --- .../tools/memory-tool.citations.test.ts | 18 ++++++- src/memory/qmd-manager.test.ts | 52 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/agents/tools/memory-tool.citations.test.ts b/src/agents/tools/memory-tool.citations.test.ts index 111525822..a57290f73 100644 --- a/src/agents/tools/memory-tool.citations.test.ts +++ b/src/agents/tools/memory-tool.citations.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +let backend: "builtin" | "qmd" = "builtin"; const stubManager = { search: vi.fn(async () => [ { @@ -13,7 +14,7 @@ const stubManager = { ]), readFile: vi.fn(), status: () => ({ - backend: "builtin" as const, + backend, files: 1, chunks: 1, dirty: false, @@ -44,6 +45,7 @@ beforeEach(() => { describe("memory search citations", () => { it("appends source information when citations are enabled", async () => { + backend = "builtin"; const cfg = { memory: { citations: "on" }, agents: { list: [{ id: "main", default: true }] } }; const tool = createMemorySearchTool({ config: cfg }); if (!tool) throw new Error("tool missing"); @@ -54,6 +56,7 @@ describe("memory search citations", () => { }); it("leaves snippet untouched when citations are off", async () => { + backend = "builtin"; const cfg = { memory: { citations: "off" }, agents: { list: [{ id: "main", default: true }] } }; const tool = createMemorySearchTool({ config: cfg }); if (!tool) throw new Error("tool missing"); @@ -62,4 +65,17 @@ describe("memory search citations", () => { expect(details.results[0]?.snippet).not.toMatch(/Source:/); expect(details.results[0]?.citation).toBeUndefined(); }); + + it("clamps decorated snippets to qmd injected budget", async () => { + backend = "qmd"; + const cfg = { + memory: { citations: "on", backend: "qmd", qmd: { limits: { maxInjectedChars: 20 } } }, + agents: { list: [{ id: "main", default: true }] }, + }; + const tool = createMemorySearchTool({ config: cfg }); + if (!tool) throw new Error("tool missing"); + const result = await tool.execute("call_citations_qmd", { query: "notes" }); + const details = result.details as { results: Array<{ snippet: string; citation?: string }> }; + expect(details.results[0]?.snippet.length).toBeLessThanOrEqual(20); + }); }); diff --git a/src/memory/qmd-manager.test.ts b/src/memory/qmd-manager.test.ts index ba77010cb..771309fa4 100644 --- a/src/memory/qmd-manager.test.ts +++ b/src/memory/qmd-manager.test.ts @@ -95,4 +95,56 @@ describe("QmdMemoryManager", () => { await manager.close(); }); + + it("scopes by channel for agent-prefixed session keys", async () => { + cfg = { + ...cfg, + memory: { + backend: "qmd", + qmd: { + includeDefaultMemory: false, + update: { interval: "0s", debounceMs: 60_000, onBoot: false }, + paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }], + scope: { + default: "deny", + rules: [{ action: "allow", match: { channel: "slack" } }], + }, + }, + }, + } as MoltbotConfig; + const resolved = resolveMemoryBackendConfig({ cfg, agentId }); + const manager = await QmdMemoryManager.create({ cfg, agentId, resolved }); + expect(manager).toBeTruthy(); + if (!manager) throw new Error("manager missing"); + + const isAllowed = (key?: string) => + (manager as unknown as { isScopeAllowed: (key?: string) => boolean }).isScopeAllowed(key); + expect(isAllowed("agent:main:slack:channel:c123")).toBe(true); + expect(isAllowed("agent:main:discord:channel:c123")).toBe(false); + + await manager.close(); + }); + + it("blocks non-markdown or symlink reads for qmd paths", async () => { + const resolved = resolveMemoryBackendConfig({ cfg, agentId }); + const manager = await QmdMemoryManager.create({ cfg, agentId, resolved }); + expect(manager).toBeTruthy(); + if (!manager) throw new Error("manager missing"); + + const textPath = path.join(workspaceDir, "secret.txt"); + await fs.writeFile(textPath, "nope", "utf-8"); + await expect(manager.readFile({ relPath: "qmd/workspace/secret.txt" })).rejects.toThrow( + "path required", + ); + + const target = path.join(workspaceDir, "target.md"); + await fs.writeFile(target, "ok", "utf-8"); + const link = path.join(workspaceDir, "link.md"); + await fs.symlink(target, link); + await expect(manager.readFile({ relPath: "qmd/workspace/link.md" })).rejects.toThrow( + "path required", + ); + + await manager.close(); + }); });