import { describe, it, expect, vi, beforeEach } from "vitest"; import { registerCommands } from "./commands.ts"; import type { Mem0Config, ScopeContext } from "./types.ts"; vi.mock("./telemetry.ts", () => ({ captureCommandEvent: vi.fn(), })); vi.mock("./dream/index.ts", () => ({ acquireDreamLock: vi.fn(() => true), })); vi.mock("./dream/prompt.ts", () => ({ DREAM_PROTOCOL: "dream protocol text", })); function makeMem0() { return { search: vi.fn(), delete: vi.fn(), add: vi.fn(), get: vi.fn(), getAll: vi.fn(), update: vi.fn(), } as any; } function makePi() { const commands = new Map Promise }>(); return { registerCommand: vi.fn((name: string, opts: any) => { commands.set(name, opts); }), sendMessage: vi.fn(), _commands: commands, _invoke: (name: string, args: string, ctx: any) => commands.get(name)!.handler(args, ctx), }; } function makeCtx(confirmResult = true) { return { hasUI: true, ui: { notify: vi.fn(), confirm: vi.fn(async () => confirmResult), select: vi.fn(), input: vi.fn(), }, }; } const defaultConfig: Mem0Config = { apiKey: "test-key", userId: "test-user", autoCapture: false, defaultScope: "project", contextInjection: false, dream: { enabled: false, auto: false, minHours: 24, minSessions: 5, minMemories: 20 }, }; const scopeCtx: ScopeContext = { userId: "test-user", appId: "test-app", runId: "test-run" }; describe("registerCommands", () => { let pi: ReturnType; let mem0: ReturnType; beforeEach(() => { pi = makePi(); mem0 = makeMem0(); registerCommands(pi as any, mem0, defaultConfig, () => scopeCtx); }); it("registers all expected commands", () => { const names = [...pi._commands.keys()]; expect(names).toContain("mem0-remember"); expect(names).toContain("mem0-forget"); expect(names).toContain("mem0-search"); expect(names).toContain("mem0-tour"); expect(names).toContain("mem0-dream"); expect(names).toContain("mem0-pin"); expect(names).toContain("mem0-scope"); expect(names).toContain("mem0-status"); }); describe("/mem0-forget", () => { it("shows warning when no query provided", async () => { const ctx = makeCtx(); await pi._invoke("mem0-forget", "", ctx); expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /mem0-forget ", "warning"); expect(mem0.search).not.toHaveBeenCalled(); }); it("notifies when no memories match", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [] }); await pi._invoke("mem0-forget", "old preference", ctx); expect(ctx.ui.notify).toHaveBeenCalledWith("No matching memories found.", "info"); }); it("asks for confirmation before deleting a single match", async () => { const ctx = makeCtx(true); mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] }); mem0.delete.mockResolvedValue({ message: "Deleted" }); await pi._invoke("mem0-forget", "test", ctx); expect(ctx.ui.confirm).toHaveBeenCalledWith( "Delete this memory?", expect.stringContaining("test mem"), ); expect(mem0.delete).toHaveBeenCalledWith("abc-123"); }); it("does not delete when user cancels confirmation", async () => { const ctx = makeCtx(false); mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] }); await pi._invoke("mem0-forget", "test", ctx); expect(ctx.ui.confirm).toHaveBeenCalled(); expect(mem0.delete).not.toHaveBeenCalled(); expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info"); }); it("uses select UI for multiple matches and deletes chosen memory", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [ { id: "id-1", memory: "mem one" }, { id: "id-2", memory: "mem two" }, ], }); mem0.delete.mockResolvedValue({ message: "Deleted" }); ctx.ui.select = vi.fn(async (_title: string, options: string[]) => options[1]); await pi._invoke("mem0-forget", "test", ctx); expect(ctx.ui.select).toHaveBeenCalledWith( "Which memory should I delete?", expect.arrayContaining([ expect.stringContaining("mem one"), expect.stringContaining("mem two"), ]), ); expect(mem0.delete).toHaveBeenCalledWith("id-2"); }); it("does not delete when user cancels select", async () => { const ctx = makeCtx(); ctx.ui.select = vi.fn(async () => undefined); mem0.search.mockResolvedValue({ results: [ { id: "id-1", memory: "mem one" }, { id: "id-2", memory: "mem two" }, ], }); await pi._invoke("mem0-forget", "test", ctx); expect(mem0.delete).not.toHaveBeenCalled(); expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info"); }); }); describe("/mem0-pin", () => { it("uses update to pin in-place, preserving memory ID", async () => { const ctx = makeCtx(true); mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "important fact" }] }); mem0.update.mockResolvedValue([]); await pi._invoke("mem0-pin", "important", ctx); expect(ctx.ui.confirm).toHaveBeenCalledWith( "Pin this memory?", expect.stringContaining("important fact"), ); expect(mem0.update).toHaveBeenCalledWith("abc-123", { text: "[PINNED] important fact" }); expect(mem0.add).not.toHaveBeenCalled(); expect(mem0.delete).not.toHaveBeenCalled(); }); it("does not pin when user cancels", async () => { const ctx = makeCtx(false); mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "fact" }] }); await pi._invoke("mem0-pin", "fact", ctx); expect(mem0.update).not.toHaveBeenCalled(); }); it("skips already-pinned memories", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "[PINNED] fact" }] }); await pi._invoke("mem0-pin", "fact", ctx); expect(ctx.ui.confirm).not.toHaveBeenCalled(); expect(mem0.add).not.toHaveBeenCalled(); expect(ctx.ui.notify).toHaveBeenCalledWith("Already pinned.", "info"); }); it("uses select UI for multiple matches and pins chosen memory", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [ { id: "id-1", memory: "fact one" }, { id: "id-2", memory: "fact two" }, ], }); mem0.update.mockResolvedValue([]); ctx.ui.select = vi.fn(async (_title: string, options: string[]) => options[1]); await pi._invoke("mem0-pin", "fact", ctx); expect(ctx.ui.select).toHaveBeenCalledWith( "Which memory should I pin?", expect.arrayContaining([ expect.stringContaining("fact one"), expect.stringContaining("fact two"), ]), ); expect(mem0.update).toHaveBeenCalledWith("id-2", { text: "[PINNED] fact two" }); }); it("does not pin when user cancels select", async () => { const ctx = makeCtx(); ctx.ui.select = vi.fn(async () => undefined); mem0.search.mockResolvedValue({ results: [ { id: "id-1", memory: "fact one" }, { id: "id-2", memory: "fact two" }, ], }); await pi._invoke("mem0-pin", "fact", ctx); expect(mem0.update).not.toHaveBeenCalled(); expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info"); }); }); describe("/mem0-search", () => { it("always performs semantic search", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [{ id: "id-1", memory: "result" }] }); await pi._invoke("mem0-search", "my preferences", ctx); expect(mem0.search).toHaveBeenCalledWith("my preferences", expect.any(Object)); expect(pi.sendMessage).toHaveBeenCalledWith( expect.objectContaining({ customType: "mem0-search" }), ); }); it("uses semantic search even for hex-looking strings", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [] }); await pi._invoke("mem0-search", "abcd1234", ctx); expect(mem0.search).toHaveBeenCalledWith("abcd1234", expect.any(Object)); expect(mem0.getAll).not.toHaveBeenCalled(); expect(mem0.get).not.toHaveBeenCalled(); }); it("shows empty results message", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [] }); await pi._invoke("mem0-search", "nonexistent", ctx); expect(pi.sendMessage).toHaveBeenCalledWith( expect.objectContaining({ content: "No memories found." }), ); }); }); describe("/mem0-remember", () => { it("stores a memory verbatim", async () => { const ctx = makeCtx(); mem0.add.mockResolvedValue({ message: "Memory stored." }); await pi._invoke("mem0-remember", "I prefer dark mode", ctx); expect(mem0.add).toHaveBeenCalledWith( [{ role: "user", content: "I prefer dark mode" }], expect.objectContaining({ infer: false }), ); }); it("shows warning when no text provided", async () => { const ctx = makeCtx(); await pi._invoke("mem0-remember", " ", ctx); expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /mem0-remember ", "warning"); }); }); });