mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
35884ef3a7
FastMCP library changes now require calling decorated functions via the .fn attribute: - Tools: @mcp.tool() functions return FunctionTool, call with tool.fn() - Prompts: @mcp.prompt() functions return FunctionPrompt, call with prompt.fn() - Resources: @mcp.resource() functions return FunctionResource, call with resource.fn() Updated core files: - view_note.py: read_note() → read_note.fn() - read_note.py: search_notes() → search_notes.fn() (2 locations) - tool.py: 6 MCP tool calls updated to use .fn - recent_activity.py: recent_activity() → recent_activity.fn() - project.py: project_info() → project_info.fn() with type ignore Updated 100+ test files systematically to use .fn attribute and fixed mock targets. All 869 tests now pass. Fixes view_note tool error in Claude Desktop. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
416 lines
11 KiB
Python
416 lines
11 KiB
Python
"""Tests for note tools that exercise the full stack with SQLite."""
|
|
|
|
from textwrap import dedent
|
|
import pytest
|
|
|
|
from basic_memory.mcp.tools import write_note, read_note, delete_note
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note(app):
|
|
"""Test creating a new note.
|
|
|
|
Should:
|
|
- Create entity with correct type and content
|
|
- Save markdown content
|
|
- Handle tags correctly
|
|
- Return valid permalink
|
|
"""
|
|
result = await write_note.fn(
|
|
title="Test Note",
|
|
folder="test",
|
|
content="# Test\nThis is a test note",
|
|
tags=["test", "documentation"],
|
|
)
|
|
|
|
assert result
|
|
assert "# Created note" in result
|
|
assert "file_path: test/Test Note.md" in result
|
|
assert "permalink: test/test-note" in result
|
|
assert "## Tags" in result
|
|
assert "- test, documentation" in result
|
|
|
|
# Try reading it back via permalink
|
|
content = await read_note.fn("test/test-note")
|
|
assert (
|
|
dedent("""
|
|
---
|
|
title: Test Note
|
|
type: note
|
|
permalink: test/test-note
|
|
tags:
|
|
- test
|
|
- documentation
|
|
---
|
|
|
|
# Test
|
|
This is a test note
|
|
""").strip()
|
|
in content
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_no_tags(app):
|
|
"""Test creating a note without tags."""
|
|
result = await write_note.fn(title="Simple Note", folder="test", content="Just some text")
|
|
|
|
assert result
|
|
assert "# Created note" in result
|
|
assert "file_path: test/Simple Note.md" in result
|
|
assert "permalink: test/simple-note" in result
|
|
# Should be able to read it back
|
|
content = await read_note.fn("test/simple-note")
|
|
assert (
|
|
dedent("""
|
|
--
|
|
title: Simple Note
|
|
type: note
|
|
permalink: test/simple-note
|
|
---
|
|
|
|
Just some text
|
|
""").strip()
|
|
in content
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_update_existing(app):
|
|
"""Test creating a new note.
|
|
|
|
Should:
|
|
- Create entity with correct type and content
|
|
- Save markdown content
|
|
- Handle tags correctly
|
|
- Return valid permalink
|
|
"""
|
|
result = await write_note.fn(
|
|
title="Test Note",
|
|
folder="test",
|
|
content="# Test\nThis is a test note",
|
|
tags=["test", "documentation"],
|
|
)
|
|
|
|
assert result # Got a valid permalink
|
|
assert "# Created note" in result
|
|
assert "file_path: test/Test Note.md" in result
|
|
assert "permalink: test/test-note" in result
|
|
assert "## Tags" in result
|
|
assert "- test, documentation" in result
|
|
|
|
result = await write_note.fn(
|
|
title="Test Note",
|
|
folder="test",
|
|
content="# Test\nThis is an updated note",
|
|
tags=["test", "documentation"],
|
|
)
|
|
assert "# Updated note" in result
|
|
assert "file_path: test/Test Note.md" in result
|
|
assert "permalink: test/test-note" in result
|
|
assert "## Tags" in result
|
|
assert "- test, documentation" in result
|
|
|
|
# Try reading it back
|
|
content = await read_note.fn("test/test-note")
|
|
assert (
|
|
dedent(
|
|
"""
|
|
---
|
|
title: Test Note
|
|
type: note
|
|
permalink: test/test-note
|
|
tags:
|
|
- test
|
|
- documentation
|
|
---
|
|
|
|
# Test
|
|
This is an updated note
|
|
"""
|
|
).strip()
|
|
== content
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_issue_93_write_note_respects_custom_permalink_new_note(app):
|
|
"""Test that write_note respects custom permalinks in frontmatter for new notes (Issue #93)"""
|
|
|
|
# Create a note with custom permalink in frontmatter
|
|
content_with_custom_permalink = dedent("""
|
|
---
|
|
permalink: custom/my-desired-permalink
|
|
---
|
|
|
|
# My New Note
|
|
|
|
This note has a custom permalink specified in frontmatter.
|
|
|
|
- [note] Testing if custom permalink is respected
|
|
""").strip()
|
|
|
|
result = await write_note.fn(
|
|
title="My New Note",
|
|
folder="notes",
|
|
content=content_with_custom_permalink,
|
|
)
|
|
|
|
# Verify the custom permalink is respected
|
|
assert "# Created note" in result
|
|
assert "file_path: notes/My New Note.md" in result
|
|
assert "permalink: custom/my-desired-permalink" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_issue_93_write_note_respects_custom_permalink_existing_note(app):
|
|
"""Test that write_note respects custom permalinks when updating existing notes (Issue #93)"""
|
|
|
|
# Step 1: Create initial note (auto-generated permalink)
|
|
result1 = await write_note.fn(
|
|
title="Existing Note",
|
|
folder="test",
|
|
content="Initial content without custom permalink",
|
|
)
|
|
|
|
assert "# Created note" in result1
|
|
|
|
# Extract the auto-generated permalink
|
|
initial_permalink = None
|
|
for line in result1.split("\n"):
|
|
if line.startswith("permalink:"):
|
|
initial_permalink = line.split(":", 1)[1].strip()
|
|
break
|
|
|
|
assert initial_permalink is not None
|
|
|
|
# Step 2: Update with content that includes custom permalink in frontmatter
|
|
updated_content = dedent("""
|
|
---
|
|
permalink: custom/new-permalink
|
|
---
|
|
|
|
# Existing Note
|
|
|
|
Updated content with custom permalink in frontmatter.
|
|
|
|
- [note] Custom permalink should be respected on update
|
|
""").strip()
|
|
|
|
result2 = await write_note.fn(
|
|
title="Existing Note",
|
|
folder="test",
|
|
content=updated_content,
|
|
)
|
|
|
|
# Verify the custom permalink is respected
|
|
assert "# Updated note" in result2
|
|
assert "permalink: custom/new-permalink" in result2
|
|
assert f"permalink: {initial_permalink}" not in result2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_note_existing(app):
|
|
"""Test deleting a new note.
|
|
|
|
Should:
|
|
- Create entity with correct type and content
|
|
- Return valid permalink
|
|
- Delete the note
|
|
"""
|
|
result = await write_note.fn(
|
|
title="Test Note",
|
|
folder="test",
|
|
content="# Test\nThis is a test note",
|
|
tags=["test", "documentation"],
|
|
)
|
|
|
|
assert result
|
|
|
|
deleted = await delete_note.fn("test/test-note")
|
|
assert deleted is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_note_doesnt_exist(app):
|
|
"""Test deleting a new note.
|
|
|
|
Should:
|
|
- Delete the note
|
|
- verify returns false
|
|
"""
|
|
deleted = await delete_note.fn("doesnt-exist")
|
|
assert deleted is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_with_tag_array_from_bug_report(app):
|
|
"""Test creating a note with a tag array as reported in issue #38.
|
|
|
|
This reproduces the exact payload from the bug report where Cursor
|
|
was passing an array of tags and getting a type mismatch error.
|
|
"""
|
|
# This is the exact payload from the bug report
|
|
bug_payload = {
|
|
"title": "Title",
|
|
"folder": "folder",
|
|
"content": "CONTENT",
|
|
"tags": ["hipporag", "search", "fallback", "symfony", "error-handling"],
|
|
}
|
|
|
|
# Try to call the function with this data directly
|
|
result = await write_note.fn(**bug_payload)
|
|
|
|
assert result
|
|
assert "permalink: folder/title" in result
|
|
assert "Tags" in result
|
|
assert "hipporag" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_verbose(app):
|
|
"""Test creating a new note.
|
|
|
|
Should:
|
|
- Create entity with correct type and content
|
|
- Save markdown content
|
|
- Handle tags correctly
|
|
- Return valid permalink
|
|
"""
|
|
result = await write_note.fn(
|
|
title="Test Note",
|
|
folder="test",
|
|
content="""
|
|
# Test\nThis is a test note
|
|
|
|
- [note] First observation
|
|
- relates to [[Knowledge]]
|
|
|
|
""",
|
|
tags=["test", "documentation"],
|
|
)
|
|
|
|
assert "# Created note" in result
|
|
assert "file_path: test/Test Note.md" in result
|
|
assert "permalink: test/test-note" in result
|
|
assert "## Observations" in result
|
|
assert "- note: 1" in result
|
|
assert "## Relations" in result
|
|
assert "## Tags" in result
|
|
assert "- test, documentation" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_preserves_custom_metadata(app, project_config):
|
|
"""Test that updating a note preserves custom metadata fields.
|
|
|
|
Reproduces issue #36 where custom frontmatter fields like Status
|
|
were being lost when updating notes with the write_note tool.
|
|
|
|
Should:
|
|
- Create a note with custom frontmatter
|
|
- Update the note with new content
|
|
- Verify custom frontmatter is preserved
|
|
"""
|
|
# First, create a note with custom metadata using write_note
|
|
await write_note.fn(
|
|
title="Custom Metadata Note",
|
|
folder="test",
|
|
content="# Initial content",
|
|
tags=["test"],
|
|
)
|
|
|
|
# Read the note to get its permalink
|
|
content = await read_note.fn("test/custom-metadata-note")
|
|
|
|
# Now directly update the file with custom frontmatter
|
|
# We need to use a direct file update to add custom frontmatter
|
|
import frontmatter
|
|
|
|
file_path = project_config.home / "test" / "Custom Metadata Note.md"
|
|
post = frontmatter.load(file_path)
|
|
|
|
# Add custom frontmatter
|
|
post["Status"] = "In Progress"
|
|
post["Priority"] = "High"
|
|
post["Version"] = "1.0"
|
|
|
|
# Write the file back
|
|
with open(file_path, "w") as f:
|
|
f.write(frontmatter.dumps(post))
|
|
|
|
# Now update the note using write_note
|
|
result = await write_note.fn(
|
|
title="Custom Metadata Note",
|
|
folder="test",
|
|
content="# Updated content",
|
|
tags=["test", "updated"],
|
|
)
|
|
|
|
# Verify the update was successful
|
|
assert ("Updated note\nfile_path: test/Custom Metadata Note.md") in result
|
|
|
|
# Read the note back and check if custom frontmatter is preserved
|
|
content = await read_note.fn("test/custom-metadata-note")
|
|
|
|
# Custom frontmatter should be preserved
|
|
assert "Status: In Progress" in content
|
|
assert "Priority: High" in content
|
|
# Version might be quoted as '1.0' due to YAML serialization
|
|
assert "Version:" in content # Just check that the field exists
|
|
assert "1.0" in content # And that the value exists somewhere
|
|
|
|
# And new content should be there
|
|
assert "# Updated content" in content
|
|
|
|
# And tags should be updated (without # prefix)
|
|
assert "- test" in content
|
|
assert "- updated" in content
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_preserves_content_frontmatter(app):
|
|
"""Test creating a new note."""
|
|
await write_note.fn(
|
|
title="Test Note",
|
|
folder="test",
|
|
content=dedent(
|
|
"""
|
|
---
|
|
title: Test Note
|
|
type: note
|
|
version: 1.0
|
|
author: name
|
|
---
|
|
# Test
|
|
|
|
This is a test note
|
|
"""
|
|
),
|
|
tags=["test", "documentation"],
|
|
)
|
|
|
|
# Try reading it back via permalink
|
|
content = await read_note.fn("test/test-note")
|
|
assert (
|
|
dedent(
|
|
"""
|
|
---
|
|
title: Test Note
|
|
type: note
|
|
permalink: test/test-note
|
|
version: 1.0
|
|
author: name
|
|
tags:
|
|
- test
|
|
- documentation
|
|
---
|
|
|
|
# Test
|
|
|
|
This is a test note
|
|
"""
|
|
).strip()
|
|
in content
|
|
)
|