Files
claude-cookbooks/tool_use/tests/test_memory_tool.py
Alex Notov 4d3ed1f75b Add memory & context management cookbook
Interactive notebook demonstrating Claude's memory tool and context editing capabilities with code review examples.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 20:11:13 -06:00

427 lines
15 KiB
Python

"""
Unit tests for the memory tool handler.
Tests security validation, command execution, and error handling.
"""
import shutil
import tempfile
import unittest
from pathlib import Path
from memory_tool import MemoryToolHandler
class TestMemoryToolHandler(unittest.TestCase):
"""Test suite for MemoryToolHandler."""
def setUp(self):
"""Create temporary directory for each test."""
self.test_dir = tempfile.mkdtemp()
self.handler = MemoryToolHandler(base_path=self.test_dir)
def tearDown(self):
"""Clean up temporary directory after each test."""
shutil.rmtree(self.test_dir)
# Security Tests
def test_path_validation_requires_memories_prefix(self):
"""Test that paths must start with /memories."""
result = self.handler.execute(command="view", path="/etc/passwd")
self.assertIn("error", result)
self.assertIn("must start with /memories", result["error"])
def test_path_validation_prevents_traversal_dotdot(self):
"""Test that .. traversal is blocked."""
result = self.handler.execute(
command="view", path="/memories/../../../etc/passwd"
)
self.assertIn("error", result)
self.assertIn("escape", result["error"].lower())
def test_path_validation_prevents_traversal_encoded(self):
"""Test that URL-encoded traversal is blocked."""
result = self.handler.execute(
command="view", path="/memories/%2e%2e/%2e%2e/etc/passwd"
)
# The path will be processed and should fail validation
self.assertIn("error", result)
def test_path_validation_allows_valid_paths(self):
"""Test that valid memory paths are accepted."""
result = self.handler.execute(
command="create", path="/memories/test.txt", file_text="test"
)
self.assertIn("success", result)
# View Command Tests
def test_view_empty_directory(self):
"""Test viewing an empty /memories directory."""
result = self.handler.execute(command="view", path="/memories")
self.assertIn("success", result)
self.assertIn("empty", result["success"].lower())
def test_view_directory_with_files(self):
"""Test viewing a directory with files."""
# Create some test files
self.handler.execute(
command="create", path="/memories/file1.txt", file_text="content1"
)
self.handler.execute(
command="create", path="/memories/file2.txt", file_text="content2"
)
result = self.handler.execute(command="view", path="/memories")
self.assertIn("success", result)
self.assertIn("file1.txt", result["success"])
self.assertIn("file2.txt", result["success"])
def test_view_file_with_line_numbers(self):
"""Test viewing a file with line numbers."""
content = "line 1\nline 2\nline 3"
self.handler.execute(
command="create", path="/memories/test.txt", file_text=content
)
result = self.handler.execute(command="view", path="/memories/test.txt")
self.assertIn("success", result)
self.assertIn(" 1: line 1", result["success"])
self.assertIn(" 2: line 2", result["success"])
self.assertIn(" 3: line 3", result["success"])
def test_view_file_with_range(self):
"""Test viewing specific line range."""
content = "line 1\nline 2\nline 3\nline 4"
self.handler.execute(
command="create", path="/memories/test.txt", file_text=content
)
result = self.handler.execute(
command="view", path="/memories/test.txt", view_range=[2, 3]
)
self.assertIn("success", result)
self.assertIn(" 2: line 2", result["success"])
self.assertIn(" 3: line 3", result["success"])
self.assertNotIn("line 1", result["success"])
self.assertNotIn("line 4", result["success"])
def test_view_nonexistent_path(self):
"""Test viewing a nonexistent path."""
result = self.handler.execute(command="view", path="/memories/notfound.txt")
self.assertIn("error", result)
self.assertIn("not found", result["error"].lower())
# Create Command Tests
def test_create_file(self):
"""Test creating a file."""
result = self.handler.execute(
command="create", path="/memories/test.txt", file_text="Hello, World!"
)
self.assertIn("success", result)
# Verify file exists
file_path = Path(self.test_dir) / "memories" / "test.txt"
self.assertTrue(file_path.exists())
self.assertEqual(file_path.read_text(), "Hello, World!")
def test_create_file_in_subdirectory(self):
"""Test creating a file in a subdirectory (auto-creates dirs)."""
result = self.handler.execute(
command="create",
path="/memories/subdir/test.txt",
file_text="Nested content",
)
self.assertIn("success", result)
file_path = Path(self.test_dir) / "memories" / "subdir" / "test.txt"
self.assertTrue(file_path.exists())
def test_create_requires_file_extension(self):
"""Test that create only allows text file extensions."""
result = self.handler.execute(
command="create", path="/memories/noext", file_text="content"
)
self.assertIn("error", result)
self.assertIn("text files are supported", result["error"])
def test_create_overwrites_existing_file(self):
"""Test that create overwrites existing files."""
self.handler.execute(
command="create", path="/memories/test.txt", file_text="original"
)
result = self.handler.execute(
command="create", path="/memories/test.txt", file_text="updated"
)
self.assertIn("success", result)
file_path = Path(self.test_dir) / "memories" / "test.txt"
self.assertEqual(file_path.read_text(), "updated")
# String Replace Command Tests
def test_str_replace_success(self):
"""Test successful string replacement."""
self.handler.execute(
command="create",
path="/memories/test.txt",
file_text="Hello World",
)
result = self.handler.execute(
command="str_replace",
path="/memories/test.txt",
old_str="World",
new_str="Universe",
)
self.assertIn("success", result)
file_path = Path(self.test_dir) / "memories" / "test.txt"
self.assertEqual(file_path.read_text(), "Hello Universe")
def test_str_replace_string_not_found(self):
"""Test replacement when string doesn't exist."""
self.handler.execute(
command="create", path="/memories/test.txt", file_text="Hello World"
)
result = self.handler.execute(
command="str_replace",
path="/memories/test.txt",
old_str="Missing",
new_str="Text",
)
self.assertIn("error", result)
self.assertIn("not found", result["error"].lower())
def test_str_replace_multiple_occurrences(self):
"""Test that replacement fails with multiple occurrences."""
self.handler.execute(
command="create",
path="/memories/test.txt",
file_text="Hello World Hello World",
)
result = self.handler.execute(
command="str_replace",
path="/memories/test.txt",
old_str="Hello",
new_str="Hi",
)
self.assertIn("error", result)
self.assertIn("appears 2 times", result["error"])
def test_str_replace_file_not_found(self):
"""Test replacement on nonexistent file."""
result = self.handler.execute(
command="str_replace",
path="/memories/notfound.txt",
old_str="old",
new_str="new",
)
self.assertIn("error", result)
self.assertIn("not found", result["error"].lower())
# Insert Command Tests
def test_insert_at_beginning(self):
"""Test inserting at line 0 (beginning)."""
self.handler.execute(
command="create", path="/memories/test.txt", file_text="line 1\nline 2"
)
result = self.handler.execute(
command="insert",
path="/memories/test.txt",
insert_line=0,
insert_text="new line",
)
self.assertIn("success", result)
file_path = Path(self.test_dir) / "memories" / "test.txt"
self.assertEqual(file_path.read_text(), "new line\nline 1\nline 2\n")
def test_insert_in_middle(self):
"""Test inserting in the middle."""
self.handler.execute(
command="create", path="/memories/test.txt", file_text="line 1\nline 2"
)
result = self.handler.execute(
command="insert",
path="/memories/test.txt",
insert_line=1,
insert_text="inserted",
)
self.assertIn("success", result)
file_path = Path(self.test_dir) / "memories" / "test.txt"
self.assertEqual(file_path.read_text(), "line 1\ninserted\nline 2\n")
def test_insert_at_end(self):
"""Test inserting at the end."""
self.handler.execute(
command="create", path="/memories/test.txt", file_text="line 1\nline 2"
)
result = self.handler.execute(
command="insert",
path="/memories/test.txt",
insert_line=2,
insert_text="last line",
)
self.assertIn("success", result)
def test_insert_invalid_line(self):
"""Test insert with invalid line number."""
self.handler.execute(
command="create", path="/memories/test.txt", file_text="line 1"
)
result = self.handler.execute(
command="insert",
path="/memories/test.txt",
insert_line=99,
insert_text="text",
)
self.assertIn("error", result)
self.assertIn("invalid", result["error"].lower())
# Delete Command Tests
def test_delete_file(self):
"""Test deleting a file."""
self.handler.execute(
command="create", path="/memories/test.txt", file_text="content"
)
result = self.handler.execute(command="delete", path="/memories/test.txt")
self.assertIn("success", result)
file_path = Path(self.test_dir) / "memories" / "test.txt"
self.assertFalse(file_path.exists())
def test_delete_directory(self):
"""Test deleting a directory."""
self.handler.execute(
command="create", path="/memories/subdir/test.txt", file_text="content"
)
result = self.handler.execute(command="delete", path="/memories/subdir")
self.assertIn("success", result)
dir_path = Path(self.test_dir) / "memories" / "subdir"
self.assertFalse(dir_path.exists())
def test_delete_cannot_delete_root(self):
"""Test that root /memories directory cannot be deleted."""
result = self.handler.execute(command="delete", path="/memories")
self.assertIn("error", result)
self.assertIn("cannot delete", result["error"].lower())
def test_delete_nonexistent_path(self):
"""Test deleting a nonexistent path."""
result = self.handler.execute(command="delete", path="/memories/notfound.txt")
self.assertIn("error", result)
self.assertIn("not found", result["error"].lower())
# Rename Command Tests
def test_rename_file(self):
"""Test renaming a file."""
self.handler.execute(
command="create", path="/memories/old.txt", file_text="content"
)
result = self.handler.execute(
command="rename", old_path="/memories/old.txt", new_path="/memories/new.txt"
)
self.assertIn("success", result)
old_path = Path(self.test_dir) / "memories" / "old.txt"
new_path = Path(self.test_dir) / "memories" / "new.txt"
self.assertFalse(old_path.exists())
self.assertTrue(new_path.exists())
def test_rename_to_subdirectory(self):
"""Test moving a file to a subdirectory."""
self.handler.execute(
command="create", path="/memories/file.txt", file_text="content"
)
result = self.handler.execute(
command="rename",
old_path="/memories/file.txt",
new_path="/memories/subdir/file.txt",
)
self.assertIn("success", result)
new_path = Path(self.test_dir) / "memories" / "subdir" / "file.txt"
self.assertTrue(new_path.exists())
def test_rename_source_not_found(self):
"""Test rename when source doesn't exist."""
result = self.handler.execute(
command="rename",
old_path="/memories/notfound.txt",
new_path="/memories/new.txt",
)
self.assertIn("error", result)
self.assertIn("not found", result["error"].lower())
def test_rename_destination_exists(self):
"""Test rename when destination already exists."""
self.handler.execute(
command="create", path="/memories/file1.txt", file_text="content1"
)
self.handler.execute(
command="create", path="/memories/file2.txt", file_text="content2"
)
result = self.handler.execute(
command="rename",
old_path="/memories/file1.txt",
new_path="/memories/file2.txt",
)
self.assertIn("error", result)
self.assertIn("already exists", result["error"].lower())
# Error Handling Tests
def test_unknown_command(self):
"""Test handling of unknown command."""
result = self.handler.execute(command="invalid", path="/memories")
self.assertIn("error", result)
self.assertIn("unknown command", result["error"].lower())
def test_missing_required_parameters(self):
"""Test error handling for missing parameters."""
result = self.handler.execute(command="view")
self.assertIn("error", result)
# Utility Tests
def test_clear_all_memory(self):
"""Test clearing all memory."""
# Create some files
self.handler.execute(
command="create", path="/memories/file1.txt", file_text="content1"
)
self.handler.execute(
command="create", path="/memories/file2.txt", file_text="content2"
)
result = self.handler.clear_all_memory()
self.assertIn("success", result)
# Verify directory exists but is empty
memory_root = Path(self.test_dir) / "memories"
self.assertTrue(memory_root.exists())
self.assertEqual(len(list(memory_root.iterdir())), 0)
if __name__ == "__main__":
unittest.main()