import { describe, expect, it, vi, beforeEach } from "vitest"; import os from "node:os"; import path from "node:path"; import fs from "node:fs"; // Mock resolvePaperclipInstanceRoot to return a temp dir const tmpDir = path.join(os.tmpdir(), `test-memory-${Date.now()}-${Math.random().toString(36).slice(2)}`); vi.mock("../home-paths.js", () => ({ resolvePaperclipInstanceRoot: () => tmpDir, })); // Import after mock is set up const { assistantMemoryService } = await import("../services/assistant-memory.js"); describe("assistantMemoryService", () => { const service = assistantMemoryService(); beforeEach(() => { // Clean up the temp dir between tests const memDir = path.join(tmpDir, "data", "assistant-memory"); if (fs.existsSync(memDir)) { fs.rmSync(memDir, { recursive: true, force: true }); } }); it("returns default empty memory when no file exists", async () => { const result = await service.get("company-a"); expect(result.facts).toEqual([]); expect(result.updatedAt).toBeNull(); }); it("appends a fact and returns updated memory", async () => { const result = await service.append("company-b", "I prefer TypeScript"); expect(result.facts).toContain("I prefer TypeScript"); expect(result.updatedAt).not.toBeNull(); }); it("persists facts across get calls", async () => { await service.append("company-c", "I prefer TypeScript"); const result = await service.get("company-c"); expect(result.facts).toContain("I prefer TypeScript"); }); it("sanitizes credential facts at write time", async () => { const result = await service.append("company-d", "My API key is sk-abc123def456ghijklmnopqrst"); expect(result.facts.some((f) => f.includes("sk-abc123"))).toBe(false); expect(result.facts.some((f) => f.includes("[REDACTED]"))).toBe(true); }); it("clears all facts", async () => { await service.append("company-e", "I prefer TypeScript"); await service.clear("company-e"); const result = await service.get("company-e"); expect(result.facts).toEqual([]); expect(result.updatedAt).toBeNull(); }); it("enforces 50-fact FIFO cap", async () => { const companyId = "company-f"; for (let i = 0; i < 51; i++) { await service.append(companyId, `Fact number ${i}`); } const result = await service.get(companyId); expect(result.facts).toHaveLength(50); // Oldest fact (0) should be evicted expect(result.facts).not.toContain("Fact number 0"); // Newest fact (50) should be present expect(result.facts).toContain("Fact number 50"); }); it("uses separate files for different companyIds", async () => { await service.append("co-x", "Fact for X"); await service.append("co-y", "Fact for Y"); const memX = await service.get("co-x"); const memY = await service.get("co-y"); expect(memX.facts).toContain("Fact for X"); expect(memX.facts).not.toContain("Fact for Y"); expect(memY.facts).toContain("Fact for Y"); expect(memY.facts).not.toContain("Fact for X"); }); });