import { beforeEach, describe, expect, it, vi } from "vitest" // Mock the Supermemory SDK (same pattern as claude-memory.test.ts) so tool // executions can be verified deterministically without network access. const documentsDeleteBulk = vi.fn() const documentsGet = vi.fn() const documentsList = vi.fn() const clientAdd = vi.fn() vi.mock("supermemory", () => { return { default: class MockSupermemory { add = clientAdd documents = { deleteBulk: documentsDeleteBulk, get: documentsGet, list: documentsList, add: vi.fn(), } }, } }) import * as aiSdk from "./ai-sdk" import { ClaudeMemoryTool } from "./claude-memory" import { forgetMemoryRequest } from "./shared/forget-memory" import * as openAi from "./openai/tools" const API_KEY = "sm_test_key" type ToolWithExecute = { execute: (args: Record) => unknown } function executeTool(tool: unknown, args: Record) { return (tool as ToolWithExecute).execute(args) } beforeEach(() => { documentsDeleteBulk.mockReset().mockResolvedValue({ success: true, deletedCount: 1, errors: [], }) documentsGet.mockReset().mockResolvedValue({ id: "doc_123", customId: "doc_123", containerTags: ["sm_project_default"], }) documentsList.mockReset().mockResolvedValue({ memories: [{ id: "doc_1", title: "Doc one" }], pagination: { currentPage: 1, totalItems: 1, totalPages: 1 }, }) clientAdd.mockReset().mockResolvedValue({ id: "doc_new" }) vi.unstubAllGlobals() }) describe("documentDelete", () => { it("ai-sdk variant passes the document id string to the SDK", async () => { const tool = aiSdk.documentDeleteTool(API_KEY) const result = (await executeTool(tool, { documentId: "doc_123" })) as { success: boolean } expect(result.success).toBe(true) expect(documentsGet).toHaveBeenCalledWith("doc_123") expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: ["doc_123"] }) }) }) describe("documentList", () => { it("ai-sdk variant returns the SDK's memories array as documents", async () => { const tool = aiSdk.documentListTool(API_KEY) const result = (await executeTool(tool, {})) as { success: boolean documents?: Array<{ id: string }> } expect(result.success).toBe(true) expect(result.documents).toEqual([{ id: "doc_1", title: "Doc one" }]) }) it("openai variant forwards page-based pagination to the SDK", async () => { const documentList = openAi.createDocumentListFunction(API_KEY) const result = await documentList({ limit: 5, page: 3 }) expect(result.success).toBe(true) expect(result.documents).toEqual([{ id: "doc_1", title: "Doc one" }]) expect(documentsList).toHaveBeenCalledWith( expect.objectContaining({ limit: 5, page: 3 }), ) }) }) describe("memoryForget", () => { function stubFetch(response = new Response(null, { status: 200 })) { const fetchMock = vi.fn().mockResolvedValue(response) vi.stubGlobal("fetch", fetchMock) return fetchMock } it("issues DELETE /v4/memories with the forget payload", async () => { const fetchMock = stubFetch() await forgetMemoryRequest(API_KEY, { containerTag: "user_1", id: "mem_1", reason: "outdated", }) expect(fetchMock).toHaveBeenCalledTimes(1) const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit] expect(url).toBe("https://api.supermemory.ai/v4/memories") expect(init.method).toBe("DELETE") expect(init.headers).toMatchObject({ Authorization: `Bearer ${API_KEY}`, }) expect(JSON.parse(init.body as string)).toEqual({ containerTag: "user_1", id: "mem_1", reason: "outdated", }) expect(init.signal).toBeInstanceOf(AbortSignal) }) it("uses a caller-provided signal instead of creating a timeout", async () => { const fetchMock = stubFetch() const controller = new AbortController() await forgetMemoryRequest( API_KEY, { containerTag: "user_1", id: "mem_1" }, undefined, { signal: controller.signal }, ) const [, init] = fetchMock.mock.calls[0] as [string, RequestInit] expect(init.signal).toBe(controller.signal) }) it("throws a descriptive error on non-2xx responses", async () => { stubFetch(new Response("nope", { status: 401, statusText: "Unauthorized" })) await expect( forgetMemoryRequest(API_KEY, { containerTag: "user_1", id: "mem_1" }), ).rejects.toThrow(/401/) }) it("ai-sdk tool forgets by content through the endpoint", async () => { const fetchMock = stubFetch() const tool = aiSdk.memoryForgetTool(API_KEY, { containerTags: ["user_2"], }) const result = (await executeTool(tool, { memoryContent: "stale fact", })) as { success: boolean } expect(result.success).toBe(true) const [, init] = fetchMock.mock.calls[0] as [string, RequestInit] expect(JSON.parse(init.body as string)).toEqual({ containerTag: "user_2", content: "stale fact", }) }) it("openai tool surfaces endpoint failures as tool errors", async () => { stubFetch(new Response("boom", { status: 500, statusText: "Server Error" })) const memoryForget = openAi.createMemoryForgetFunction(API_KEY) const result = await memoryForget({ memoryId: "mem_9" }) expect(result.success).toBe(false) expect(result.error).toMatch(/500/) }) it("still requires an id or content", async () => { const fetchMock = stubFetch() const memoryForget = openAi.createMemoryForgetFunction(API_KEY) const result = await memoryForget({}) expect(result.success).toBe(false) expect(fetchMock).not.toHaveBeenCalled() }) }) describe("ClaudeMemoryTool", () => { const FILE_PATH = "/memories/prefs.txt" const CUSTOM_ID = "memories_prefs_txt" const DOCUMENT_ID = "doc_file_1" function mockFileDocument(content: string) { const metadata = { claude_memory_type: "file", file_path: FILE_PATH, } documentsList.mockResolvedValue({ memories: [ { id: DOCUMENT_ID, customId: CUSTOM_ID, containerTags: ["claude_memory"], metadata, }, ], pagination: { currentPage: 1, totalItems: 1, totalPages: 1 }, }) documentsGet.mockResolvedValue({ id: DOCUMENT_ID, customId: CUSTOM_ID, containerTags: ["sm_project_default", "claude_memory"], content, metadata, }) } it("str_replace accepts an empty new_str to delete text", async () => { mockFileDocument("keep this\nremove this\n") const tool = new ClaudeMemoryTool(API_KEY) const result = await tool.handleCommand({ command: "str_replace", path: FILE_PATH, old_str: "remove this\n", new_str: "", }) expect(result.success).toBe(true) expect(clientAdd).toHaveBeenCalledWith( expect.objectContaining({ content: "keep this\n" }), ) }) it("str_replace still rejects a missing new_str", async () => { const tool = new ClaudeMemoryTool(API_KEY) const result = await tool.handleCommand({ command: "str_replace", path: FILE_PATH, old_str: "something", }) expect(result.success).toBe(false) expect(result.error).toContain("new_str") }) it("insert accepts an empty insert_text for blank lines", async () => { mockFileDocument("line1\nline2") const tool = new ClaudeMemoryTool(API_KEY) const result = await tool.handleCommand({ command: "insert", path: FILE_PATH, insert_line: 2, insert_text: "", }) expect(result.success).toBe(true) expect(clientAdd).toHaveBeenCalledWith( expect.objectContaining({ content: "line1\n\nline2" }), ) }) it("delete actually deletes the backing document", async () => { mockFileDocument("contents") const tool = new ClaudeMemoryTool(API_KEY) const result = await tool.handleCommand({ command: "delete", path: FILE_PATH, }) expect(result.success).toBe(true) expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: [DOCUMENT_ID] }) }) it("rename removes the old document after creating the new one", async () => { mockFileDocument("contents") const tool = new ClaudeMemoryTool(API_KEY) const result = await tool.handleCommand({ command: "rename", path: FILE_PATH, new_path: "/memories/renamed.txt", }) expect(result.success).toBe(true) expect(clientAdd).toHaveBeenCalledWith( expect.objectContaining({ customId: "memories_renamed_txt" }), ) expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: [DOCUMENT_ID] }) }) })