33-01: memory-sanitizer, assistant-memory service, REST routes, 17 tests 33-02: useNexusMode hook, PersonalAssistantPage, sidebar nav, route wiring Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
3 KiB
TypeScript
84 lines
3 KiB
TypeScript
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");
|
|
});
|
|
});
|