mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
bd5923a370
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
299 lines
9.2 KiB
Python
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
|