diff --git a/src/basic_memory/file_utils.py b/src/basic_memory/file_utils.py index fdb2dfa27..fa5c0678c 100644 --- a/src/basic_memory/file_utils.py +++ b/src/basic_memory/file_utils.py @@ -210,11 +210,20 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str: # Read current content content = path_obj.read_text(encoding="utf-8") - # Parse current frontmatter + # Parse current frontmatter with proper error handling for malformed YAML current_fm = {} if has_frontmatter(content): - current_fm = parse_frontmatter(content) - content = remove_frontmatter(content) + try: + current_fm = parse_frontmatter(content) + content = remove_frontmatter(content) + except (ParseError, yaml.YAMLError) as e: + # Log warning and treat as plain markdown without frontmatter + logger.warning( + f"Failed to parse YAML frontmatter in {path_obj}: {e}. " + "Treating file as plain markdown without frontmatter." + ) + # Keep full content, treat as having no frontmatter + current_fm = {} # Update frontmatter new_fm = {**current_fm, **updates} @@ -229,11 +238,13 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str: return await compute_checksum(final_content) except Exception as e: # pragma: no cover - logger.error( - "Failed to update frontmatter", - path=str(path) if isinstance(path, (str, Path)) else "", - error=str(e), - ) + # Only log real errors (not YAML parsing, which is handled above) + if not isinstance(e, (ParseError, yaml.YAMLError)): + logger.error( + "Failed to update frontmatter", + path=str(path) if isinstance(path, (str, Path)) else "", + error=str(e), + ) raise FileError(f"Failed to update frontmatter: {e}") diff --git a/tests/utils/test_file_utils.py b/tests/utils/test_file_utils.py index b00c04e32..c7526a0d5 100644 --- a/tests/utils/test_file_utils.py +++ b/tests/utils/test_file_utils.py @@ -259,6 +259,37 @@ async def test_update_frontmatter_errors(tmp_path: Path): await update_frontmatter(nonexistent, {"title": "Test"}) +@pytest.mark.asyncio +async def test_update_frontmatter_handles_malformed_yaml(tmp_path: Path): + """Test that update_frontmatter handles malformed YAML gracefully (issue #378).""" + test_file = tmp_path / "test.md" + + # Create file with malformed YAML frontmatter (colon in title breaks YAML) + content = """--- +title: KB: Something +--- + +# Test Content + +Some content here""" + test_file.write_text(content) + + # Should handle gracefully and treat as having no frontmatter + updates = {"title": "Fixed Title", "type": "note"} + await update_frontmatter(test_file, updates) + + # Verify file was updated successfully + updated = test_file.read_text(encoding="utf-8") + assert "title: Fixed Title" in updated + assert "type: note" in updated + assert "Test Content" in updated + assert "Some content here" in updated + + # Verify new frontmatter is valid + fm = parse_frontmatter(updated) + assert fm == updates + + @pytest.mark.asyncio def test_sanitize_for_filename_removes_invalid_characters(): # Test all invalid characters listed in the regex