From 1d98df895bf49df697306b300e72de904f315bec Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 22 Dec 2024 17:06:06 -0600 Subject: [PATCH] fixing markdown parsing tests --- src/basic_memory/markdown/base_parser.py | 14 ++- src/basic_memory/markdown/parser.py | 87 ++++++++----------- tests/markdown/test_metadata_parser.py | 53 ++++++----- tests/markdown/test_observation_edge_cases.py | 84 ++++++++++-------- tests/markdown/test_parse_entity_file.py | 29 ++++--- tests/markdown/test_parse_observation.py | 37 ++++---- tests/markdown/test_parse_relation.py | 27 +++--- tests/markdown/test_parser.py | 19 ++-- 8 files changed, 185 insertions(+), 165 deletions(-) diff --git a/src/basic_memory/markdown/base_parser.py b/src/basic_memory/markdown/base_parser.py index 9c2b43cb..afaa83dc 100644 --- a/src/basic_memory/markdown/base_parser.py +++ b/src/basic_memory/markdown/base_parser.py @@ -63,7 +63,7 @@ class MarkdownParser(ABC, Generic[T]): try: # Split into frontmatter and content frontmatter, markdown = await parse_frontmatter(content) - + # Extract metadata from frontmatter if present metadata = frontmatter.pop("metadata", None) @@ -83,9 +83,7 @@ class MarkdownParser(ABC, Generic[T]): # Create final document return await self.create_document( - frontmatter=parsed_frontmatter, - content=parsed_content, - metadata=parsed_metadata + frontmatter=parsed_frontmatter, content=parsed_content, metadata=parsed_metadata ) except Exception as e: @@ -106,7 +104,7 @@ class MarkdownParser(ABC, Generic[T]): """ # Initialize state title = None - sections: Dict[str, List[str]] = {} + sections: Dict[str, str] = {} current_section = None current_lines: List[str] = [] @@ -122,7 +120,7 @@ class MarkdownParser(ABC, Generic[T]): if current_section and current_lines: sections[current_section] = "\n".join(current_lines).strip() current_lines = [] - + # Start new section current_section = line[3:].strip().lower() continue @@ -149,7 +147,7 @@ class MarkdownParser(ABC, Generic[T]): pass @abstractmethod - async def parse_content(self, title: str, sections: Dict[str, List[str]]) -> Any: + async def parse_content(self, title: str, sections: Dict[str, str]) -> Any: """Parse content sections.""" pass @@ -161,4 +159,4 @@ class MarkdownParser(ABC, Generic[T]): @abstractmethod async def create_document(self, frontmatter: Any, content: Any, metadata: Optional[Any]) -> T: """Create document from parsed sections.""" - pass \ No newline at end of file + pass diff --git a/src/basic_memory/markdown/parser.py b/src/basic_memory/markdown/parser.py index a34e3a27..edbe0de2 100644 --- a/src/basic_memory/markdown/parser.py +++ b/src/basic_memory/markdown/parser.py @@ -1,6 +1,6 @@ """Parser for Basic Memory entity markdown files.""" -from typing import Dict, Any, Optional, List +from typing import Dict, Any, Optional from loguru import logger @@ -11,7 +11,7 @@ from basic_memory.markdown.schemas import ( EntityContent, EntityMetadata, Observation, - Relation + Relation, ) @@ -44,19 +44,19 @@ class EntityParser(MarkdownParser[Entity]): try: # Preprocess fields for schema validation processed = frontmatter.copy() - + # Ensure id is string - if 'id' in processed: - processed['id'] = str(processed['id']) + if "id" in processed: + processed["id"] = str(processed["id"]) # Handle tags field - if 'tags' in processed: - if isinstance(processed['tags'], str): + if "tags" in processed: + if isinstance(processed["tags"], str): # Split comma-separated tags and strip whitespace - processed['tags'] = [tag.strip() for tag in processed['tags'].split(',')] + processed["tags"] = [tag.strip() for tag in processed["tags"].split(",")] return EntityFrontmatter(**processed) - + except Exception as e: logger.error(f"Invalid entity frontmatter: {e}") raise ParseError(f"Invalid entity frontmatter: {str(e)}") from e @@ -85,10 +85,10 @@ class EntityParser(MarkdownParser[Entity]): observations = [] if "observations" not in sections: raise ParseError("Missing required observations section") - + for line in sections["observations"].splitlines(): if line and not line.isspace(): - observation = await self._parse_observation(line) + observation = await self.parse_observation(line) if observation: observations.append(observation) @@ -97,15 +97,12 @@ class EntityParser(MarkdownParser[Entity]): if "relations" in sections: for line in sections["relations"].splitlines(): if line and not line.isspace(): - relation = await self._parse_relation(line) + relation = await self.parse_relation(line) if relation: relations.append(relation) return EntityContent( - title=title, - description=description, - observations=observations, - relations=relations + title=title, description=description, observations=observations, relations=relations ) except ParseError: @@ -114,7 +111,7 @@ class EntityParser(MarkdownParser[Entity]): logger.error(f"Invalid entity content: {e}") raise ParseError(f"Invalid entity content: {str(e)}") from e - async def _parse_observation(self, line: str) -> Optional[Observation]: + async def parse_observation(self, line: str) -> Optional[Observation]: """ Parse a single observation line. @@ -127,47 +124,44 @@ class EntityParser(MarkdownParser[Entity]): # Extract category if present [category] category = None content = line - if line.startswith('['): - end_bracket = line.find(']') + if line.startswith("["): + end_bracket = line.find("]") if end_bracket != -1: category = line[1:end_bracket].strip() - content = line[end_bracket + 1:].strip() + content = line[end_bracket + 1 :].strip() # Extract context if present (context) context = None - if content.endswith(')'): - context_start = content.rfind('(') + if content.endswith(")"): + context_start = content.rfind("(") if context_start != -1: - context = content[context_start + 1:-1].strip() + context = content[context_start + 1 : -1].strip() content = content[:context_start].strip() # Extract tags #tag1 #tag2 tags = [] content_parts = [] for part in content.split(): - if part.startswith('#'): + if part.startswith("#"): tags.append(part[1:]) # Remove # prefix else: content_parts.append(part) - - content = ' '.join(content_parts).strip() - + + content = " ".join(content_parts).strip() + if not content: logger.warning(f"Skipping observation with no content: {line}") return None return Observation( - category=category, - content=content, - context=context, - tags=tags if tags else None + category=category, content=content, context=context, tags=tags if tags else None ) except Exception as e: logger.warning(f"Failed to parse observation '{line}': {e}") return None - async def _parse_relation(self, line: str) -> Optional[Relation]: + async def parse_relation(self, line: str) -> Optional[Relation]: """ Parse a single relation line. @@ -180,34 +174,30 @@ class EntityParser(MarkdownParser[Entity]): # Extract context if present (context) context = None main_part = line - if line.endswith(')'): - context_start = line.rfind('(') + if line.endswith(")"): + context_start = line.rfind("(") if context_start != -1: - context = line[context_start + 1:-1].strip() + context = line[context_start + 1 : -1].strip() main_part = line[:context_start].strip() # Extract relation type and target [[Entity]] - if '[[' not in main_part or ']]' not in main_part: + if "[[" not in main_part or "]]" not in main_part: logger.warning(f"Invalid relation format (missing [[]]): {line}") return None # Split into relation type and target - relation_parts = main_part.split('[[', 1) + relation_parts = main_part.split("[[", 1) relation_type = relation_parts[0].strip() if not relation_type: logger.warning(f"Missing relation type: {line}") return None - target = relation_parts[1].split(']]')[0].strip() + target = relation_parts[1].split("]]")[0].strip() if not target: logger.warning(f"Missing target entity: {line}") return None - return Relation( - type=relation_type, - target=target, - context=context - ) + return Relation(type=relation_type, target=target, context=context) except Exception as e: logger.warning(f"Failed to parse relation '{line}': {e}") @@ -235,10 +225,7 @@ class EntityParser(MarkdownParser[Entity]): raise ParseError(f"Invalid entity metadata: {str(e)}") from e async def create_document( - self, - frontmatter: EntityFrontmatter, - content: EntityContent, - metadata: EntityMetadata + self, frontmatter: EntityFrontmatter, content: EntityContent, metadata: EntityMetadata ) -> Entity: """ Create entity from parsed sections. @@ -251,8 +238,4 @@ class EntityParser(MarkdownParser[Entity]): Returns: Complete entity """ - return Entity( - frontmatter=frontmatter, - content=content, - metadata=metadata - ) \ No newline at end of file + return Entity(frontmatter=frontmatter, content=content, metadata=metadata) diff --git a/tests/markdown/test_metadata_parser.py b/tests/markdown/test_metadata_parser.py index 1bd9e9f1..540152a8 100644 --- a/tests/markdown/test_metadata_parser.py +++ b/tests/markdown/test_metadata_parser.py @@ -2,10 +2,13 @@ from textwrap import dedent -from basic_memory.markdown import EntityMetadata +import pytest + +from basic_memory.utils.file_utils import parse_frontmatter -def test_parse_metadata(): +@pytest.mark.asyncio +async def test_parse_metadata(): """Test parsing basic metadata.""" text = dedent(""" owner: team-auth @@ -13,22 +16,24 @@ def test_parse_metadata(): status: active """) - result = EntityMetadata.from_text(text) + result, remaining = await parse_frontmatter(text) - assert result.metadata["owner"] == "team-auth" - assert result.metadata["priority"] == "high" - assert result.metadata["status"] == "active" + assert result["owner"] == "team-auth" + assert result["priority"] == "high" + assert result["status"] == "active" -def test_parse_metadata_empty(): +@pytest.mark.asyncio +async def test_parse_metadata_empty(): """Test parsing empty metadata.""" text = "" - result = EntityMetadata.from_text(text) - assert result.metadata == {} + result, remaining = await parse_frontmatter(text) + assert result == {} -def test_parse_metadata_whitespace(): +@pytest.mark.asyncio +async def test_parse_metadata_whitespace(): """Test handling of various whitespace in metadata.""" text = dedent(""" owner: team-auth @@ -36,14 +41,15 @@ def test_parse_metadata_whitespace(): status: active """) - result = EntityMetadata.from_text(text) + result, remaining = await parse_frontmatter(text) - assert result.metadata["owner"] == "team-auth" - assert result.metadata["priority"] == "high" - assert result.metadata["status"] == "active" + assert result["owner"] == "team-auth" + assert result["priority"] == "high" + assert result["status"] == "active" -def test_parse_metadata_multiline_values(): +@pytest.mark.asyncio +async def test_parse_metadata_multiline_values(): """Test handling of multiline metadata values.""" text = dedent(""" owner: team-auth @@ -53,21 +59,22 @@ def test_parse_metadata_multiline_values(): status: active """) - result = EntityMetadata.from_text(text) + result, _ = await parse_frontmatter(text) - assert result.metadata["owner"] == "team-auth" - assert result.metadata["status"] == "active" - assert len(result.metadata["description"].splitlines()) == 3 + assert result["owner"] == "team-auth" + assert result["status"] == "active" + assert len(result["description"].splitlines()) == 3 -def test_parse_metadata_invalid(): +@pytest.mark.asyncio +async def test_parse_metadata_invalid(): """Test handling of invalid metadata format.""" text = dedent(""" owner team-auth priority: high """) - result = EntityMetadata.from_text(text) + result, _ = await parse_frontmatter(text) - assert "priority" in result.metadata - assert "owner" not in result.metadata + assert "priority" in result + assert "owner" not in result diff --git a/tests/markdown/test_observation_edge_cases.py b/tests/markdown/test_observation_edge_cases.py index 321de673..700b03ef 100644 --- a/tests/markdown/test_observation_edge_cases.py +++ b/tests/markdown/test_observation_edge_cases.py @@ -2,87 +2,103 @@ import pytest -from basic_memory.markdown import ParseError -from basic_memory.markdown.schemas import Observation +from basic_memory.markdown import ParseError, EntityParser -def test_observation_empty_input(): +@pytest.mark.asyncio +async def test_observation_empty_input(): """Test handling of empty input.""" - assert Observation.from_line("") is None - assert Observation.from_line(" ") is None - assert Observation.from_line("\n") is None + parser = EntityParser() + assert await parser.parse_observation("") is None + assert await parser.parse_observation(" ") is None + assert await parser.parse_observation("\n") is None -def test_observation_unicode(): +@pytest.mark.asyncio +async def test_observation_unicode(): """Test handling of Unicode content.""" # Invalid UTF-8 sequences - assert Observation.from_line("- [test] Bad UTF \xff") is None - assert Observation.from_line("- [test] Bad UTF \xfe") is None + parser = EntityParser() + assert await parser.parse_observation("- [test] Bad UTF \xff") is None + assert await parser.parse_observation("- [test] Bad UTF \xfe") is None # Control characters - assert Observation.from_line("- [test] With \x00 null") is None - assert Observation.from_line("- [test] With \x01 ctrl-a") is None - assert Observation.from_line("- [test] With \x1b escape") is None - assert Observation.from_line("- [test] With \x7f delete") is None - assert Observation.from_line("- [test] With " + chr(0x1F) + " unit sep") is None + assert await parser.parse_observation("- [test] With \x00 null") is None + assert await parser.parse_observation("- [test] With \x01 ctrl-a") is None + assert await parser.parse_observation("- [test] With \x1b escape") is None + assert await parser.parse_observation("- [test] With \x7f delete") is None + assert await parser.parse_observation("- [test] With " + chr(0x1F) + " unit sep") is None # Valid UTF-8 - obs = Observation.from_line("- [测试] Unicode content #标签") + obs = await parser.parse_observation("- [测试] Unicode content #标签") assert obs is not None assert obs.category == "测试" - assert "标签" in obs.tags + assert "标签" in obs.tags # pyright: ignore [reportOperatorIssue] -def test_observation_invalid_context(): +@pytest.mark.asyncio +async def test_observation_invalid_context(): """Test handling of invalid context format.""" - obs = Observation.from_line("- [test] Content (unclosed") + parser = EntityParser() + obs = await parser.parse_observation("- [test] Content (unclosed") assert obs is not None assert obs.content == "Content (unclosed" assert obs.context is None - obs = Observation.from_line("- [test] Content (with) extra) parens)") + obs = await parser.parse_observation("- [test] Content (with) extra) parens)") assert obs is not None assert obs.content == "Content" assert obs.context == "with) extra) parens" -def test_observation_complex_format(): +@pytest.mark.asyncio +async def test_observation_complex_format(): """Test parsing complex observation formats.""" # Test multiple nested tags and spaces - obs = Observation.from_line("- [complex test] This is #tag1#tag2 with #tag3 content") + parser = EntityParser() + obs = await parser.parse_observation("- [complex test] This is #tag1#tag2 with #tag3 content") assert obs is not None assert obs.category == "complex test" - assert set(obs.tags) == {"tag1", "tag2", "tag3"} + assert set(obs.tags) == {"tag1", "tag2", "tag3"} # pyright: ignore [reportArgumentType] assert obs.content == "This is with content" -def test_observation_exception_handling(): +@pytest.mark.asyncio +async def test_observation_exception_handling(): """Test general error handling in observation parsing.""" # Test with a problematic regex pattern that could cause catastrophic backtracking long_input = "[test] " + "a" * 1000000 # Very long input - assert Observation.from_line(long_input) is None + parser = EntityParser() + + assert await parser.parse_observation(long_input) is None # Test with invalid types - assert Observation.from_line(None) is None # type: ignore - assert Observation.from_line(123) is None # type: ignore - assert Observation.from_line(object()) is None # type: ignore + assert await parser.parse_observation(None) is None # type: ignore + assert await parser.parse_observation(123) is None # type: ignore + assert await parser.parse_observation(object()) is None # type: ignore -def test_observation_malformed_category(): +@pytest.mark.asyncio +async def test_observation_malformed_category(): """Test handling of malformed category brackets.""" + + parser = EntityParser() with pytest.raises(ParseError, match="unclosed category"): - Observation.from_line("- [test Content") + await parser.parse_observation("- [test Content") with pytest.raises(ParseError, match="missing category"): - Observation.from_line("- test] Content") + await parser.parse_observation("- test] Content") - assert Observation.from_line("- [] Empty category") is None + assert await parser.parse_observation("- [] Empty category") is None -def test_observation_whitespace(): +@pytest.mark.asyncio +async def test_observation_whitespace(): """Test handling of whitespace.""" # Valid whitespace cases - obs = Observation.from_line("- [test] Content") + + parser = EntityParser() + obs = await parser.parse_observation("- [test] Content") assert obs is not None assert obs.content == "Content" @@ -96,6 +112,6 @@ def test_observation_whitespace(): for char, name in test_chars.items(): content = f"- [test] Content{char}with{char}{name}" - obs = Observation.from_line(content) + obs = await parser.parse_observation(content) assert obs is not None assert obs.content == f"Content with {name}" diff --git a/tests/markdown/test_parse_entity_file.py b/tests/markdown/test_parse_entity_file.py index de60b905..4871ebfc 100644 --- a/tests/markdown/test_parse_entity_file.py +++ b/tests/markdown/test_parse_entity_file.py @@ -2,15 +2,18 @@ from textwrap import dedent +import pytest + from basic_memory.markdown.parser import EntityParser -def test_parse_complete_file(tmp_path): +@pytest.mark.asyncio +async def test_parse_complete_file(tmp_path): """Test parsing a complete entity file.""" content = dedent(""" --- type: component - id: component/auth_service + id: 123 created: 2024-12-21T14:00:00Z modified: 2024-12-21T14:00:00Z tags: authentication, security, core @@ -42,11 +45,11 @@ def test_parse_complete_file(tmp_path): test_file.write_text(content) parser = EntityParser() - entity = parser.parse_file(test_file) + entity = await parser.parse_file(test_file) # Check frontmatter assert entity.frontmatter.type == "component" - assert entity.frontmatter.id == "component/auth_service" + assert entity.frontmatter.id == "123" assert "authentication" in entity.frontmatter.tags # Check content @@ -57,7 +60,7 @@ def test_parse_complete_file(tmp_path): # Check specific observation obs = entity.content.observations[0] assert obs.category == "design" - assert "security" in obs.tags + assert "security" in obs.tags # pyright: ignore [reportOperatorIssue] assert obs.context == "JWT based" # Check specific relation @@ -71,15 +74,16 @@ def test_parse_complete_file(tmp_path): assert entity.metadata.metadata["priority"] == "high" -def test_parse_minimal_file(tmp_path): +@pytest.mark.asyncio +async def test_parse_minimal_file(tmp_path): """Test parsing a minimal valid entity file.""" content = dedent(""" --- type: component - id: minimal + id: 0 created: 2024-12-21T14:00:00Z modified: 2024-12-21T14:00:00Z - tags: [] + tags: --- # Minimal Entity @@ -95,16 +99,17 @@ def test_parse_minimal_file(tmp_path): test_file.write_text(content) parser = EntityParser() - entity = parser.parse_file(test_file) + entity = await parser.parse_file(test_file) assert entity.frontmatter.type == "component" - assert entity.frontmatter.id == "minimal" + assert entity.frontmatter.id == "0" assert len(entity.content.observations) == 1 assert len(entity.content.relations) == 1 assert not entity.metadata.metadata # Empty metadata -def test_file_with_metadata_only(tmp_path): +@pytest.mark.asyncio +async def test_file_with_metadata_only(tmp_path): """Test parsing a file that has metadata but no content.""" content = dedent(""" --- @@ -127,7 +132,7 @@ def test_file_with_metadata_only(tmp_path): test_file.write_text(content) parser = EntityParser() - entity = parser.parse_file(test_file) + entity = await parser.parse_file(test_file) assert entity.metadata.metadata["owner"] == "test-team" assert entity.metadata.metadata["status"] == "active" diff --git a/tests/markdown/test_parse_observation.py b/tests/markdown/test_parse_observation.py index a3a4de4b..ef4c5186 100644 --- a/tests/markdown/test_parse_observation.py +++ b/tests/markdown/test_parse_observation.py @@ -2,73 +2,76 @@ import pytest -from basic_memory.markdown import Observation from basic_memory.markdown.parser import EntityParser, ParseError -def test_parse_observation_basic(): +@pytest.mark.asyncio +async def test_parse_observation_basic(): """Test basic observation parsing with category and tags.""" parser = EntityParser() - obs = Observation.from_line("- [design] Core feature #important #mvp") + obs = await parser.parse_observation("- [design] Core feature #important #mvp") assert obs is not None assert obs.category == "design" assert obs.content == "Core feature" - assert set(obs.tags) == {"important", "mvp"} + assert set(obs.tags) == {"important", "mvp"} # pyright: ignore [reportArgumentType] assert obs.context is None -def test_parse_observation_with_context(): +@pytest.mark.asyncio +async def test_parse_observation_with_context(): """Test observation parsing with context in parentheses.""" parser = EntityParser() - obs = Observation.from_line( + obs = await parser.parse_observation( "- [feature] Authentication system #security #auth (Required for MVP)" ) assert obs is not None assert obs.category == "feature" assert obs.content == "Authentication system" - assert set(obs.tags) == {"security", "auth"} + assert set(obs.tags) == {"security", "auth"} # pyright: ignore [reportArgumentType] assert obs.context == "Required for MVP" -def test_parse_observation_edge_cases(): +@pytest.mark.asyncio +async def test_parse_observation_edge_cases(): """Test observation parsing edge cases.""" parser = EntityParser() # Multiple word tags - obs = Observation.from_line("- [tech] Database #high-priority #needs-review") + obs = await parser.parse_observation("- [tech] Database #high-priority #needs-review") assert obs is not None - assert set(obs.tags) == {"high-priority", "needs-review"} + assert set(obs.tags) == {"high-priority", "needs-review"} # pyright: ignore [reportArgumentType] # Multiple word category - obs = Observation.from_line("- [user experience] Design #ux") + obs = await parser.parse_observation("- [user experience] Design #ux") assert obs is not None assert obs.category == "user experience" # Parentheses in content shouldn't be treated as context - obs = Observation.from_line("- [code] Function (x) returns y #function") + obs = await parser.parse_observation("- [code] Function (x) returns y #function") assert obs is not None assert obs.content == "Function (x) returns y" assert obs.context is None # Multiple hashtags together - obs = Observation.from_line("- [test] Feature #important#urgent#now") + obs = await parser.parse_observation("- [test] Feature #important#urgent#now") assert obs is not None - assert set(obs.tags) == {"important", "urgent", "now"} + assert set(obs.tags) == {"important", "urgent", "now"} # pyright: ignore [reportArgumentType] -def test_parse_observation_errors(): +@pytest.mark.asyncio +async def test_parse_observation_errors(): """Test error handling in observation parsing.""" parser = EntityParser() # Missing category brackets with pytest.raises(ParseError, match="missing category"): - Observation.from_line("- Design without brackets #test") + await parser.parse_observation("- Design without brackets #test") # Unclosed category with pytest.raises(ParseError, match="unclosed category"): - Observation.from_line("- [design Core feature #test") + await parser.parse_observation("- [design Core feature #test") diff --git a/tests/markdown/test_parse_relation.py b/tests/markdown/test_parse_relation.py index 3082a602..9fc1f30d 100644 --- a/tests/markdown/test_parse_relation.py +++ b/tests/markdown/test_parse_relation.py @@ -2,61 +2,64 @@ import pytest -from basic_memory.markdown import Relation from basic_memory.markdown.parser import EntityParser, ParseError -def test_parse_relation_basic(): +@pytest.mark.asyncio +async def test_parse_relation_basic(): """Test basic relation parsing.""" parser = EntityParser() - rel = Relation.from_line("- implements [[Auth Service]]") + rel = await parser.parse_relation("- implements [[Auth Service]]") assert rel is not None assert rel.type == "implements" assert rel.target == "Auth Service" assert rel.context is None -def test_parse_relation_with_context(): +@pytest.mark.asyncio +async def test_parse_relation_with_context(): """Test relation parsing with context.""" parser = EntityParser() - rel = Relation.from_line("- depends_on [[Database]] (Required for persistence)") + rel = await parser.parse_relation("- depends_on [[Database]] (Required for persistence)") assert rel is not None assert rel.type == "depends_on" assert rel.target == "Database" assert rel.context == "Required for persistence" -def test_parse_relation_edge_cases(): +@pytest.mark.asyncio +async def test_parse_relation_edge_cases(): """Test relation parsing edge cases.""" parser = EntityParser() # Multiple word type - rel = Relation.from_line("- is used by [[Client App]] (Primary consumer)") + rel = await parser.parse_relation("- is used by [[Client App]] (Primary consumer)") assert rel is not None assert rel.type == "is used by" # Brackets in context - rel = Relation.from_line("- implements [[API]] (Follows [OpenAPI] spec)") + rel = await parser.parse_relation("- implements [[API]] (Follows [OpenAPI] spec)") assert rel is not None assert rel.context == "Follows [OpenAPI] spec" # Extra spaces - rel = Relation.from_line("- specifies [[Format]] (Documentation)") + rel = await parser.parse_relation("- specifies [[Format]] (Documentation)") assert rel is not None assert rel.type == "specifies" assert rel.target == "Format" -def test_parse_relation_errors(): +@pytest.mark.asyncio +async def test_parse_relation_errors(): """Test error handling in relation parsing.""" parser = EntityParser() # Missing target brackets with pytest.raises(ParseError, match="missing \\[\\["): - Relation.from_line("- implements Auth Service") + await parser.parse_relation("- implements Auth Service") # Unclosed target with pytest.raises(ParseError, match="missing ]]"): - Relation.from_line("- implements [[Auth Service") + await parser.parse_relation("- implements [[Auth Service") diff --git a/tests/markdown/test_parser.py b/tests/markdown/test_parser.py index 7d62cebf..776b0633 100644 --- a/tests/markdown/test_parser.py +++ b/tests/markdown/test_parser.py @@ -16,15 +16,13 @@ from basic_memory.utils.file_utils import ParseError, FileError @pytest.fixture def sample_entity_content(): """Sample entity file content.""" - return """--- -type: test + return """ +--- id: 123 +type: test created: 2024-12-22T10:00:00Z modified: 2024-12-22T10:00:00Z tags: entity, test -metadata: - checksum: abc123 - doc_id: 1 --- # Test Entity @@ -37,6 +35,13 @@ A test entity for testing purposes. ## Relations - depends_on [[Other Entity]] (Testing the relation parsing) + +--- +metadata: + checksum: abc123 + doc_id: 1 +--- + """ @@ -54,7 +59,7 @@ async def test_parse_valid_file(tmp_path: Path, sample_entity_content): # Verify frontmatter assert isinstance(entity.frontmatter, EntityFrontmatter) assert entity.frontmatter.type == "test" - assert entity.frontmatter.id == "test/test_entity" + assert entity.frontmatter.id == "123" # Verify content assert isinstance(entity.content, EntityContent) @@ -132,7 +137,7 @@ async def test_parse_content_str(sample_entity_content): assert isinstance(entity, Entity) assert entity.frontmatter.type == "test" - assert entity.frontmatter.id == "test/test_entity" + assert entity.frontmatter.id == "123" assert entity.content.title == "Test Entity" # Verify observations parsed correctly