Skip to content

Commit cd062de

Browse files
feat: add file extension preservation check to move_note
- Fetch source entity to get original file extension - Compare source and destination extensions - Return error if extensions don't match - Add tests for extension mismatch and preservation This ensures file type consistency when moving notes. Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
1 parent cdd82e9 commit cd062de

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

src/basic_memory/mcp/tools/move_note.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,41 @@ async def move_note(
460460
All examples in Basic Memory expect file extensions to be explicitly provided.
461461
""").strip()
462462

463+
# Get the source entity to check its file extension
464+
try:
465+
# Fetch source entity information
466+
url = f"{project_url}/knowledge/entities/{identifier}"
467+
response = await call_get(client, url)
468+
source_entity = EntityResponse.model_validate(response.json())
469+
470+
# Extract file extensions
471+
source_ext = source_entity.file_path.split(".")[-1] if "." in source_entity.file_path else ""
472+
dest_ext = destination_path.split(".")[-1] if "." in destination_path else ""
473+
474+
# Check if extensions match
475+
if source_ext and dest_ext and source_ext.lower() != dest_ext.lower():
476+
logger.warning(f"Move failed - file extension mismatch: source={source_ext}, dest={dest_ext}")
477+
return dedent(f"""
478+
# Move Failed - File Extension Mismatch
479+
480+
The destination file extension '.{dest_ext}' does not match the source file extension '.{source_ext}'.
481+
482+
To preserve file type consistency, the destination must have the same extension as the source.
483+
484+
## Source file:
485+
- Path: `{source_entity.file_path}`
486+
- Extension: `.{source_ext}`
487+
488+
## Try again with matching extension:
489+
```
490+
move_note("{identifier}", "{destination_path.rsplit('.', 1)[0]}.{source_ext}")
491+
```
492+
""").strip()
493+
except Exception as e:
494+
# If we can't fetch the source entity, log it but continue
495+
# This might happen if the identifier is not yet resolved
496+
logger.debug(f"Could not fetch source entity for extension check: {e}")
497+
463498
try:
464499
# Prepare move request
465500
move_data = {

tests/mcp/test_tool_move_note.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,69 @@ async def test_move_note_missing_file_extension(client):
231231
assert "Testing extension validation" in content
232232

233233

234+
@pytest.mark.asyncio
235+
async def test_move_note_file_extension_mismatch(client):
236+
"""Test that moving note with different extension is blocked."""
237+
# Create initial note with .md extension
238+
await write_note.fn(
239+
title="MarkdownNote",
240+
folder="source",
241+
content="# Markdown Note\nThis is a markdown file.",
242+
)
243+
244+
# Try to move with .txt extension
245+
result = await move_note.fn(
246+
identifier="source/markdown-note",
247+
destination_path="target/renamed-note.txt",
248+
)
249+
250+
# Should return error about extension mismatch
251+
assert isinstance(result, str)
252+
assert "# Move Failed - File Extension Mismatch" in result
253+
assert "does not match the source file extension" in result
254+
assert ".md" in result
255+
assert ".txt" in result
256+
assert "renamed-note.md" in result # Should suggest correct extension
257+
258+
# Test that note still exists at original location with original extension
259+
content = await read_note.fn("source/markdown-note")
260+
assert "# Markdown Note" in content
261+
assert "This is a markdown file" in content
262+
263+
264+
@pytest.mark.asyncio
265+
async def test_move_note_preserves_file_extension(client):
266+
"""Test that moving note with matching extension succeeds."""
267+
# Create initial note with .md extension
268+
await write_note.fn(
269+
title="PreserveExtension",
270+
folder="source",
271+
content="# Preserve Extension\nTesting that extension is preserved.",
272+
)
273+
274+
# Move with same .md extension
275+
result = await move_note.fn(
276+
identifier="source/preserve-extension",
277+
destination_path="target/preserved-note.md",
278+
)
279+
280+
# Should succeed
281+
assert isinstance(result, str)
282+
assert "✅ Note moved successfully" in result
283+
284+
# Verify note exists at new location with same extension
285+
content = await read_note.fn("target/preserved-note")
286+
assert "# Preserve Extension" in content
287+
assert "Testing that extension is preserved" in content
288+
289+
# Verify old location no longer exists
290+
try:
291+
await read_note.fn("source/preserve-extension")
292+
assert False, "Original note should not exist after move"
293+
except Exception:
294+
pass # Expected
295+
296+
234297
@pytest.mark.asyncio
235298
async def test_move_note_destination_exists(client):
236299
"""Test moving note to existing destination."""

0 commit comments

Comments
 (0)