nexus/server/src/__tests__/33-assistant-memory.test.ts
Nexus Dev 0bfec2a3c8 feat(33-01,33-02): memory service + sanitizer, personal assistant page
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>
2026-04-03 22:03:09 +00:00

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");
});
});