"""Round-trip tests for parsers. These tests verify that parse -> save -> parse produces identical data. This ensures no data loss during save operations. """ import tempfile from pathlib import Path import pytest from development_hub.parsers.todos_parser import TodosParser from development_hub.parsers.goals_parser import GoalsParser, MilestonesParser, GoalsSaver from development_hub.models.todo import Todo, TodoList from development_hub.models.goal import Goal, GoalList, Milestone, Deliverable, MilestoneStatus, DeliverableStatus class TestTodosRoundtrip: """Test TodosParser round-trip preservation.""" def test_simple_todos_roundtrip(self, tmp_path): """Parse -> save -> parse should produce identical data.""" todos_file = tmp_path / "todos.md" original_content = """--- type: todos project: test-project updated: 2026-01-08 --- # TODOs ## Active Tasks ### High Priority - [ ] Important task #high - [ ] Another important task #high @M1 ### Medium Priority - [ ] Medium task #medium - [x] Completed medium task #medium (2026-01-07) ### Low Priority - [ ] Low priority task #low ## Completed - [x] Done task #high (2026-01-06) - [x] Another done #medium (2026-01-05) """ todos_file.write_text(original_content) # First parse parser1 = TodosParser(todos_file) todo_list1 = parser1.parse() # Save parser1.save(todo_list1) # Second parse parser2 = TodosParser(todos_file) todo_list2 = parser2.parse() # Compare assert len(todo_list1.all_todos) == len(todo_list2.all_todos) assert len(todo_list1.completed) == len(todo_list2.completed) # Compare individual todos for t1, t2 in zip(todo_list1.all_todos, todo_list2.all_todos): assert t1.text == t2.text assert t1.priority == t2.priority assert t1.completed == t2.completed assert t1.milestone == t2.milestone def test_todos_with_milestones_roundtrip(self, tmp_path): """Milestone tags should be preserved.""" todos_file = tmp_path / "todos.md" original_content = """--- type: todos project: test --- # TODOs ## Active Tasks ### High Priority - [ ] Task with milestone @M1 #high - [ ] Task with M2 @M2 #high ### Medium Priority ### Low Priority ## Completed """ todos_file.write_text(original_content) parser1 = TodosParser(todos_file) todo_list1 = parser1.parse() # Verify milestones parsed high_todos = [t for t in todo_list1.all_todos if t.priority == "high"] assert any(t.milestone == "M1" for t in high_todos) assert any(t.milestone == "M2" for t in high_todos) # Save and reparse parser1.save(todo_list1) parser2 = TodosParser(todos_file) todo_list2 = parser2.parse() # Verify milestones preserved high_todos2 = [t for t in todo_list2.all_todos if t.priority == "high"] assert any(t.milestone == "M1" for t in high_todos2) assert any(t.milestone == "M2" for t in high_todos2) class TestGoalsRoundtrip: """Test GoalsParser round-trip preservation.""" def test_goals_roundtrip(self, tmp_path): """Goals should be preserved across parse -> save -> parse.""" goals_file = tmp_path / "goals.md" original_content = """--- type: goals project: test-project updated: 2026-01-08 --- # Goals ## Active - [ ] Incomplete goal #high - [~] Partial goal #medium - [x] Completed goal #low ## Future - [ ] Future goal 1 #medium - [ ] Future goal 2 #low ## Non-Goals - [ ] Not doing this #medium - [x] Confirmed non-goal #high """ goals_file.write_text(original_content) # First parse parser1 = GoalsParser(goals_file) goal_list1 = parser1.parse() # Save using GoalsSaver saver = GoalsSaver(goals_file, parser1.frontmatter) saver.save(goal_list1) # Second parse parser2 = GoalsParser(goals_file) goal_list2 = parser2.parse() # Compare counts assert len(goal_list1.active) == len(goal_list2.active) assert len(goal_list1.future) == len(goal_list2.future) assert len(goal_list1.non_goals) == len(goal_list2.non_goals) # Compare active goals for g1, g2 in zip(goal_list1.active, goal_list2.active): assert g1.text == g2.text assert g1.completed == g2.completed assert g1.partial == g2.partial assert g1.priority == g2.priority def test_three_state_goals_roundtrip(self, tmp_path): """Three-state goals ([ ], [~], [x]) should be preserved.""" goals_file = tmp_path / "goals.md" original_content = """--- type: goals project: test --- # Goals ## Active - [ ] Not started #medium - [~] In progress #high - [x] Done #low """ goals_file.write_text(original_content) parser1 = GoalsParser(goals_file) goal_list1 = parser1.parse() # Verify states parsed correctly assert goal_list1.active[0].completed == False assert goal_list1.active[0].partial == False assert goal_list1.active[1].completed == False assert goal_list1.active[1].partial == True assert goal_list1.active[2].completed == True assert goal_list1.active[2].partial == False # Save and reparse saver = GoalsSaver(goals_file, parser1.frontmatter) saver.save(goal_list1) parser2 = GoalsParser(goals_file) goal_list2 = parser2.parse() # Verify states preserved assert goal_list2.active[0].completed == False assert goal_list2.active[0].partial == False assert goal_list2.active[1].completed == False assert goal_list2.active[1].partial == True assert goal_list2.active[2].completed == True assert goal_list2.active[2].partial == False class TestMilestonesRoundtrip: """Test MilestonesParser round-trip preservation.""" def test_milestones_roundtrip(self, tmp_path): """Milestones should be preserved across parse -> save -> parse.""" milestones_file = tmp_path / "milestones.md" original_content = """--- type: milestones project: test-project updated: 2026-01-08 --- # Milestones #### M1: First Milestone **Target**: January 2026 **Status**: In Progress (50%) First milestone description. | Deliverable | Status | |-------------|--------| | Task A | Done | | Task B | In Progress | | Task C | Not Started | --- #### M2: Second Milestone **Target**: February 2026 **Status**: Not Started Second milestone description. | Deliverable | Status | |-------------|--------| | Feature X | Not Started | | Feature Y | Not Started | --- """ milestones_file.write_text(original_content) # First parse parser1 = MilestonesParser(milestones_file) milestones1 = parser1.parse() # Save parser1.save(milestones1) # Second parse parser2 = MilestonesParser(milestones_file) milestones2 = parser2.parse() # Compare counts assert len(milestones1) == len(milestones2) # Compare individual milestones for m1, m2 in zip(milestones1, milestones2): assert m1.id == m2.id assert m1.name == m2.name assert m1.target == m2.target assert m1.status == m2.status assert len(m1.deliverables) == len(m2.deliverables) # Compare deliverables for d1, d2 in zip(m1.deliverables, m2.deliverables): assert d1.name == d2.name assert d1.status == d2.status def test_milestone_status_roundtrip(self, tmp_path): """Milestone statuses should be preserved.""" milestones_file = tmp_path / "milestones.md" original_content = """--- type: milestones project: test --- # Milestones #### M1: Complete **Target**: Dec 2025 **Status**: Completed (100%) --- #### M2: In Progress **Target**: Jan 2026 **Status**: In Progress (75%) --- #### M3: Planning **Target**: Feb 2026 **Status**: Planning (10%) --- #### M4: Not Started **Target**: Q2 2026 **Status**: Not Started --- """ milestones_file.write_text(original_content) parser1 = MilestonesParser(milestones_file) milestones1 = parser1.parse() # Create lookup by ID by_id1 = {m.id: m for m in milestones1} # Verify statuses parsed assert by_id1["M1"].status == MilestoneStatus.COMPLETE assert by_id1["M2"].status == MilestoneStatus.IN_PROGRESS assert by_id1["M3"].status == MilestoneStatus.PLANNING assert by_id1["M4"].status == MilestoneStatus.NOT_STARTED # Save and reparse parser1.save(milestones1) parser2 = MilestonesParser(milestones_file) milestones2 = parser2.parse() # Create lookup by ID by_id2 = {m.id: m for m in milestones2} # Verify statuses preserved (order may change due to Active/Completed sections) assert by_id2["M1"].status == MilestoneStatus.COMPLETE assert by_id2["M2"].status == MilestoneStatus.IN_PROGRESS assert by_id2["M3"].status == MilestoneStatus.PLANNING assert by_id2["M4"].status == MilestoneStatus.NOT_STARTED class TestAtomicWrite: """Test atomic write functionality.""" def test_atomic_write_creates_file(self, tmp_path): """atomic_write should create file if it doesn't exist.""" from development_hub.parsers.base import atomic_write test_file = tmp_path / "new_file.md" content = "# Test Content\n\nSome text here." atomic_write(test_file, content) assert test_file.exists() assert test_file.read_text() == content def test_atomic_write_overwrites_file(self, tmp_path): """atomic_write should safely overwrite existing file.""" from development_hub.parsers.base import atomic_write test_file = tmp_path / "existing.md" test_file.write_text("Original content") new_content = "New content here" atomic_write(test_file, new_content) assert test_file.read_text() == new_content def test_atomic_write_no_temp_files_left(self, tmp_path): """atomic_write should not leave temp files after success.""" from development_hub.parsers.base import atomic_write test_file = tmp_path / "test.md" atomic_write(test_file, "content") # Check no temp files remain temp_files = list(tmp_path.glob(".test.md.*")) assert len(temp_files) == 0 if __name__ == "__main__": pytest.main([__file__, "-v"])