"""Phase 5 of issue #8: muse conflicts and muse status display. Coverage -------- muse conflicts --json: - resolved_count and resolved_conflicts present in JSON - resolved_conflicts = original_conflict_paths minus conflict_paths - conflict_count reflects only unresolved entries - resolved_conflicts is empty when nothing resolved yet - resolved_conflicts is full original list after all resolved - no-merge-in-progress: resolved_count=0, resolved_conflicts=[] - text output shows progress hint when some resolved muse status --json: - resolved_conflict_paths and resolved_conflict_count always present - resolved_conflict_paths empty when no merge in progress - resolved_conflict_paths populated after partial resolve - resolved_conflict_count == len(resolved_conflict_paths) - conflict_count only counts unresolved entries """ from __future__ import annotations import json import os import pathlib import pytest from muse.core.merge_engine import ( read_merge_state, resolve_path, resolve_symbol, write_merge_state, ) from muse.core.types import MUSE_DIR, fake_id from tests.cli_test_helper import CliRunner runner = CliRunner() _BASE = fake_id("base") _OURS = fake_id("ours") _THEIRS = fake_id("theirs") # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _invoke(repo: pathlib.Path, args: list[str]) -> "InvokeResult": # type: ignore[name-defined] saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, args) finally: os.chdir(saved) @pytest.fixture() def repo(tmp_path: pathlib.Path) -> pathlib.Path: """Initialised code repo with one committed file.""" _invoke(tmp_path, ["init"]) (tmp_path / "hello.md").write_text("# Hello\n") (tmp_path / "world.md").write_text("# World\n") _invoke(tmp_path, ["code", "add", "."]) _invoke(tmp_path, ["commit", "-m", "initial"]) return tmp_path def _set_merge_state(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", ) # --------------------------------------------------------------------------- # muse conflicts --json # --------------------------------------------------------------------------- class TestConflictsJson: def test_no_merge_has_zero_resolved(self, repo: pathlib.Path) -> None: r = _invoke(repo, ["conflicts", "--json"]) data = json.loads(r.output) assert data["resolved_count"] == 0 assert data["resolved_conflicts"] == [] def test_no_resolved_yet(self, repo: pathlib.Path) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) r = _invoke(repo, ["conflicts", "--json"]) data = json.loads(r.output) assert data["resolved_count"] == 0 assert data["resolved_conflicts"] == [] assert data["conflict_count"] == 2 def test_partial_resolve_shows_resolved_conflicts( self, repo: pathlib.Path ) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) resolve_path(repo, "hello.md") r = _invoke(repo, ["conflicts", "--json"]) data = json.loads(r.output) assert data["resolved_count"] == 1 assert len(data["resolved_conflicts"]) == 1 assert data["resolved_conflicts"][0]["path"] == "hello.md" assert data["conflict_count"] == 1 assert data["conflicts"][0]["path"] == "world.md" def test_all_resolved_shows_all_in_resolved_conflicts( self, repo: pathlib.Path ) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) resolve_path(repo, "hello.md") resolve_path(repo, "world.md") r = _invoke(repo, ["conflicts", "--json"]) data = json.loads(r.output) assert data["conflict_count"] == 0 assert data["conflicts"] == [] assert data["resolved_count"] == 2 assert {c["path"] for c in data["resolved_conflicts"]} == {"hello.md", "world.md"} def test_symbol_level_resolved_conflicts(self, repo: pathlib.Path) -> None: _set_merge_state(repo, ["hello.md::A", "hello.md::B"]) resolve_symbol(repo, "hello.md::A") r = _invoke(repo, ["conflicts", "--json"]) data = json.loads(r.output) assert data["resolved_count"] == 1 assert data["resolved_conflicts"][0]["path"] == "hello.md::A" assert data["resolved_conflicts"][0]["symbol"] == "A" assert data["resolved_conflicts"][0]["kind"] == "symbol" assert data["conflict_count"] == 1 def test_resolved_conflicts_have_correct_kind_for_file( self, repo: pathlib.Path ) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) resolve_path(repo, "hello.md") r = _invoke(repo, ["conflicts", "--json"]) data = json.loads(r.output) resolved = data["resolved_conflicts"] assert resolved[0]["kind"] == "file" assert resolved[0]["symbol"] is None def test_total_conflict_count_unchanged_by_resolve( self, repo: pathlib.Path ) -> None: """total_conflict_count = len(conflict_paths before any resolve) = still unresolved.""" _set_merge_state(repo, ["hello.md", "world.md"]) resolve_path(repo, "hello.md") r = _invoke(repo, ["conflicts", "--json"]) data = json.loads(r.output) # total_conflict_count = len(current conflict_paths after resolve) = 1 assert data["total_conflict_count"] == 1 def test_json_schema_always_has_resolved_keys( self, repo: pathlib.Path ) -> None: """resolved_count and resolved_conflicts are always present — never absent.""" r = _invoke(repo, ["conflicts", "--json"]) data = json.loads(r.output) assert "resolved_count" in data assert "resolved_conflicts" in data def test_text_output_shows_progress_when_partially_resolved( self, repo: pathlib.Path ) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) resolve_path(repo, "hello.md") r = _invoke(repo, ["conflicts"]) assert "resolved" in r.output.lower() # --------------------------------------------------------------------------- # muse status --json # --------------------------------------------------------------------------- class TestStatusJson: def test_no_merge_has_empty_resolved_conflict_paths( self, repo: pathlib.Path ) -> None: r = _invoke(repo, ["status", "--json"]) data = json.loads(r.output) assert data["resolved_conflict_paths"] == [] assert data["resolved_conflict_count"] == 0 def test_no_resolved_yet(self, repo: pathlib.Path) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) r = _invoke(repo, ["status", "--json"]) data = json.loads(r.output) assert data["resolved_conflict_paths"] == [] assert data["resolved_conflict_count"] == 0 assert data["conflict_count"] == 2 def test_partial_resolve_shown_in_status(self, repo: pathlib.Path) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) resolve_path(repo, "hello.md") r = _invoke(repo, ["status", "--json"]) data = json.loads(r.output) assert data["resolved_conflict_paths"] == ["hello.md"] assert data["resolved_conflict_count"] == 1 assert data["conflict_count"] == 1 assert data["conflict_paths"] == ["world.md"] def test_all_resolved_reflected_in_status(self, repo: pathlib.Path) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) resolve_path(repo, "hello.md") resolve_path(repo, "world.md") r = _invoke(repo, ["status", "--json"]) data = json.loads(r.output) assert data["conflict_count"] == 0 assert data["conflict_paths"] == [] assert data["resolved_conflict_count"] == 2 assert set(data["resolved_conflict_paths"]) == {"hello.md", "world.md"} def test_resolved_conflict_count_equals_len_of_list( self, repo: pathlib.Path ) -> None: _set_merge_state(repo, ["hello.md::A", "hello.md::B", "world.md"]) resolve_symbol(repo, "hello.md::A") r = _invoke(repo, ["status", "--json"]) data = json.loads(r.output) assert data["resolved_conflict_count"] == len(data["resolved_conflict_paths"]) def test_schema_always_has_resolved_keys(self, repo: pathlib.Path) -> None: """resolved_conflict_paths and resolved_conflict_count always present.""" r = _invoke(repo, ["status", "--json"]) data = json.loads(r.output) assert "resolved_conflict_paths" in data assert "resolved_conflict_count" in data def test_text_status_shows_progress_when_partially_resolved( self, repo: pathlib.Path ) -> None: _set_merge_state(repo, ["hello.md", "world.md"]) resolve_path(repo, "hello.md") r = _invoke(repo, ["status"]) assert "resolved" in r.output.lower()