Files
basicmachines-co-basic-memory/tests/mcp/test_tool_write_note_metadata.py
Paul Hernandez bd5923a370 feat: add overwrite guard to write_note tool (#632)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 16:12:38 -06:00

299 lines
9.2 KiB
Python

"""Tests for the write_note `metadata` parameter.
Covers positive, negative, and edge-case scenarios for passing arbitrary
frontmatter fields through note_metadata.
"""
import pytest
from basic_memory.mcp.tools import write_note, read_note
# --- Positive tests ---
@pytest.mark.asyncio
async def test_metadata_simple_keys(app, test_project):
"""Simple key-value metadata appears as top-level YAML frontmatter."""
result = await write_note(
project=test_project.name,
title="Simple Metadata",
directory="meta-tests",
content="# Simple Metadata\n\nBody text.",
metadata={"author": "Alice", "status": "draft"},
)
assert "# Created note" in result
content = await read_note("meta-tests/simple-metadata", project=test_project.name)
assert "author: Alice" in content
assert "status: draft" in content
@pytest.mark.asyncio
async def test_metadata_nested_dict(app, test_project):
"""Nested dict metadata renders as nested YAML."""
result = await write_note(
project=test_project.name,
title="Nested Metadata",
directory="meta-tests",
content="# Nested Metadata",
metadata={
"schema": {"name": "string", "role?": "string"},
"settings": {"validation": "warn"},
},
)
assert "# Created note" in result
content = await read_note("meta-tests/nested-metadata", project=test_project.name)
assert "schema:" in content
assert "name: string" in content
assert "role?: string" in content
assert "settings:" in content
assert "validation: warn" in content
@pytest.mark.asyncio
async def test_metadata_with_tags(app, test_project):
"""Metadata and tags coexist — both appear in frontmatter."""
result = await write_note(
project=test_project.name,
title="Tags And Metadata",
directory="meta-tests",
content="# Tags And Metadata",
tags=["one", "two"],
metadata={"priority": "high"},
)
assert "# Created note" in result
assert "## Tags" in result
content = await read_note("meta-tests/tags-and-metadata", project=test_project.name)
assert "priority: high" in content
assert "- one" in content
assert "- two" in content
@pytest.mark.asyncio
async def test_metadata_various_value_types(app, test_project):
"""Metadata values of int, bool, and list types survive round-trip."""
result = await write_note(
project=test_project.name,
title="Typed Values",
directory="meta-tests",
content="# Typed Values",
metadata={"version": 3, "active": True, "aliases": ["tv", "typed"]},
)
assert "# Created note" in result
content = await read_note("meta-tests/typed-values", project=test_project.name)
# YAML normalizes values to strings during frontmatter round-trip
assert "version:" in content
assert "active:" in content
assert "aliases:" in content
@pytest.mark.asyncio
async def test_metadata_survives_update(app, test_project):
"""Metadata set at create time persists through an update cycle."""
# Create with metadata
await write_note(
project=test_project.name,
title="Update Cycle",
directory="meta-tests",
content="# Version 1",
metadata={"author": "Bob", "version": 1},
)
# Update same note with new content + metadata
result = await write_note(
project=test_project.name,
title="Update Cycle",
directory="meta-tests",
content="# Version 2",
metadata={"author": "Bob", "version": 2},
overwrite=True,
)
assert "# Updated note" in result
content = await read_note("meta-tests/update-cycle", project=test_project.name)
assert "# Version 2" in content
assert "author: Bob" in content
assert "version:" in content
@pytest.mark.asyncio
async def test_metadata_with_note_type(app, test_project):
"""Metadata works together with a custom note_type."""
result = await write_note(
project=test_project.name,
title="Config Entry",
directory="meta-tests",
content="# Config Entry",
note_type="config",
metadata={"env": "production", "ttl": 300},
)
assert "# Created note" in result
content = await read_note("meta-tests/config-entry", project=test_project.name)
assert "type: config" in content
assert "env: production" in content
# --- Edge cases: empty / None ---
@pytest.mark.asyncio
async def test_metadata_none_default(app, test_project):
"""metadata=None (default) produces the same output as before the feature existed."""
result = await write_note(
project=test_project.name,
title="No Metadata",
directory="meta-tests",
content="# No Metadata",
tags=["plain"],
metadata=None,
)
assert "# Created note" in result
content = await read_note("meta-tests/no-metadata", project=test_project.name)
# Only standard keys should be present
assert "title: No Metadata" in content
assert "type: note" in content
assert "- plain" in content
@pytest.mark.asyncio
async def test_metadata_empty_dict(app, test_project):
"""metadata={} behaves identically to metadata=None."""
result = await write_note(
project=test_project.name,
title="Empty Dict",
directory="meta-tests",
content="# Empty Dict",
metadata={},
)
assert "# Created note" in result
content = await read_note("meta-tests/empty-dict", project=test_project.name)
assert "title: Empty Dict" in content
assert "type: note" in content
# --- Edge cases: key conflicts ---
@pytest.mark.asyncio
async def test_metadata_title_key_stripped(app, test_project):
"""A 'title' key in metadata does not override the title parameter.
schema_to_markdown pops 'title' from note_metadata so the Entity.title wins.
"""
result = await write_note(
project=test_project.name,
title="Real Title",
directory="meta-tests",
content="# Real Title",
metadata={"title": "Fake Title"},
)
assert "# Created note" in result
content = await read_note("meta-tests/real-title", project=test_project.name)
assert "title: Real Title" in content
assert "Fake Title" not in content
@pytest.mark.asyncio
async def test_metadata_type_key_stripped(app, test_project):
"""A 'type' key in metadata does not override note_type parameter.
schema_to_markdown pops 'type' from note_metadata so Entity.note_type wins.
"""
result = await write_note(
project=test_project.name,
title="Type Conflict",
directory="meta-tests",
content="# Type Conflict",
note_type="guide",
metadata={"type": "evil"},
)
assert "# Created note" in result
content = await read_note("meta-tests/type-conflict", project=test_project.name)
assert "type: guide" in content
assert "evil" not in content
@pytest.mark.asyncio
async def test_metadata_permalink_key_stripped(app, test_project):
"""A 'permalink' key in metadata does not hijack the canonical permalink.
schema_to_markdown pops 'permalink' from note_metadata.
"""
result = await write_note(
project=test_project.name,
title="Permalink Conflict",
directory="meta-tests",
content="# Permalink Conflict",
metadata={"permalink": "hacked/path"},
)
assert "# Created note" in result
# The canonical permalink should be based on title/directory, not the metadata value
assert "meta-tests/permalink-conflict" in result
content = await read_note("meta-tests/permalink-conflict", project=test_project.name)
assert "hacked/path" not in content
@pytest.mark.asyncio
async def test_tags_param_wins_over_metadata_tags(app, test_project):
"""When both tags param and metadata['tags'] are provided, the explicit param wins.
The explicit tags parameter is applied after metadata.update(), so it takes
precedence. The summary and file contents stay consistent.
"""
result = await write_note(
project=test_project.name,
title="Tags Override",
directory="meta-tests",
content="# Tags Override",
tags=["from-param"],
metadata={"tags": ["from-metadata"]},
)
assert "# Created note" in result
# Summary should reflect the winning tags
assert "from-param" in result
content = await read_note("meta-tests/tags-override", project=test_project.name)
# Explicit tags parameter wins over metadata tags key
assert "- from-param" in content
assert "from-metadata" not in content
@pytest.mark.asyncio
async def test_metadata_tags_key_works_when_no_tags_param(app, test_project):
"""When only metadata['tags'] is provided (no tags param), it is used."""
result = await write_note(
project=test_project.name,
title="Metadata Tags Only",
directory="meta-tests",
content="# Metadata Tags Only",
metadata={"tags": ["meta-tag-1", "meta-tag-2"]},
)
assert "# Created note" in result
content = await read_note("meta-tests/metadata-tags-only", project=test_project.name)
assert "- meta-tag-1" in content
assert "- meta-tag-2" in content