mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
74e12eb782
Signed-off-by: Brandon Mayes <5610870+bdmayes@users.noreply.github.com>
349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""
|
|
Integration tests for write_note MCP tool.
|
|
|
|
Comprehensive tests covering all scenarios including note creation, content formatting,
|
|
tag handling, error conditions, and edge cases from bug reports.
|
|
"""
|
|
|
|
from textwrap import dedent
|
|
|
|
import pytest
|
|
from fastmcp import Client
|
|
from unittest.mock import patch
|
|
|
|
from basic_memory.config import ConfigManager
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_basic_creation(mcp_server, app):
|
|
"""Test creating a simple note with basic content."""
|
|
|
|
async with Client(mcp_server) as client:
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "Simple Note",
|
|
"folder": "basic",
|
|
"content": "# Simple Note\n\nThis is a simple note for testing.",
|
|
"tags": "simple,test",
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
assert result.content[0].type == "text"
|
|
response_text = result.content[0].text
|
|
|
|
assert "# Created note" in response_text
|
|
assert "file_path: basic/Simple Note.md" in response_text
|
|
assert "permalink: basic/simple-note" in response_text
|
|
assert "## Tags" in response_text
|
|
assert "- simple, test" in response_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_no_tags(mcp_server, app):
|
|
"""Test creating a note without tags."""
|
|
|
|
async with Client(mcp_server) as client:
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "No Tags Note",
|
|
"folder": "test",
|
|
"content": "Just some plain text without tags.",
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
assert result.content[0].type == "text"
|
|
response_text = result.content[0].text
|
|
|
|
assert "# Created note" in response_text
|
|
assert "file_path: test/No Tags Note.md" in response_text
|
|
assert "permalink: test/no-tags-note" in response_text
|
|
# Should not have tags section when no tags provided
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_update_existing(mcp_server, app):
|
|
"""Test updating an existing note."""
|
|
|
|
async with Client(mcp_server) as client:
|
|
# Create initial note
|
|
result1 = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "Update Test",
|
|
"folder": "test",
|
|
"content": "# Update Test\n\nOriginal content.",
|
|
"tags": "original",
|
|
},
|
|
)
|
|
|
|
assert "# Created note" in result1.content[0].text
|
|
|
|
# Update the same note
|
|
result2 = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "Update Test",
|
|
"folder": "test",
|
|
"content": "# Update Test\n\nUpdated content with changes.",
|
|
"tags": "updated,modified",
|
|
},
|
|
)
|
|
|
|
assert len(result2.content) == 1
|
|
assert result2.content[0].type == "text"
|
|
response_text = result2.content[0].text
|
|
|
|
assert "# Updated note" in response_text
|
|
assert "file_path: test/Update Test.md" in response_text
|
|
assert "permalink: test/update-test" in response_text
|
|
assert "- updated, modified" in response_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_tag_array(mcp_server, app):
|
|
"""Test creating a note with tag array (Issue #38 regression test)."""
|
|
|
|
async with Client(mcp_server) as client:
|
|
# This reproduces the exact bug from Issue #38
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "Array Tags Test",
|
|
"folder": "test",
|
|
"content": "Testing tag array handling",
|
|
"tags": ["python", "testing", "integration", "mcp"],
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
assert result.content[0].type == "text"
|
|
response_text = result.content[0].text
|
|
|
|
assert "# Created note" in response_text
|
|
assert "file_path: test/Array Tags Test.md" in response_text
|
|
assert "permalink: test/array-tags-test" in response_text
|
|
assert "## Tags" in response_text
|
|
assert "python" in response_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_custom_permalink(mcp_server, app):
|
|
"""Test custom permalink handling (Issue #93 regression test)."""
|
|
|
|
async with Client(mcp_server) as client:
|
|
content_with_custom_permalink = dedent("""
|
|
---
|
|
permalink: custom/my-special-permalink
|
|
---
|
|
|
|
# Custom Permalink Note
|
|
|
|
This note has a custom permalink in frontmatter.
|
|
|
|
- [note] Testing custom permalink preservation
|
|
""").strip()
|
|
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "Custom Permalink Note",
|
|
"folder": "notes",
|
|
"content": content_with_custom_permalink,
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
assert result.content[0].type == "text"
|
|
response_text = result.content[0].text
|
|
|
|
assert "# Created note" in response_text
|
|
assert "file_path: notes/Custom Permalink Note.md" in response_text
|
|
assert "permalink: custom/my-special-permalink" in response_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_unicode_content(mcp_server, app):
|
|
"""Test handling unicode content including emojis."""
|
|
|
|
async with Client(mcp_server) as client:
|
|
unicode_content = "# Unicode Test 🚀\n\nThis note has emoji 🎉 and unicode ♠♣♥♦\n\n- [note] Testing unicode handling 测试"
|
|
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "Unicode Test 🌟",
|
|
"folder": "test",
|
|
"content": unicode_content,
|
|
"tags": "unicode,emoji,测试",
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
assert result.content[0].type == "text"
|
|
response_text = result.content[0].text
|
|
|
|
assert "# Created note" in response_text
|
|
assert "file_path: test/Unicode Test 🌟.md" in response_text
|
|
# Permalink should be sanitized
|
|
assert "permalink: test/unicode-test" in response_text
|
|
assert "## Tags" in response_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_complex_content_with_observations_relations(mcp_server, app):
|
|
"""Test creating note with complex content including observations and relations."""
|
|
|
|
async with Client(mcp_server) as client:
|
|
complex_content = dedent("""
|
|
# Complex Note
|
|
|
|
This note demonstrates the full knowledge format.
|
|
|
|
## Observations
|
|
- [tech] Uses Python and FastAPI
|
|
- [design] Follows MCP protocol specification
|
|
- [note] Integration tests are comprehensive
|
|
|
|
## Relations
|
|
- implements [[MCP Protocol]]
|
|
- depends_on [[FastAPI Framework]]
|
|
- tested_by [[Integration Tests]]
|
|
|
|
## Additional Content
|
|
|
|
Some more regular markdown content here.
|
|
""").strip()
|
|
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "Complex Knowledge Note",
|
|
"folder": "knowledge",
|
|
"content": complex_content,
|
|
"tags": "complex,knowledge,relations",
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
assert result.content[0].type == "text"
|
|
response_text = result.content[0].text
|
|
|
|
assert "# Created note" in response_text
|
|
assert "file_path: knowledge/Complex Knowledge Note.md" in response_text
|
|
assert "permalink: knowledge/complex-knowledge-note" in response_text
|
|
|
|
# Should show observation and relation counts
|
|
assert "## Observations" in response_text
|
|
assert "tech: 1" in response_text
|
|
assert "design: 1" in response_text
|
|
assert "note: 1" in response_text
|
|
|
|
assert "## Relations" in response_text
|
|
# Should show outgoing relations
|
|
|
|
assert "## Tags" in response_text
|
|
assert "complex, knowledge, relations" in response_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_preserve_frontmatter(mcp_server, app):
|
|
"""Test that custom frontmatter is preserved when updating notes."""
|
|
|
|
async with Client(mcp_server) as client:
|
|
content_with_frontmatter = dedent("""
|
|
---
|
|
title: Frontmatter Note
|
|
type: note
|
|
version: 1.0
|
|
author: Test Author
|
|
status: draft
|
|
---
|
|
|
|
# Frontmatter Note
|
|
|
|
This note has custom frontmatter that should be preserved.
|
|
""").strip()
|
|
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "Frontmatter Note",
|
|
"folder": "test",
|
|
"content": content_with_frontmatter,
|
|
"tags": "frontmatter,preservation",
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
assert result.content[0].type == "text"
|
|
response_text = result.content[0].text
|
|
|
|
assert "# Created note" in response_text
|
|
assert "file_path: test/Frontmatter Note.md" in response_text
|
|
assert "permalink: test/frontmatter-note" in response_text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_kebab_filenames_basic(mcp_server):
|
|
"""Test note creation with kebab_filenames=True and invalid filename characters."""
|
|
|
|
config = ConfigManager().config
|
|
curr_config_val = config.kebab_filenames
|
|
config.kebab_filenames = True
|
|
|
|
with patch.object(ConfigManager, "config", config):
|
|
async with Client(mcp_server) as client:
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": "My Note: With/Invalid|Chars?",
|
|
"folder": "my-folder",
|
|
"content": "Testing kebab-case and invalid characters.",
|
|
"tags": "kebab,invalid,filename",
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
response_text = result.content[0].text
|
|
|
|
# File path and permalink should be kebab-case and sanitized
|
|
assert "file_path: my-folder/my-note-with-invalid-chars.md" in response_text
|
|
assert "permalink: my-folder/my-note-with-invalid-chars" in response_text
|
|
|
|
# Restore original config value
|
|
config.kebab_filenames = curr_config_val
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_note_kebab_filenames_repeat_invalid(mcp_server):
|
|
"""Test note creation with multiple invalid and repeated characters."""
|
|
|
|
config = ConfigManager().config
|
|
curr_config_val = config.kebab_filenames
|
|
config.kebab_filenames = True
|
|
|
|
with patch.object(ConfigManager, "config", config):
|
|
async with Client(mcp_server) as client:
|
|
result = await client.call_tool(
|
|
"write_note",
|
|
{
|
|
"title": 'Crazy<>:"|?*Note/Name',
|
|
"folder": "my-folder",
|
|
"content": "Should be fully kebab-case and safe.",
|
|
"tags": "crazy,filename,test",
|
|
},
|
|
)
|
|
|
|
assert len(result.content) == 1
|
|
response_text = result.content[0].text
|
|
|
|
assert "file_path: my-folder/crazy-note-name.md" in response_text
|
|
assert "permalink: my-folder/crazy-note-name" in response_text
|
|
|
|
# Restore original config value
|
|
config.kebab_filenames = curr_config_val
|