fix(tools): make Claude memory tool exact-file lookup and listing deterministic

This commit is contained in:
Aditya kumar singh 2026-08-27 23:14:21 +05:30
parent f11d8c4620
commit 8342ffa18e
2 changed files with 56 additions and 38 deletions

View file

@ -2,14 +2,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest"
// Mock the Supermemory SDK so the Claude memory tool's `view`/`readFile` path
// can be exercised deterministically without any network access. We only need
// `search.execute` to return a single document with known multi-line content.
const searchExecute = vi.fn()
// `documents.list` and `documents.get` to return a single document with known multi-line content.
const listMock = vi.fn()
const getMock = vi.fn()
const addMock = vi.fn()
vi.mock("supermemory", () => {
return {
default: class MockSupermemory {
search = { execute: searchExecute }
documents = { list: listMock, get: getMock }
add = addMock
memories = { forget: vi.fn() }
},
@ -23,10 +24,13 @@ const FILE_PATH = "/memories/notes.txt"
const FILE_CONTENT = "line1\nline2\nline3\nline4\nline5"
function mockDocument(content: string) {
// `readFile` matches by `documentId === normalizePathToCustomId(path)`.
// normalizePathToCustomId("/memories/notes.txt") -> "memories_notes_txt"
searchExecute.mockResolvedValue({
results: [{ documentId: "memories_notes_txt", content }],
listMock.mockResolvedValue({
memories: [{ id: "doc_12345", customId: "memories_notes_txt" }],
})
getMock.mockResolvedValue({
id: "doc_12345",
customId: "memories_notes_txt",
content,
})
}
@ -34,7 +38,8 @@ describe("ClaudeMemoryTool view_range", () => {
let tool: ClaudeMemoryTool
beforeEach(() => {
searchExecute.mockReset()
listMock.mockReset()
getMock.mockReset()
mockDocument(FILE_CONTENT)
tool = new ClaudeMemoryTool("test-api-key")
})
@ -89,18 +94,25 @@ describe("ClaudeMemoryTool exact-file matching", () => {
let tool: ClaudeMemoryTool
beforeEach(() => {
searchExecute.mockReset()
listMock.mockReset()
getMock.mockReset()
addMock.mockReset()
tool = new ClaudeMemoryTool("test-api-key")
})
it("view finds the exact file even when a neighbour ranks first", async () => {
searchExecute.mockResolvedValue({
results: [
{ documentId: "memories_notes_backup_txt", content: "backup stuff" },
{ documentId: "memories_notes_txt", content: FILE_CONTENT },
listMock.mockResolvedValue({
memories: [
{ id: "doc_backup", customId: "memories_notes_backup_txt" },
{ id: "doc_primary", customId: "memories_notes_txt" },
],
})
getMock.mockImplementation(async (id: string) => {
if (id === "doc_primary") {
return { id: "doc_primary", customId: "memories_notes_txt", content: FILE_CONTENT }
}
return { id: "doc_backup", customId: "memories_notes_backup_txt", content: "backup stuff" }
})
const result = await tool.handleCommand({
command: "view",
@ -115,9 +127,9 @@ describe("ClaudeMemoryTool exact-file matching", () => {
it("view reports not-found instead of returning a different file", async () => {
// Semantic search can surface a similarly-named file; that must not
// be served as the requested one.
searchExecute.mockResolvedValue({
results: [
{ documentId: "memories_notes_backup_txt", content: "backup stuff" },
listMock.mockResolvedValue({
memories: [
{ id: "doc_backup", customId: "memories_notes_backup_txt" },
],
})
@ -131,9 +143,9 @@ describe("ClaudeMemoryTool exact-file matching", () => {
})
it("str_replace refuses to modify a different file than requested", async () => {
searchExecute.mockResolvedValue({
results: [
{ documentId: "memories_notes_backup_txt", content: "backup stuff" },
listMock.mockResolvedValue({
memories: [
{ id: "doc_backup", customId: "memories_notes_backup_txt" },
],
})
@ -153,10 +165,16 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => {
let tool: ClaudeMemoryTool
beforeEach(() => {
searchExecute.mockReset()
listMock.mockReset()
getMock.mockReset()
addMock.mockReset()
searchExecute.mockResolvedValue({
results: [{ documentId: "memories_notes_txt", content: FILE_CONTENT }],
listMock.mockResolvedValue({
memories: [{ id: "doc_12345", customId: "memories_notes_txt" }],
})
getMock.mockResolvedValue({
id: "doc_12345",
customId: "memories_notes_txt",
content: FILE_CONTENT,
})
tool = new ClaudeMemoryTool("test-api-key")
})

View file

@ -194,14 +194,13 @@ export class ClaudeMemoryTool {
private async listDirectory(dirPath: string): Promise<MemoryResponse> {
try {
// Search for all memory files
const response = await this.client.search.execute({
q: "*", // Search for all
const response = await this.client.documents.list({
containerTags: this.containerTags,
limit: 100, // Get many files (max allowed)
includeFullDocs: false,
includeContent: false,
})
if (!response.results) {
if (!response.memories) {
return {
success: true,
content: `Directory: ${dirPath}\n(empty)`,
@ -212,7 +211,7 @@ export class ClaudeMemoryTool {
const files: string[] = []
const dirs = new Set<string>()
for (const result of response.results) {
for (const result of response.memories) {
// Get the file path from metadata (since customId is normalized)
const filePath = result.metadata?.file_path as string
if (!filePath || !filePath.startsWith(dirPath)) continue
@ -577,30 +576,31 @@ export class ClaudeMemoryTool {
try {
const normalizedId = this.normalizePathToCustomId(filePath)
const response = await this.client.search.execute({
q: normalizedId,
const response = await this.client.documents.list({
containerTags: this.containerTags,
limit: 5,
includeFullDocs: true,
limit: 100,
includeContent: false,
})
// Only accept the exact customId match. Falling back to the top
// semantic hit would let callers read — and worse, modify or
// delete — a different file than the one they asked for.
const document = response.results?.find(
(r) => r.documentId === normalizedId,
const matchedDoc = response.memories?.find(
(d) => d.customId === normalizedId,
)
if (!document) {
if (!matchedDoc) {
return {
success: false,
error: `File not found: ${filePath}`,
}
}
const document = await this.client.documents.get(matchedDoc.id)
return {
success: true,
document,
document: {
...document,
documentId: document.id,
},
}
} catch (error) {
return {