"""Tests for markdown/utils.py - entity model conversion utilities."""
from datetime import datetime, timezone
from pathlib import Path
import pytest
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter, Observation
from basic_memory.markdown.utils import entity_model_from_markdown
from basic_memory.models import Entity
class TestEntityModelFromMarkdown:
"""Tests for entity_model_from_markdown function."""
def _create_markdown(
self,
title: str = "Test Entity",
entity_type: str = "note",
permalink: str = "test/test-entity",
created: datetime | None = None,
modified: datetime | None = None,
observations: list[Observation] | None = None,
) -> EntityMarkdown:
"""Helper to create test EntityMarkdown objects."""
now = datetime.now(timezone.utc)
return EntityMarkdown(
frontmatter=EntityFrontmatter(
title=title,
type=entity_type,
permalink=permalink,
),
content=f"# {title}\n\nTest content.",
observations=observations or [],
relations=[],
created=created or now,
modified=modified or now,
)
def test_new_entity_has_external_id(self):
"""Test that a new entity always gets an external_id set.
This is a regression test for GitHub issue #512 where SQLite failed
with NOT NULL constraint on external_id because SQLAlchemy's Python-side
default wasn't always evaluated.
"""
markdown = self._create_markdown()
file_path = Path("test/test-entity.md")
entity = entity_model_from_markdown(file_path, markdown)
# external_id must be set (non-None, non-empty)
assert entity.external_id is not None
assert entity.external_id != ""
# Should be a valid UUID format (36 chars with hyphens)
assert len(entity.external_id) == 36
assert entity.external_id.count("-") == 4
def test_existing_entity_preserves_external_id(self):
"""Test that an existing entity's external_id is preserved."""
markdown = self._create_markdown()
file_path = Path("test/test-entity.md")
# Create existing entity with known external_id
existing_external_id = "12345678-1234-1234-1234-123456789012"
existing_entity = Entity()
existing_entity.external_id = existing_external_id
entity = entity_model_from_markdown(file_path, markdown, entity=existing_entity)
# Should preserve the existing external_id
assert entity.external_id == existing_external_id
def test_entity_with_empty_external_id_gets_new_one(self):
"""Test that an entity with empty string external_id gets a new UUID."""
markdown = self._create_markdown()
file_path = Path("test/test-entity.md")
# Create existing entity with empty external_id
existing_entity = Entity()
existing_entity.external_id = ""
entity = entity_model_from_markdown(file_path, markdown, entity=existing_entity)
# Should have a new external_id
assert entity.external_id is not None
assert entity.external_id != ""
assert len(entity.external_id) == 36
def test_entity_with_none_external_id_gets_new_one(self):
"""Test that an entity with None external_id gets a new UUID."""
markdown = self._create_markdown()
file_path = Path("test/test-entity.md")
# Create existing entity with None external_id
existing_entity = Entity()
# Explicitly set to None to test this case
object.__setattr__(existing_entity, "external_id", None)
entity = entity_model_from_markdown(file_path, markdown, entity=existing_entity)
# Should have a new external_id
assert entity.external_id is not None
assert entity.external_id != ""
assert len(entity.external_id) == 36
def test_multiple_calls_generate_unique_ids(self):
"""Test that multiple new entities get unique external_ids."""
markdown1 = self._create_markdown(title="Entity 1", permalink="test/entity-1")
markdown2 = self._create_markdown(title="Entity 2", permalink="test/entity-2")
entity1 = entity_model_from_markdown(Path("test/entity-1.md"), markdown1)
entity2 = entity_model_from_markdown(Path("test/entity-2.md"), markdown2)
# Both should have external_ids
assert entity1.external_id is not None
assert entity2.external_id is not None
# They should be unique
assert entity1.external_id != entity2.external_id
def test_entity_model_fields_populated_with_external_id(self):
"""Test that entity fields are populated correctly, including external_id.
This is a basic sanity check that entity_model_from_markdown sets
the key fields we care about for the #512 fix.
"""
markdown = self._create_markdown(
title="My Test Entity",
entity_type="component",
permalink="components/my-test",
)
file_path = Path("components/my-test.md")
entity = entity_model_from_markdown(file_path, markdown, project_id=1)
# The key assertion: external_id must always be set
assert entity.external_id is not None
assert len(entity.external_id) == 36 # UUID format
# Verify file_path is set correctly (uses posix format)
assert entity.file_path == "components/my-test.md"
# Timestamps should be set from markdown
assert entity.created_at is not None
assert entity.updated_at is not None
def test_missing_dates_raises_error(self):
"""Test that missing created/modified dates raise ValueError."""
markdown = EntityMarkdown(
frontmatter=EntityFrontmatter(
title="Test",
type="note",
),
content="# Test",
observations=[],
relations=[],
created=None,
modified=None,
)
with pytest.raises(ValueError, match="Both created and modified dates are required"):
entity_model_from_markdown(Path("test.md"), markdown)