"""Tests for the Markdown adapter."""
from datetime import datetime
from pathlib import Path
import pytest
from mcp_task_aggregator.adapters import MarkdownAdapter, MarkdownConfig
from mcp_task_aggregator.models import TodoSource, TodoStatus
class TestMarkdownAdapter:
"""Tests for MarkdownAdapter."""
@pytest.fixture
def temp_todo_file(self, tmp_path: Path) -> Path:
"""Create a temporary TODO.md file with sample tasks."""
todo_file = tmp_path / "TODO.md"
todo_file.write_text(
"""# Project Tasks
## High Priority
- [ ] Implement feature A
- [x] Complete documentation
- [-] Cancelled task
## Low Priority
- [ ] Refactor module B
- [~] Work in progress task
- [>] Blocked task
- [?] Task under review
"""
)
return todo_file
@pytest.fixture
def adapter(self, tmp_path: Path) -> MarkdownAdapter:
"""Create a MarkdownAdapter with test configuration."""
config = MarkdownConfig(search_paths=[tmp_path])
return MarkdownAdapter(config)
def test_fetch_tasks_finds_todo_file(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test that fetch_tasks discovers and parses TODO files."""
assert temp_todo_file.exists() # Ensure fixture file exists
tasks = adapter.fetch_tasks()
assert len(tasks) == 7
assert all("file_path" in task for task in tasks)
assert all("line_number" in task for task in tasks)
def test_fetch_tasks_parses_checkbox_states(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test that checkbox states are correctly parsed."""
assert temp_todo_file.exists() # Ensure fixture file exists
tasks = adapter.fetch_tasks()
checkbox_states = [task["checkbox_state"] for task in tasks]
assert "[ ]" in checkbox_states
assert "[x]" in checkbox_states
assert "[-]" in checkbox_states
assert "[~]" in checkbox_states
assert "[>]" in checkbox_states
assert "[?]" in checkbox_states
def test_fetch_tasks_captures_headings(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test that parent headings are captured."""
assert temp_todo_file.exists() # Ensure fixture file exists
tasks = adapter.fetch_tasks()
high_priority_tasks = [t for t in tasks if t["parent_heading"] == "High Priority"]
low_priority_tasks = [t for t in tasks if t["parent_heading"] == "Low Priority"]
assert len(high_priority_tasks) == 3
assert len(low_priority_tasks) == 4
def test_fetch_tasks_captures_indent_level(self, tmp_path: Path):
"""Test that indent levels are captured for nested tasks."""
todo_file = tmp_path / "TODO.md"
todo_file.write_text(
"""# Tasks
- [ ] Parent task
- [ ] Nested task level 1
- [ ] Nested task level 2
"""
)
config = MarkdownConfig(search_paths=[tmp_path])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
assert len(tasks) == 3
assert tasks[0]["indent_level"] == 0
assert tasks[1]["indent_level"] == 2
assert tasks[2]["indent_level"] == 4
def test_normalize_task_basic(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test basic task normalization."""
assert temp_todo_file.exists() # Ensure fixture file exists
tasks = adapter.fetch_tasks()
normalized = adapter.normalize_task(tasks[0])
assert normalized.content == "Implement feature A"
assert normalized.status == TodoStatus.TODO
assert normalized.source_system == TodoSource.MARKDOWN
assert normalized.source_id is not None
assert normalized.source_url is not None
def test_normalize_task_status_mapping(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test that checkbox states map to correct TodoStatus."""
assert temp_todo_file.exists() # Ensure fixture file exists
tasks = adapter.fetch_tasks()
normalized_tasks = [adapter.normalize_task(t) for t in tasks]
status_map = {t.content: t.status for t in normalized_tasks}
assert status_map["Implement feature A"] == TodoStatus.TODO
assert status_map["Complete documentation"] == TodoStatus.DONE
assert status_map["Cancelled task"] == TodoStatus.CANCELLED
assert status_map["Work in progress task"] == TodoStatus.IN_PROGRESS
assert status_map["Blocked task"] == TodoStatus.BLOCKED
assert status_map["Task under review"] == TodoStatus.IN_REVIEW
def test_normalize_task_extracts_priority(self, tmp_path: Path):
"""Test that priority markers are extracted."""
todo_file = tmp_path / "TODO.md"
todo_file.write_text(
"""# Tasks
- [ ] (P1) High priority task
- [ ] (P3) Medium priority task
- [ ] No priority task
"""
)
config = MarkdownConfig(search_paths=[tmp_path])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
normalized = [adapter.normalize_task(t) for t in tasks]
assert normalized[0].priority == 1
assert normalized[0].content == "High priority task"
assert normalized[1].priority == 3
assert normalized[1].content == "Medium priority task"
assert normalized[2].priority == 0
assert normalized[2].content == "No priority task"
def test_normalize_task_extracts_due_date(self, tmp_path: Path):
"""Test that due date markers are extracted."""
todo_file = tmp_path / "TODO.md"
todo_file.write_text(
"""# Tasks
- [ ] Task with due date (due:2024-12-25)
- [ ] Task without due date
"""
)
config = MarkdownConfig(search_paths=[tmp_path])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
normalized = [adapter.normalize_task(t) for t in tasks]
assert normalized[0].due_date == datetime(2024, 12, 25)
assert normalized[0].content == "Task with due date"
assert normalized[1].due_date is None
def test_normalize_task_builds_tags(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test that tags are built from task content."""
assert temp_todo_file.exists() # Ensure fixture file exists
tasks = adapter.fetch_tasks()
normalized = adapter.normalize_task(tasks[0])
tag_names = [t.name for t in normalized.tags]
assert "markdown" in tag_names
assert any("file:" in t for t in tag_names)
assert any("section:" in t for t in tag_names)
def test_normalize_task_extracts_hashtags(self, tmp_path: Path):
"""Test that hashtags are extracted as tags."""
todo_file = tmp_path / "TODO.md"
todo_file.write_text(
"""# Tasks
- [ ] Task with #feature and #urgent tags
"""
)
config = MarkdownConfig(search_paths=[tmp_path])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
normalized = adapter.normalize_task(tasks[0])
tag_names = [t.name for t in normalized.tags]
assert "feature" in tag_names
assert "urgent" in tag_names
def test_normalize_task_extracts_context_tags(self, tmp_path: Path):
"""Test that @context tags are extracted."""
todo_file = tmp_path / "TODO.md"
todo_file.write_text(
"""# Tasks
- [ ] Task with @home and @work contexts
"""
)
config = MarkdownConfig(search_paths=[tmp_path])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
normalized = adapter.normalize_task(tasks[0])
tag_names = [t.name for t in normalized.tags]
assert "context:home" in tag_names
assert "context:work" in tag_names
def test_normalize_task_generates_sync_hash(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test that sync hash is generated."""
assert temp_todo_file.exists() # Ensure fixture file exists
tasks = adapter.fetch_tasks()
normalized = adapter.normalize_task(tasks[0])
assert normalized.sync_hash is not None
assert len(normalized.sync_hash) == 16
def test_normalize_task_builds_external_metadata(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test that external metadata includes markdown-specific fields."""
tasks = adapter.fetch_tasks()
normalized = adapter.normalize_task(tasks[0])
assert normalized.external_metadata is not None
assert normalized.external_metadata.markdown is not None
assert normalized.external_metadata.markdown.file_path == str(temp_todo_file)
assert normalized.external_metadata.markdown.line_number > 0
assert normalized.external_metadata.markdown.checkbox_state == "[ ]"
def test_map_status_unknown_returns_todo(self, adapter: MarkdownAdapter):
"""Test that unknown checkbox states default to TODO."""
status = adapter.map_status("[unknown]")
assert status == TodoStatus.TODO
def test_fetch_tasks_multiple_files(self, tmp_path: Path):
"""Test that multiple TODO files are found and parsed."""
(tmp_path / "TODO.md").write_text("- [ ] Task from TODO.md\n")
(tmp_path / "TO-DO.md").write_text("- [ ] Task from TO-DO.md\n")
config = MarkdownConfig(search_paths=[tmp_path])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
assert len(tasks) == 2
def test_fetch_tasks_nonexistent_path(self, tmp_path: Path):
"""Test that nonexistent paths are handled gracefully."""
config = MarkdownConfig(search_paths=[tmp_path / "nonexistent"])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
assert tasks == []
def test_fetch_tasks_direct_file_path(self, tmp_path: Path):
"""Test that direct file paths work as search paths."""
todo_file = tmp_path / "custom-tasks.md"
todo_file.write_text("- [ ] Custom task\n")
config = MarkdownConfig(search_paths=[todo_file])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
assert len(tasks) == 1
assert tasks[0]["content"] == "Custom task"
def test_config_default_patterns(self):
"""Test that default file patterns are set."""
config = MarkdownConfig(search_paths=[Path(".")])
assert "TO-DO.md" in config.file_patterns
assert "TODO.md" in config.file_patterns
assert "todo.md" in config.file_patterns
assert "to-do.md" in config.file_patterns
def test_config_custom_patterns(self, tmp_path: Path):
"""Test that custom file patterns work."""
(tmp_path / "TASKS.md").write_text("- [ ] Custom pattern task\n")
config = MarkdownConfig(
search_paths=[tmp_path],
file_patterns=["TASKS.md"],
)
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
assert len(tasks) == 1
def test_list_marker_variations(self, tmp_path: Path):
"""Test that different list markers are supported."""
todo_file = tmp_path / "TODO.md"
todo_file.write_text(
"""# Tasks
- [ ] Task with dash
* [ ] Task with asterisk
+ [ ] Task with plus
"""
)
config = MarkdownConfig(search_paths=[tmp_path])
adapter = MarkdownAdapter(config)
tasks = adapter.fetch_tasks()
assert len(tasks) == 3
def test_source_url_format(self, adapter: MarkdownAdapter, temp_todo_file: Path):
"""Test that source URL is correctly formatted."""
assert temp_todo_file.exists() # Ensure fixture file exists
tasks = adapter.fetch_tasks()
normalized = adapter.normalize_task(tasks[0])
assert normalized.source_url.startswith("file://")
assert "#L" in normalized.source_url