supermemory/packages/tools/src/tool-operations.test.ts
2026-08-22 12:13:25 +05:30

292 lines
8.1 KiB
TypeScript

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<string, unknown>) => unknown }
function executeTool(tool: unknown, args: Record<string, unknown>) {
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] })
})
})