"""Tests for Phase 1 of issue #8: resolve_path and resolve_symbol primitives. Coverage -------- - resolve_path: removes all conflict entries whose file portion matches path - resolve_path: leaves original_conflict_paths untouched - resolve_path: idempotent when path already resolved - resolve_path: returns empty list when path not in conflict_paths - resolve_path: raises ValueError when no merge in progress - resolve_symbol: removes exactly one matching entry - resolve_symbol: returns False when symbol not present (idempotent) - resolve_symbol: leaves original_conflict_paths untouched - resolve_symbol: raises ValueError when no merge in progress - Mixed symbol/file entries: resolve_path clears only matching file portion - After resolve_path clears all conflicts, conflict_paths is empty - write_merge_state / read_merge_state round-trip preserves all fields """ from __future__ import annotations import json import pathlib import pytest from muse.core.merge_engine import ( MergeState, clear_merge_state, read_merge_state, resolve_path, resolve_symbol, write_merge_state, ) from muse.core.types import MUSE_DIR, fake_id # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _BASE = fake_id("base") _OURS = fake_id("ours") _THEIRS = fake_id("theirs") def _repo(tmp_path: pathlib.Path) -> pathlib.Path: (tmp_path / MUSE_DIR).mkdir() return tmp_path def _write(root: pathlib.Path, conflicts: list[str]) -> None: write_merge_state( root, base_commit=_BASE, ours_commit=_OURS, theirs_commit=_THEIRS, conflict_paths=conflicts, other_branch="feat/x", ) def _state(root: pathlib.Path) -> MergeState: s = read_merge_state(root) assert s is not None return s # --------------------------------------------------------------------------- # resolve_path # --------------------------------------------------------------------------- class TestResolvePath: def test_clears_plain_file_entry(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md", "world.md"]) cleared = resolve_path(repo, "hello.md") assert cleared == ["hello.md"] assert _state(repo).conflict_paths == ["world.md"] def test_clears_symbol_level_entries_for_file(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md::Hello World", "hello.md::Subtitle", "other.py"]) cleared = resolve_path(repo, "hello.md") assert sorted(cleared) == ["hello.md::Hello World", "hello.md::Subtitle"] assert _state(repo).conflict_paths == ["other.py"] def test_clears_mixed_plain_and_symbol_entries(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md", "hello.md::Section", "readme.txt"]) cleared = resolve_path(repo, "hello.md") assert sorted(cleared) == ["hello.md", "hello.md::Section"] assert _state(repo).conflict_paths == ["readme.txt"] def test_returns_empty_when_path_not_conflicted(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["other.py"]) cleared = resolve_path(repo, "hello.md") assert cleared == [] assert _state(repo).conflict_paths == ["other.py"] def test_idempotent_second_call_returns_empty(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md"]) resolve_path(repo, "hello.md") cleared2 = resolve_path(repo, "hello.md") assert cleared2 == [] def test_does_not_mutate_original_conflict_paths(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md", "world.md"]) original_before = _state(repo).original_conflict_paths[:] resolve_path(repo, "hello.md") assert _state(repo).original_conflict_paths == original_before def test_empties_conflict_paths_when_all_resolved(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md::A", "hello.md::B"]) resolve_path(repo, "hello.md") assert _state(repo).conflict_paths == [] def test_raises_when_no_merge_in_progress(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) with pytest.raises(ValueError, match="No merge in progress"): resolve_path(repo, "hello.md") def test_preserves_other_merge_state_fields(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md", "other.py"]) resolve_path(repo, "hello.md") s = _state(repo) assert s.base_commit == _BASE assert s.ours_commit == _OURS assert s.theirs_commit == _THEIRS assert s.other_branch == "feat/x" def test_does_not_clear_entries_with_similar_prefix(self, tmp_path: pathlib.Path) -> None: """resolve_path("hello.md") must not clear "hello.md.bak" or "hello.md2".""" repo = _repo(tmp_path) _write(repo, ["hello.md", "hello.md.bak", "hello.md2"]) cleared = resolve_path(repo, "hello.md") assert cleared == ["hello.md"] remaining = _state(repo).conflict_paths assert "hello.md.bak" in remaining assert "hello.md2" in remaining # --------------------------------------------------------------------------- # resolve_symbol # --------------------------------------------------------------------------- class TestResolveSymbol: def test_removes_plain_path_entry(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md", "world.md"]) found = resolve_symbol(repo, "hello.md") assert found is True assert _state(repo).conflict_paths == ["world.md"] def test_removes_symbol_address_entry(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md::Hello World", "hello.md::Subtitle"]) found = resolve_symbol(repo, "hello.md::Hello World") assert found is True assert _state(repo).conflict_paths == ["hello.md::Subtitle"] def test_returns_false_when_not_present(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["other.py"]) found = resolve_symbol(repo, "hello.md::Missing") assert found is False assert _state(repo).conflict_paths == ["other.py"] def test_idempotent_second_call_returns_false(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md"]) resolve_symbol(repo, "hello.md") found2 = resolve_symbol(repo, "hello.md") assert found2 is False def test_does_not_mutate_original_conflict_paths(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md", "world.md"]) original_before = _state(repo).original_conflict_paths[:] resolve_symbol(repo, "hello.md") assert _state(repo).original_conflict_paths == original_before def test_raises_when_no_merge_in_progress(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) with pytest.raises(ValueError, match="No merge in progress"): resolve_symbol(repo, "hello.md") def test_removes_only_exact_match(self, tmp_path: pathlib.Path) -> None: """resolve_symbol must not remove "hello.md::A" when asked for "hello.md".""" repo = _repo(tmp_path) _write(repo, ["hello.md", "hello.md::A"]) resolve_symbol(repo, "hello.md") assert _state(repo).conflict_paths == ["hello.md::A"] def test_preserves_other_merge_state_fields(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["hello.md"]) resolve_symbol(repo, "hello.md") s = _state(repo) assert s.base_commit == _BASE assert s.ours_commit == _OURS assert s.theirs_commit == _THEIRS assert s.other_branch == "feat/x" def test_empties_conflict_paths_when_last_entry_resolved(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["only.py"]) resolve_symbol(repo, "only.py") assert _state(repo).conflict_paths == [] def test_original_paths_persisted_across_multiple_resolves(self, tmp_path: pathlib.Path) -> None: repo = _repo(tmp_path) _write(repo, ["a.py", "b.py", "c.py"]) original = _state(repo).original_conflict_paths[:] resolve_symbol(repo, "a.py") resolve_symbol(repo, "b.py") resolve_symbol(repo, "c.py") assert _state(repo).original_conflict_paths == original assert _state(repo).conflict_paths == []