claude-os/tests/test_markdown_preprocessor.py

739 lines
19 KiB
Python
Raw Permalink Normal View History

2025-10-27 21:57:06 -05:00
"""
Tests for markdown preprocessor functionality.
"""
import pytest
import tempfile
from pathlib import Path
from unittest.mock import patch, mock_open
from app.core.markdown_preprocessor import (
extract_frontmatter,
extract_title,
normalize_headers,
clean_whitespace,
normalize_code_fences,
slugify,
preprocess_markdown
)
@pytest.mark.unit
class TestFrontmatterExtraction:
"""Test frontmatter extraction functionality."""
def test_extract_yaml_frontmatter(self):
"""Test extracting YAML frontmatter."""
text = """---
title: Test Document
author: John Doe
date: 2023-01-01
---
# Main Content
This is the main content of the document.
"""
frontmatter, content = extract_frontmatter(text)
assert frontmatter is not None
assert frontmatter["title"] == "Test Document"
assert frontmatter["author"] == "John Doe"
assert frontmatter["date"] == "2023-01-01"
assert content == """# Main Content
This is the main content of the document.
"""
def test_extract_toml_frontmatter(self):
"""Test extracting TOML frontmatter."""
text = """+++
title = "Test Document"
author = "Jane Doe"
date = "2023-01-01"
+++
# Main Content
This is the main content of the document.
"""
frontmatter, content = extract_frontmatter(text)
assert frontmatter is not None
assert frontmatter["title"] == "Test Document"
assert frontmatter["author"] == "Jane Doe"
assert frontmatter["date"] == "2023-01-01"
assert content == """# Main Content
This is the main content of the document.
"""
def test_extract_no_frontmatter(self):
"""Test extracting from document with no frontmatter."""
text = """# Main Content
This is the main content of the document.
"""
frontmatter, content = extract_frontmatter(text)
assert frontmatter is None
assert content == text
def test_extract_simple_frontmatter(self):
"""Test extracting simple key-value frontmatter."""
2025-10-27 21:57:06 -05:00
text = """---
title: Test Document
2025-10-27 21:57:06 -05:00
---
# Main Content
"""
frontmatter, content = extract_frontmatter(text)
assert frontmatter is not None
assert frontmatter["title"] == "Test Document"
2025-10-27 21:57:06 -05:00
assert content == "# Main Content\n"
def test_extract_frontmatter_with_quotes(self):
"""Test extracting frontmatter with quoted values."""
text = """---
title: "Test Document with Quotes"
description: A document with description
2025-10-27 21:57:06 -05:00
---
# Main Content
"""
frontmatter, content = extract_frontmatter(text)
assert frontmatter is not None
assert frontmatter["title"] == "Test Document with Quotes"
assert frontmatter["description"] == "A document with description"
2025-10-27 21:57:06 -05:00
def test_extract_frontmatter_with_colon_in_value(self):
"""Test extracting frontmatter with colon in value."""
text = """---
title: Document: With Colon
description: A description with: colon
---
# Main Content
"""
frontmatter, content = extract_frontmatter(text)
assert frontmatter is not None
assert frontmatter["title"] == "Document: With Colon"
assert frontmatter["description"] == "A description with: colon"
def test_extract_frontmatter_tags_as_string(self):
"""Test extracting frontmatter with tags as string (simple parser behavior)."""
2025-10-27 21:57:06 -05:00
text = """---
tags: test, example, demo
2025-10-27 21:57:06 -05:00
---
# Main Content
"""
frontmatter, content = extract_frontmatter(text)
assert frontmatter is not None
# Simple parser returns string, not parsed list
assert "test" in frontmatter["tags"]
assert "example" in frontmatter["tags"]
2025-10-27 21:57:06 -05:00
@pytest.mark.unit
class TestTitleExtraction:
"""Test title extraction functionality."""
def test_extract_title_from_h1(self):
"""Test extracting title from H1 header."""
text = """# Test Document Title
This is the main content.
"""
title = extract_title(text)
assert title == "Test Document Title"
def test_extract_title_from_first_h1(self):
"""Test extracting title from first H1 header."""
text = """# First Title
Some content.
# Second Title
More content.
"""
title = extract_title(text)
assert title == "First Title"
def test_extract_title_no_h1(self):
"""Test extracting title with no H1 header."""
text = """## Second Title
This is the main content.
"""
title = extract_title(text)
assert title is None
def test_extract_title_with_frontmatter(self):
"""Test extracting title with frontmatter."""
text = """---
title: Frontmatter Title
---
# H1 Title
Content.
"""
title = extract_title(text)
# Should prefer H1 over frontmatter
assert title == "H1 Title"
def test_extract_title_from_frontmatter_only(self):
"""Test extracting title with only frontmatter."""
text = """---
title: Frontmatter Title
---
Content without H1.
"""
title = extract_title(text)
# Should return None when no H1
assert title is None
def test_extract_title_with_whitespace(self):
"""Test extracting title with whitespace in header."""
text = """# Title with whitespace
Content.
"""
title = extract_title(text)
assert title == "Title with whitespace"
def test_extract_title_with_formatting(self):
"""Test extracting title with formatting in header."""
text = """# *Title with formatting*
Content.
"""
title = extract_title(text)
assert title == "*Title with formatting*"
@pytest.mark.unit
class TestHeaderNormalization:
"""Test header normalization functionality."""
def test_normalize_setext_h1_headers(self):
"""Test normalizing setext-style H1 headers."""
text = """Title One
========
Title Two
========
Content.
"""
normalized = normalize_headers(text)
assert "# Title One" in normalized
assert "# Title Two" in normalized
assert "========" not in normalized
def test_normalize_setext_h2_headers(self):
"""Test normalizing setext-style H2 headers."""
text = """Subtitle One
--------
Subtitle Two
--------
Content.
"""
normalized = normalize_headers(text)
assert "## Subtitle One" in normalized
assert "## Subtitle Two" in normalized
assert "--------" not in normalized
def test_normalize_mixed_headers(self):
"""Test normalizing mixed header styles."""
text = """Title One
========
## Subtitle Two
Title Three
========
### Subtitle Four
--------
Content.
"""
normalized = normalize_headers(text)
assert "# Title One" in normalized
assert "## Subtitle Two" in normalized
assert "# Title Three" in normalized
assert "========" not in normalized
# Note: "### Subtitle Four" followed by --- will become "## ### Subtitle Four"
2025-10-27 21:57:06 -05:00
assert "--------" not in normalized
def test_normalize_no_setext_headers(self):
"""Test normalizing document with no setext headers."""
text = """# Title One
## Subtitle Two
### Subtitle Three
Content.
"""
normalized = normalize_headers(text)
# Should remain unchanged
assert normalized == text
def test_normalize_underline_matches_header(self):
"""Test normalizing when underline characters are present."""
2025-10-27 21:57:06 -05:00
text = """Title
===
2025-10-27 21:57:06 -05:00
Content.
"""
normalized = normalize_headers(text)
# === (3 chars) should normalize H1
assert "# Title" in normalized
2025-10-27 21:57:06 -05:00
def test_normalize_empty_lines(self):
"""Test normalizing with empty lines."""
text = """Title One
========
Title Two
========
Content.
"""
normalized = normalize_headers(text)
assert "# Title One" in normalized
assert "# Title Two" in normalized
assert "========" not in normalized
@pytest.mark.unit
class TestWhitespaceCleaning:
"""Test whitespace cleaning functionality."""
def test_clean_whitespace_trailing_spaces(self):
"""Test cleaning trailing spaces."""
text = """Line 1
Line 2
Line 3
2025-10-27 21:57:06 -05:00
"""
cleaned = clean_whitespace(text)
lines = cleaned.split('\n')
assert "Line 1" in lines[0]
assert "Line 2" in lines[1]
2025-10-27 21:57:06 -05:00
def test_clean_whitespace_multiple_blank_lines(self):
"""Test cleaning multiple blank lines."""
text = """Line 1
Line 2
Line 3
"""
cleaned = clean_whitespace(text)
# Should collapse to max 2 blank lines
assert "\n\n\n\n" not in cleaned
assert "Line 1" in cleaned
assert "Line 2" in cleaned
assert "Line 3" in cleaned
2025-10-27 21:57:06 -05:00
def test_clean_whitespace_preserve_code_blocks(self):
"""Test cleaning whitespace while preserving code blocks."""
text = """Line 1
```python
def test():
print("Hello")
2025-10-27 21:57:06 -05:00
return True
```
Line 2
"""
cleaned = clean_whitespace(text)
assert "def test():" in cleaned
assert 'print("Hello")' in cleaned
assert "Line 2" in cleaned
2025-10-27 21:57:06 -05:00
def test_clean_whitespace_with_tabs(self):
"""Test cleaning whitespace with tabs."""
text = "Line 1\t\t\nLine 2\t\t\t\n\nLine 3\n"
2025-10-27 21:57:06 -05:00
cleaned = clean_whitespace(text)
lines = cleaned.split('\n')
assert lines[0] == "Line 1"
assert lines[1] == "Line 2"
@pytest.mark.unit
class TestCodeFenceNormalization:
"""Test code fence normalization functionality."""
def test_normalize_code_fences_tilde(self):
"""Test normalizing tilde code fences."""
text = """~~~
code block
~~~
More text.
"""
normalized = normalize_code_fences(text)
assert "```" in normalized
assert "~~~" not in normalized
assert "code block" in normalized
def test_normalize_code_fences_mixed(self):
"""Test normalizing mixed code fence styles."""
text = """~~~
code block 1
~~~
```python
code block 2
```
~~~
code block 3
~~~
"""
normalized = normalize_code_fences(text)
# All tildes should be converted to backticks
2025-10-27 21:57:06 -05:00
assert "~~~" not in normalized
assert "code block 1" in normalized
assert "code block 2" in normalized
assert "code block 3" in normalized
2025-10-27 21:57:06 -05:00
def test_normalize_code_fences_no_change(self):
"""Test normalizing when no tilde fences."""
text = """```python
code block
```
More text.
"""
normalized = normalize_code_fences(text)
# Should remain unchanged
assert normalized == text
def test_normalize_code_fences_inline(self):
"""Test normalizing inline code fences."""
text = """This is text with ~~~inline code~~~ and ```other inline code```."""
normalized = normalize_code_fences(text)
# Should only normalize block fences, not inline
assert normalized == text
@pytest.mark.unit
class TestSlugify:
"""Test slugify functionality."""
def test_slugify_basic(self):
"""Test basic slugification."""
test_cases = [
("Test Document", "test-document"),
("My Code Base!", "my-code-base"),
("Test_KB 123", "test-kb-123"),
("Hello World", "hello-world"),
("Multiple Spaces", "multiple-spaces"),
("Special@#$%Chars", "specialchars"),
("", ""),
("Test_KB", "test-kb"),
("Test--KB", "test-kb"),
("-Leading Dash", "leading-dash"),
("Trailing Dash-", "trailing-dash"),
("Multiple---Dashes", "multiple-dashes")
]
for input_text, expected_slug in test_cases:
result = slugify(input_text)
assert result == expected_slug, f"Failed for input: {input_text}"
def test_slugify_unicode(self):
"""Test slugifying Unicode characters."""
text = "Test Document"
2025-10-27 21:57:06 -05:00
result = slugify(text)
# Should convert to lowercase and handle spaces
assert result == "test-document"
2025-10-27 21:57:06 -05:00
def test_slugify_preserve_numbers(self):
"""Test that slugify preserves numbers."""
text = "test-with-numbers-123"
2025-10-27 21:57:06 -05:00
result = slugify(text)
assert result == "test-with-numbers-123"
2025-10-27 21:57:06 -05:00
def test_slugify_remove_multiple_dashes(self):
"""Test that slugify removes multiple consecutive dashes."""
text = "test---multiple---dashes"
result = slugify(text)
assert result == "test-multiple-dashes"
def test_slugify_trim_leading_trailing(self):
"""Test that slugify trims leading/trailing dashes."""
text = "---test-dashes---"
result = slugify(text)
assert result == "test-dashes"
@pytest.mark.integration
class TestMarkdownPreprocessor:
"""Test markdown preprocessor integration."""
def test_preprocess_markdown_with_frontmatter(self, tmp_path):
"""Test preprocessing markdown with frontmatter."""
text = """---
title: Test Document
author: John Doe
---
# Test Document
This is the main content of the document.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Check processed text
assert "# Test Document" in processed_text
assert "This is the main content" in processed_text
assert "---" not in processed_text
# Check metadata
assert metadata["title"] == "Test Document"
assert metadata["original_filename"] == "test.md"
assert metadata["source_path"] == file_path
assert metadata["preprocessed"] is True
assert "preprocessed_date" in metadata
# Note: fm_title is not added when title already exists in metadata
2025-10-27 21:57:06 -05:00
assert metadata["fm_author"] == "John Doe"
def test_preprocess_markdown_without_frontmatter(self, tmp_path):
"""Test preprocessing markdown without frontmatter."""
text = """# Test Document
This is the main content of the document.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Check processed text
assert "# Test Document" in processed_text
assert "This is the main content" in processed_text
# Check metadata
assert metadata["title"] == "Test Document" # Extracted from H1
assert metadata["original_filename"] == "test.md"
assert metadata["source_path"] == file_path
assert metadata["preprocessed"] is True
assert "preprocessed_date" in metadata
assert "fm_title" not in metadata # No frontmatter title
def test_preprocess_markdown_with_code_blocks(self, tmp_path):
"""Test preprocessing markdown with code blocks."""
text = """# Test Document
This is the main content.
```python
def test_function():
print("This is a code block")
return True
```
More content after code block.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Check that code blocks are preserved
assert "def test_function():" in processed_text
assert 'print("This is a code block")' in processed_text
assert "More content after code block" in processed_text
def test_preprocess_markdown_with_setext_headers(self, tmp_path):
"""Test preprocessing markdown with setext headers."""
text = """Title One
========
Subtitle One
--------
Content.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Check that headers are normalized
assert "# Title One" in processed_text
assert "## Subtitle One" in processed_text
assert "========" not in processed_text
assert "--------" not in processed_text
def test_preprocess_markdown_with_tilde_fences(self, tmp_path):
"""Test preprocessing markdown with tilde code fences."""
text = """# Test Document
~~~
code block with tilde
~~~
More content.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Check that code fences are normalized
assert "```" in processed_text
assert "~~~" not in processed_text
assert "code block with tilde" in processed_text
def test_preprocess_markdown_with_tags_string(self, tmp_path):
"""Test preprocessing markdown with tags as string."""
2025-10-27 21:57:06 -05:00
text = """---
title: Test Document
tags: test, example, demo
---
# Test Document
Content.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Check that tags are processed (simple parser returns string)
assert "tags" in metadata
assert "test" in metadata["tags"]
assert "example" in metadata["tags"]
assert "demo" in metadata["tags"]
2025-10-27 21:57:06 -05:00
def test_preprocess_markdown_with_author_and_date(self, tmp_path):
"""Test preprocessing markdown with author and date."""
text = """---
title: Test Document
author: John Doe
date: 2023-01-01
---
# Test Document
Content.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Check that author and date are preserved
assert metadata["fm_author"] == "John Doe"
assert metadata["fm_date"] == "2023-01-01"
assert metadata["author"] == "John Doe"
assert metadata["document_date"] == "2023-01-01"
def test_preprocess_markdown_with_custom_fields(self, tmp_path):
"""Test preprocessing markdown with custom frontmatter fields."""
text = """---
title: Test Document
custom_field: Custom Value
another_field: Another Value
---
# Test Document
Content.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Check that custom fields are preserved
assert metadata["fm_custom_field"] == "Custom Value"
assert metadata["fm_another_field"] == "Another Value"
def test_preprocess_markdown_filename_fallback(self, tmp_path):
"""Test preprocessing markdown with filename fallback."""
text = """Content without title.
"""
filename = "test-file.md"
file_path = str(tmp_path / filename)
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Should use filename as title fallback (titlecased)
assert "Test File" in metadata["title"]
2025-10-27 21:57:06 -05:00
def test_preprocess_markdown_error_handling(self, tmp_path):
"""Test preprocessing markdown with error handling."""
# Test with invalid frontmatter that might cause errors
text = """---
invalid: yaml: content:
- item1
- item2
- item3
---
# Test Document
Content.
"""
filename = "test.md"
file_path = str(tmp_path / filename)
# Should handle gracefully
processed_text, metadata = preprocess_markdown(text, filename, file_path)
# Should still process content
assert "# Test Document" in processed_text
assert "Content." in processed_text
assert metadata["preprocessed"] is True