This commit is contained in:
nancy 2026-10-24 20:24:15 -07:00 • committed by GitHub
commit 1e829021a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 68 additions and 0 deletions

View file

@ -292,6 +292,57 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => {
})
})
describe("ClaudeMemoryTool str_replace uniqueness", () => {
let tool: ClaudeMemoryTool
beforeEach(() => {
documentsGetMock.mockReset()
updateMock.mockReset().mockResolvedValue({ id: "doc", status: "queued" })
mockDocument("status: todo\nnotes\nstatus: todo")
tool = new ClaudeMemoryTool("test-api-key")
})
it("refuses to replace an old_str that occurs more than once", async () => {
const result = await tool.handleCommand({
command: "str_replace",
path: FILE_PATH,
old_str: "status: todo",
new_str: "status: done",
})
expect(result.success).toBe(false)
expect(result.error).toContain("Multiple occurrences")
expect(result.error).toContain("lines: 1, 3")
expect(updateMock).not.toHaveBeenCalled()
})
it("counts overlapping occurrences", async () => {
mockDocument("aaa")
const result = await tool.handleCommand({
command: "str_replace",
path: FILE_PATH,
old_str: "aa",
new_str: "b",
})
expect(result.success).toBe(false)
expect(updateMock).not.toHaveBeenCalled()
})
it("still replaces a unique old_str", async () => {
const result = await tool.handleCommand({
command: "str_replace",
path: FILE_PATH,
old_str: "notes",
new_str: "updated notes",
})
expect(result.success).toBe(true)
const stored = updateMock.mock.calls[0]?.[2]?.content as string
expect(stored).toBe("status: todo\nupdated notes\nstatus: todo")
})
})
describe("ClaudeMemoryTool path traversal", () => {
let tool: ClaudeMemoryTool

View file

@ -389,6 +389,23 @@ export class ClaudeMemoryTool {
}
}
// old_str must be unique, like Anthropic's memory tool: replacing
// only the first of several matches would silently edit the wrong spot.
const matchingLines: number[] = []
for (
let index = originalContent.indexOf(oldStr);
index !== -1;
index = originalContent.indexOf(oldStr, index + 1)
) {
matchingLines.push(originalContent.slice(0, index).split("\n").length)
}
if (matchingLines.length > 1) {
return {
success: false,
error: `No replacement was performed. Multiple occurrences of old_str "${oldStr}" in lines: ${matchingLines.join(", ")}. Please ensure it is unique`,
}
}
// Replace the string. The function replacer keeps `$` sequences
// in the replacement literal — a bare string here would expand
// patterns like $&, $', and $` and silently corrupt the file.