gabriel / muse public
test_resolve_phase5.py python
241 lines 9.0 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Phase 5 of issue #8: muse conflicts and muse status display.
2
3 Coverage
4 --------
5 muse conflicts --json:
6 - resolved_count and resolved_conflicts present in JSON
7 - resolved_conflicts = original_conflict_paths minus conflict_paths
8 - conflict_count reflects only unresolved entries
9 - resolved_conflicts is empty when nothing resolved yet
10 - resolved_conflicts is full original list after all resolved
11 - no-merge-in-progress: resolved_count=0, resolved_conflicts=[]
12 - text output shows progress hint when some resolved
13
14 muse status --json:
15 - resolved_conflict_paths and resolved_conflict_count always present
16 - resolved_conflict_paths empty when no merge in progress
17 - resolved_conflict_paths populated after partial resolve
18 - resolved_conflict_count == len(resolved_conflict_paths)
19 - conflict_count only counts unresolved entries
20 """
21
22 from __future__ import annotations
23
24 import json
25 import os
26 import pathlib
27
28 import pytest
29
30 from muse.core.merge_engine import (
31 read_merge_state,
32 resolve_path,
33 resolve_symbol,
34 write_merge_state,
35 )
36 from muse.core.types import MUSE_DIR, fake_id
37 from tests.cli_test_helper import CliRunner
38
39 runner = CliRunner()
40
41 _BASE = fake_id("base")
42 _OURS = fake_id("ours")
43 _THEIRS = fake_id("theirs")
44
45
46 # ---------------------------------------------------------------------------
47 # Helpers
48 # ---------------------------------------------------------------------------
49
50 def _invoke(repo: pathlib.Path, args: list[str]) -> "InvokeResult": # type: ignore[name-defined]
51 saved = os.getcwd()
52 try:
53 os.chdir(repo)
54 return runner.invoke(None, args)
55 finally:
56 os.chdir(saved)
57
58
59 @pytest.fixture()
60 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
61 """Initialised code repo with one committed file."""
62 _invoke(tmp_path, ["init"])
63 (tmp_path / "hello.md").write_text("# Hello\n")
64 (tmp_path / "world.md").write_text("# World\n")
65 _invoke(tmp_path, ["code", "add", "."])
66 _invoke(tmp_path, ["commit", "-m", "initial"])
67 return tmp_path
68
69
70 def _set_merge_state(root: pathlib.Path, conflicts: list[str]) -> None:
71 write_merge_state(
72 root,
73 base_commit=_BASE,
74 ours_commit=_OURS,
75 theirs_commit=_THEIRS,
76 conflict_paths=conflicts,
77 other_branch="feat/x",
78 )
79
80
81 # ---------------------------------------------------------------------------
82 # muse conflicts --json
83 # ---------------------------------------------------------------------------
84
85 class TestConflictsJson:
86 def test_no_merge_has_zero_resolved(self, repo: pathlib.Path) -> None:
87 r = _invoke(repo, ["conflicts", "--json"])
88 data = json.loads(r.output)
89 assert data["resolved_count"] == 0
90 assert data["resolved_conflicts"] == []
91
92 def test_no_resolved_yet(self, repo: pathlib.Path) -> None:
93 _set_merge_state(repo, ["hello.md", "world.md"])
94 r = _invoke(repo, ["conflicts", "--json"])
95 data = json.loads(r.output)
96 assert data["resolved_count"] == 0
97 assert data["resolved_conflicts"] == []
98 assert data["conflict_count"] == 2
99
100 def test_partial_resolve_shows_resolved_conflicts(
101 self, repo: pathlib.Path
102 ) -> None:
103 _set_merge_state(repo, ["hello.md", "world.md"])
104 resolve_path(repo, "hello.md")
105 r = _invoke(repo, ["conflicts", "--json"])
106 data = json.loads(r.output)
107 assert data["resolved_count"] == 1
108 assert len(data["resolved_conflicts"]) == 1
109 assert data["resolved_conflicts"][0]["path"] == "hello.md"
110 assert data["conflict_count"] == 1
111 assert data["conflicts"][0]["path"] == "world.md"
112
113 def test_all_resolved_shows_all_in_resolved_conflicts(
114 self, repo: pathlib.Path
115 ) -> None:
116 _set_merge_state(repo, ["hello.md", "world.md"])
117 resolve_path(repo, "hello.md")
118 resolve_path(repo, "world.md")
119 r = _invoke(repo, ["conflicts", "--json"])
120 data = json.loads(r.output)
121 assert data["conflict_count"] == 0
122 assert data["conflicts"] == []
123 assert data["resolved_count"] == 2
124 assert {c["path"] for c in data["resolved_conflicts"]} == {"hello.md", "world.md"}
125
126 def test_symbol_level_resolved_conflicts(self, repo: pathlib.Path) -> None:
127 _set_merge_state(repo, ["hello.md::A", "hello.md::B"])
128 resolve_symbol(repo, "hello.md::A")
129 r = _invoke(repo, ["conflicts", "--json"])
130 data = json.loads(r.output)
131 assert data["resolved_count"] == 1
132 assert data["resolved_conflicts"][0]["path"] == "hello.md::A"
133 assert data["resolved_conflicts"][0]["symbol"] == "A"
134 assert data["resolved_conflicts"][0]["kind"] == "symbol"
135 assert data["conflict_count"] == 1
136
137 def test_resolved_conflicts_have_correct_kind_for_file(
138 self, repo: pathlib.Path
139 ) -> None:
140 _set_merge_state(repo, ["hello.md", "world.md"])
141 resolve_path(repo, "hello.md")
142 r = _invoke(repo, ["conflicts", "--json"])
143 data = json.loads(r.output)
144 resolved = data["resolved_conflicts"]
145 assert resolved[0]["kind"] == "file"
146 assert resolved[0]["symbol"] is None
147
148 def test_total_conflict_count_unchanged_by_resolve(
149 self, repo: pathlib.Path
150 ) -> None:
151 """total_conflict_count = len(conflict_paths before any resolve) = still unresolved."""
152 _set_merge_state(repo, ["hello.md", "world.md"])
153 resolve_path(repo, "hello.md")
154 r = _invoke(repo, ["conflicts", "--json"])
155 data = json.loads(r.output)
156 # total_conflict_count = len(current conflict_paths after resolve) = 1
157 assert data["total_conflict_count"] == 1
158
159 def test_json_schema_always_has_resolved_keys(
160 self, repo: pathlib.Path
161 ) -> None:
162 """resolved_count and resolved_conflicts are always present — never absent."""
163 r = _invoke(repo, ["conflicts", "--json"])
164 data = json.loads(r.output)
165 assert "resolved_count" in data
166 assert "resolved_conflicts" in data
167
168 def test_text_output_shows_progress_when_partially_resolved(
169 self, repo: pathlib.Path
170 ) -> None:
171 _set_merge_state(repo, ["hello.md", "world.md"])
172 resolve_path(repo, "hello.md")
173 r = _invoke(repo, ["conflicts"])
174 assert "resolved" in r.output.lower()
175
176
177 # ---------------------------------------------------------------------------
178 # muse status --json
179 # ---------------------------------------------------------------------------
180
181 class TestStatusJson:
182 def test_no_merge_has_empty_resolved_conflict_paths(
183 self, repo: pathlib.Path
184 ) -> None:
185 r = _invoke(repo, ["status", "--json"])
186 data = json.loads(r.output)
187 assert data["resolved_conflict_paths"] == []
188 assert data["resolved_conflict_count"] == 0
189
190 def test_no_resolved_yet(self, repo: pathlib.Path) -> None:
191 _set_merge_state(repo, ["hello.md", "world.md"])
192 r = _invoke(repo, ["status", "--json"])
193 data = json.loads(r.output)
194 assert data["resolved_conflict_paths"] == []
195 assert data["resolved_conflict_count"] == 0
196 assert data["conflict_count"] == 2
197
198 def test_partial_resolve_shown_in_status(self, repo: pathlib.Path) -> None:
199 _set_merge_state(repo, ["hello.md", "world.md"])
200 resolve_path(repo, "hello.md")
201 r = _invoke(repo, ["status", "--json"])
202 data = json.loads(r.output)
203 assert data["resolved_conflict_paths"] == ["hello.md"]
204 assert data["resolved_conflict_count"] == 1
205 assert data["conflict_count"] == 1
206 assert data["conflict_paths"] == ["world.md"]
207
208 def test_all_resolved_reflected_in_status(self, repo: pathlib.Path) -> None:
209 _set_merge_state(repo, ["hello.md", "world.md"])
210 resolve_path(repo, "hello.md")
211 resolve_path(repo, "world.md")
212 r = _invoke(repo, ["status", "--json"])
213 data = json.loads(r.output)
214 assert data["conflict_count"] == 0
215 assert data["conflict_paths"] == []
216 assert data["resolved_conflict_count"] == 2
217 assert set(data["resolved_conflict_paths"]) == {"hello.md", "world.md"}
218
219 def test_resolved_conflict_count_equals_len_of_list(
220 self, repo: pathlib.Path
221 ) -> None:
222 _set_merge_state(repo, ["hello.md::A", "hello.md::B", "world.md"])
223 resolve_symbol(repo, "hello.md::A")
224 r = _invoke(repo, ["status", "--json"])
225 data = json.loads(r.output)
226 assert data["resolved_conflict_count"] == len(data["resolved_conflict_paths"])
227
228 def test_schema_always_has_resolved_keys(self, repo: pathlib.Path) -> None:
229 """resolved_conflict_paths and resolved_conflict_count always present."""
230 r = _invoke(repo, ["status", "--json"])
231 data = json.loads(r.output)
232 assert "resolved_conflict_paths" in data
233 assert "resolved_conflict_count" in data
234
235 def test_text_status_shows_progress_when_partially_resolved(
236 self, repo: pathlib.Path
237 ) -> None:
238 _set_merge_state(repo, ["hello.md", "world.md"])
239 resolve_path(repo, "hello.md")
240 r = _invoke(repo, ["status"])
241 assert "resolved" in r.output.lower()
File History 3 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 23 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 29 days ago