Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/basic_memory/file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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 "<unknown>",
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 "<unknown>",
error=str(e),
)
raise FileError(f"Failed to update frontmatter: {e}")


Expand Down
31 changes: 31 additions & 0 deletions tests/utils/test_file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading