mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
05adda1502
Signed-off-by: phernandez <paul@basicmachines.co>
278 lines
11 KiB
Python
278 lines
11 KiB
Python
"""Tests for delete_note MCP tool."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from basic_memory.mcp.tools.delete_note import delete_note, _format_delete_error_response
|
|
from basic_memory.mcp.tools.read_note import read_note
|
|
from basic_memory.mcp.tools.write_note import write_note
|
|
|
|
|
|
class TestDeleteNoteErrorFormatting:
|
|
"""Test the error formatting function for better user experience."""
|
|
|
|
def test_format_delete_error_note_not_found(self, test_project):
|
|
"""Test formatting for note not found errors."""
|
|
result = _format_delete_error_response(test_project.name, "entity not found", "test-note")
|
|
|
|
assert "# Delete Failed - Note Not Found" in result
|
|
assert "The note 'test-note' could not be found" in result
|
|
assert 'search_notes("test-project", "test-note")' in result
|
|
assert "Already deleted" in result
|
|
assert "Wrong identifier" in result
|
|
|
|
def test_format_delete_error_permission_denied(self, test_project):
|
|
"""Test formatting for permission errors."""
|
|
result = _format_delete_error_response(test_project.name, "permission denied", "test-note")
|
|
|
|
assert "# Delete Failed - Permission Error" in result
|
|
assert "You don't have permission to delete 'test-note'" in result
|
|
assert "Check permissions" in result
|
|
assert "File locks" in result
|
|
assert "list_memory_projects()" in result
|
|
|
|
def test_format_delete_error_access_forbidden(self, test_project):
|
|
"""Test formatting for access forbidden errors."""
|
|
result = _format_delete_error_response(test_project.name, "access forbidden", "test-note")
|
|
|
|
assert "# Delete Failed - Permission Error" in result
|
|
assert "You don't have permission to delete 'test-note'" in result
|
|
|
|
def test_format_delete_error_server_error(self, test_project):
|
|
"""Test formatting for server errors."""
|
|
result = _format_delete_error_response(
|
|
test_project.name, "server error occurred", "test-note"
|
|
)
|
|
|
|
assert "# Delete Failed - System Error" in result
|
|
assert "A system error occurred while deleting 'test-note'" in result
|
|
assert "Try again" in result
|
|
assert "Check file status" in result
|
|
|
|
def test_format_delete_error_filesystem_error(self, test_project):
|
|
"""Test formatting for filesystem errors."""
|
|
result = _format_delete_error_response(test_project.name, "filesystem error", "test-note")
|
|
|
|
assert "# Delete Failed - System Error" in result
|
|
assert "A system error occurred while deleting 'test-note'" in result
|
|
|
|
def test_format_delete_error_disk_error(self, test_project):
|
|
"""Test formatting for disk errors."""
|
|
result = _format_delete_error_response(test_project.name, "disk full", "test-note")
|
|
|
|
assert "# Delete Failed - System Error" in result
|
|
assert "A system error occurred while deleting 'test-note'" in result
|
|
|
|
def test_format_delete_error_database_error(self, test_project):
|
|
"""Test formatting for database errors."""
|
|
result = _format_delete_error_response(test_project.name, "database error", "test-note")
|
|
|
|
assert "# Delete Failed - Database Error" in result
|
|
assert "A database error occurred while deleting 'test-note'" in result
|
|
assert "Sync conflict" in result
|
|
assert "Database lock" in result
|
|
|
|
def test_format_delete_error_sync_error(self, test_project):
|
|
"""Test formatting for sync errors."""
|
|
result = _format_delete_error_response(test_project.name, "sync failed", "test-note")
|
|
|
|
assert "# Delete Failed - Database Error" in result
|
|
assert "A database error occurred while deleting 'test-note'" in result
|
|
|
|
def test_format_delete_error_generic(self, test_project):
|
|
"""Test formatting for generic errors."""
|
|
result = _format_delete_error_response(test_project.name, "unknown error", "test-note")
|
|
|
|
assert "# Delete Failed" in result
|
|
assert "Error deleting note 'test-note': unknown error" in result
|
|
assert "General troubleshooting" in result
|
|
assert "Verify the note exists" in result
|
|
|
|
def test_format_delete_error_with_complex_identifier(self, test_project):
|
|
"""Test formatting with complex identifiers (permalinks)."""
|
|
result = _format_delete_error_response(
|
|
test_project.name, "entity not found", "folder/note-title"
|
|
)
|
|
|
|
assert 'search_notes("test-project", "note-title")' in result
|
|
assert "Note Title" in result # Title format
|
|
assert "folder/note-title" in result # Permalink format
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_note_rejects_fuzzy_match(client, test_project):
|
|
"""delete_note must reject nonexistent identifiers, not fuzzy-match to a similar note."""
|
|
await write_note(
|
|
project=test_project.name,
|
|
title="Delete Target Note",
|
|
directory="test",
|
|
content="# Delete Target Note\nShould not be deleted.",
|
|
)
|
|
|
|
# Attempt to delete a nonexistent note — should return False, not silently delete the existing note
|
|
result = await delete_note(
|
|
project=test_project.name,
|
|
identifier="Delete Target NONEXISTENT",
|
|
)
|
|
|
|
# Should indicate not found (False or error string)
|
|
assert result is False or (isinstance(result, str) and "not found" in result.lower())
|
|
|
|
# Verify the existing note was NOT deleted
|
|
content = await read_note("Delete Target Note", project=test_project.name)
|
|
assert "Should not be deleted" in content
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_note_detects_project_from_memory_url(client, test_project):
|
|
"""delete_note should detect project from memory:// URL prefix when project=None."""
|
|
# Create a note to delete
|
|
await write_note(
|
|
project=test_project.name,
|
|
title="Delete URL Note",
|
|
directory="test",
|
|
content="# Delete URL Note\nContent to delete.",
|
|
)
|
|
|
|
# Delete using memory:// URL with project=None — should auto-detect project
|
|
# The note may or may not be found (depends on URL resolution), but the key
|
|
# assertion is that routing goes to the correct project
|
|
result = await delete_note(
|
|
identifier=f"memory://{test_project.name}/test/delete-url-note",
|
|
project=None,
|
|
)
|
|
|
|
# Result is True (deleted) or False (not found by that URL) — either is acceptable.
|
|
# The important thing is it didn't error and routed to the correct project.
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_note_skips_detection_for_plain_path(client, test_project):
|
|
"""delete_note should NOT call detect_project_from_memory_url_prefix for plain paths.
|
|
|
|
A plain path like 'research/note' should not be misrouted to a project
|
|
named 'research' — the 'research' segment is a directory, not a project.
|
|
"""
|
|
with patch(
|
|
"basic_memory.mcp.tools.delete_note.detect_project_from_memory_url_prefix"
|
|
) as mock_detect:
|
|
# Use a plain path (no memory:// prefix) — detection should not be called
|
|
await delete_note(
|
|
identifier="test/nonexistent-note",
|
|
project=None,
|
|
)
|
|
|
|
mock_detect.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_note_skips_detection_when_project_provided(client, test_project):
|
|
"""delete_note should skip URL detection when project is explicitly provided."""
|
|
with patch(
|
|
"basic_memory.mcp.tools.delete_note.detect_project_from_memory_url_prefix"
|
|
) as mock_detect:
|
|
await delete_note(
|
|
identifier=f"memory://{test_project.name}/test/some-note",
|
|
project=test_project.name,
|
|
)
|
|
|
|
mock_detect.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_directory_memory_url_strips_project_prefix(client, test_project):
|
|
"""Directory deletes use project-relative file paths after memory:// routing."""
|
|
await write_note(
|
|
project=test_project.name,
|
|
title="Delete Directory Memory URL",
|
|
directory="memory-url-dir",
|
|
content="# Delete Directory Memory URL\nDelete via project-prefixed memory URL.",
|
|
)
|
|
|
|
result = await delete_note(
|
|
identifier=f"memory://{test_project.name}/memory-url-dir",
|
|
is_directory=True,
|
|
project=test_project.name,
|
|
output_format="json",
|
|
)
|
|
|
|
assert isinstance(result, dict)
|
|
assert result["deleted"] is True
|
|
assert result["total_files"] == 1
|
|
assert result["successful_deletes"] == 1
|
|
assert result["failed_deletes"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_directory_memory_url_keeps_real_project_prefix_when_prefixes_disabled(
|
|
client,
|
|
test_project,
|
|
config_manager,
|
|
):
|
|
"""When project prefixes are disabled, resolved directory paths are already project-relative."""
|
|
config = config_manager.load_config()
|
|
config.permalinks_include_project = False
|
|
config_manager.save_config(config)
|
|
|
|
real_directory = f"{test_project.name}/archive"
|
|
await write_note(
|
|
project=test_project.name,
|
|
title="Delete Directory Real Project Prefix",
|
|
directory=real_directory,
|
|
content="# Delete Directory Real Project Prefix\nDelete target content.",
|
|
)
|
|
await write_note(
|
|
project=test_project.name,
|
|
title="Keep Directory Decoy",
|
|
directory="archive",
|
|
content="# Keep Directory Decoy\nDecoy content.",
|
|
)
|
|
|
|
result = await delete_note(
|
|
identifier=f"memory://{test_project.name}/{real_directory}",
|
|
is_directory=True,
|
|
project=test_project.name,
|
|
output_format="json",
|
|
)
|
|
|
|
assert isinstance(result, dict)
|
|
assert result["deleted"] is True
|
|
assert result["total_files"] == 1
|
|
assert result["successful_deletes"] == 1
|
|
assert result["failed_deletes"] == 0
|
|
|
|
deleted = await read_note("Delete Directory Real Project Prefix", project=test_project.name)
|
|
assert "Delete target content" not in deleted
|
|
decoy = await read_note("Keep Directory Decoy", project=test_project.name)
|
|
assert "Decoy content" in decoy
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_directory_workspace_memory_url_strips_route_prefix(client, test_project):
|
|
"""Workspace-qualified memory:// directory deletes still hit project-relative files."""
|
|
from basic_memory.workspace_context import workspace_permalink_context
|
|
|
|
directory = "workspace-memory-url-dir"
|
|
with workspace_permalink_context(workspace_slug="team-paul", workspace_type="organization"):
|
|
await write_note(
|
|
project=test_project.name,
|
|
title="Delete Workspace Directory Memory URL",
|
|
directory=directory,
|
|
content="# Delete Workspace Directory Memory URL\nDelete via workspace memory URL.",
|
|
)
|
|
|
|
result = await delete_note(
|
|
identifier=f"memory://team-paul/{test_project.name}/{directory}",
|
|
is_directory=True,
|
|
project=test_project.name,
|
|
output_format="json",
|
|
)
|
|
|
|
assert isinstance(result, dict)
|
|
assert result["deleted"] is True
|
|
assert result["total_files"] == 1
|
|
assert result["successful_deletes"] == 1
|
|
assert result["failed_deletes"] == 0
|