gabriel / muse public
test_merge_conflict_markers.py python
316 lines 13.0 KB
Raw
sha256:f6cd81bc71702f5c1c6890bd39aaba994fe58c75f019d7c03934724fa2739bb4 fix: carry dev changes harmony dropped in merge — detached … Sonnet 4.6 minor ⚠ breaking 16 days ago
1 """TDD tests for conflict marker writing during muse merge.
2
3 When a three-way merge produces a conflict, the conflicting file must contain
4 Cohen Transform markers (<<<<<<< / ||||||| / ======= / >>>>>>>) in the working
5 tree so the user (or agent) can inspect both versions and resolve manually.
6
7 Before the fix, merge.py left conflicting files at their ours content without
8 any markers, making it impossible to see what the conflict was.
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16 from collections.abc import Mapping
17
18 import pytest
19 from tests.cli_test_helper import CliRunner
20 from muse.core.types import blob_id
21 from muse.core.object_store import write_object
22 from muse.core.paths import heads_dir, muse_dir, ref_path
23
24 type Manifest = dict[str, str]
25
26 runner = CliRunner()
27 cli = None
28
29
30 # ---------------------------------------------------------------------------
31 # Shared helpers (same pattern as test_cmd_merge.py)
32 # ---------------------------------------------------------------------------
33
34
35 def _init_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, str]:
36 from muse.core.types import fake_id
37 dot_muse = muse_dir(tmp_path)
38 dot_muse.mkdir()
39 repo_id = fake_id("repo")
40 (dot_muse / "repo.json").write_text(json.dumps({
41 "repo_id": repo_id,
42 "domain": "code",
43 "default_branch": "main",
44 "created_at": "2025-01-01T00:00:00+00:00",
45 }), encoding="utf-8")
46 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
47 (dot_muse / "refs" / "heads").mkdir(parents=True)
48 (dot_muse / "snapshots").mkdir()
49 (dot_muse / "commits").mkdir()
50 (dot_muse / "objects").mkdir()
51 return tmp_path, repo_id
52
53
54 def _write_blob(root: pathlib.Path, content: bytes) -> str:
55 oid = blob_id(content)
56 write_object(root, oid, content)
57 return oid
58
59
60 def _make_commit(
61 root: pathlib.Path,
62 repo_id: str,
63 branch: str,
64 files: dict[str, bytes],
65 parent_id: str | None = None,
66 message: str = "commit",
67 ) -> str:
68 from muse.core.commits import CommitRecord, write_commit
69 from muse.core.snapshots import SnapshotRecord, write_snapshot
70 from muse.core.ids import hash_snapshot, hash_commit
71
72 manifest: Manifest = {}
73 for rel, content in files.items():
74 oid = _write_blob(root, content)
75 manifest[rel] = oid
76 dest = root / rel
77 dest.parent.mkdir(parents=True, exist_ok=True)
78 dest.write_bytes(content)
79
80 snap_id = hash_snapshot(manifest)
81 committed_at = datetime.datetime.now(datetime.timezone.utc)
82 commit_id = hash_commit(
83 parent_ids=[parent_id] if parent_id else [],
84 snapshot_id=snap_id,
85 message=message,
86 committed_at_iso=committed_at.isoformat(),
87 )
88 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
89 write_commit(root, CommitRecord(
90 commit_id=commit_id, branch=branch,
91 snapshot_id=snap_id, message=message, committed_at=committed_at,
92 parent_commit_id=parent_id,
93 ))
94 rf = ref_path(root, branch)
95 rf.parent.mkdir(parents=True, exist_ok=True)
96 rf.write_text(commit_id, encoding="utf-8")
97 return commit_id
98
99
100 def _env(root: pathlib.Path) -> Mapping[str, str]:
101 return {"MUSE_REPO_ROOT": str(root)}
102
103
104 # ---------------------------------------------------------------------------
105 # Tests
106 # ---------------------------------------------------------------------------
107
108
109 class TestConflictMarkersWrittenToWorkingTree:
110 """Conflict markers must appear in the working tree file after muse merge."""
111
112 def test_conflict_file_contains_ours_marker(self, tmp_path: pathlib.Path) -> None:
113 """<<<<<< ours marker must be present in the conflicting file."""
114 root, repo_id = _init_repo(tmp_path)
115
116 base_id = _make_commit(root, repo_id, "main",
117 {"hello.txt": b"Version Base\n"}, message="base")
118
119 (heads_dir(root) / "feat").write_text(base_id)
120 _make_commit(root, repo_id, "feat",
121 {"hello.txt": b"Version B\n"}, parent_id=base_id, message="feat")
122
123 _make_commit(root, repo_id, "main",
124 {"hello.txt": b"Version A\n"}, parent_id=base_id, message="main")
125
126 runner.invoke(cli, ["merge", "feat"], env=_env(root))
127
128 content = (root / "hello.txt").read_text(encoding="utf-8")
129 assert "<<<<<<<" in content, f"Expected conflict marker in file, got:\n{content}"
130
131 def test_conflict_file_contains_theirs_separator(self, tmp_path: pathlib.Path) -> None:
132 """======= separator must be present in the conflicting file."""
133 root, repo_id = _init_repo(tmp_path)
134
135 base_id = _make_commit(root, repo_id, "main",
136 {"hello.txt": b"Version Base\n"}, message="base")
137 (heads_dir(root) / "feat").write_text(base_id)
138 _make_commit(root, repo_id, "feat",
139 {"hello.txt": b"Version B\n"}, parent_id=base_id, message="feat")
140 _make_commit(root, repo_id, "main",
141 {"hello.txt": b"Version A\n"}, parent_id=base_id, message="main")
142
143 runner.invoke(cli, ["merge", "feat"], env=_env(root))
144
145 content = (root / "hello.txt").read_text(encoding="utf-8")
146 assert "=======" in content, f"Expected ======= separator, got:\n{content}"
147
148 def test_conflict_file_contains_end_marker(self, tmp_path: pathlib.Path) -> None:
149 """>>>>>>> end marker must be present in the conflicting file."""
150 root, repo_id = _init_repo(tmp_path)
151
152 base_id = _make_commit(root, repo_id, "main",
153 {"hello.txt": b"Version Base\n"}, message="base")
154 (heads_dir(root) / "feat").write_text(base_id)
155 _make_commit(root, repo_id, "feat",
156 {"hello.txt": b"Version B\n"}, parent_id=base_id, message="feat")
157 _make_commit(root, repo_id, "main",
158 {"hello.txt": b"Version A\n"}, parent_id=base_id, message="main")
159
160 runner.invoke(cli, ["merge", "feat"], env=_env(root))
161
162 content = (root / "hello.txt").read_text(encoding="utf-8")
163 assert ">>>>>>>" in content, f"Expected >>>>>>> end marker, got:\n{content}"
164
165 def test_conflict_file_contains_ours_content(self, tmp_path: pathlib.Path) -> None:
166 """The ours side content must appear in the conflict block."""
167 root, repo_id = _init_repo(tmp_path)
168
169 base_id = _make_commit(root, repo_id, "main",
170 {"hello.txt": b"Version Base\n"}, message="base")
171 (heads_dir(root) / "feat").write_text(base_id)
172 _make_commit(root, repo_id, "feat",
173 {"hello.txt": b"Version B\n"}, parent_id=base_id, message="feat")
174 _make_commit(root, repo_id, "main",
175 {"hello.txt": b"Version A\n"}, parent_id=base_id, message="main")
176
177 runner.invoke(cli, ["merge", "feat"], env=_env(root))
178
179 content = (root / "hello.txt").read_text(encoding="utf-8")
180 assert "Version A" in content, f"Expected ours content in markers, got:\n{content}"
181
182 def test_conflict_file_contains_theirs_content(self, tmp_path: pathlib.Path) -> None:
183 """The theirs side content must appear in the conflict block."""
184 root, repo_id = _init_repo(tmp_path)
185
186 base_id = _make_commit(root, repo_id, "main",
187 {"hello.txt": b"Version Base\n"}, message="base")
188 (heads_dir(root) / "feat").write_text(base_id)
189 _make_commit(root, repo_id, "feat",
190 {"hello.txt": b"Version B\n"}, parent_id=base_id, message="feat")
191 _make_commit(root, repo_id, "main",
192 {"hello.txt": b"Version A\n"}, parent_id=base_id, message="main")
193
194 runner.invoke(cli, ["merge", "feat"], env=_env(root))
195
196 content = (root / "hello.txt").read_text(encoding="utf-8")
197 assert "Version B" in content, f"Expected theirs content in markers, got:\n{content}"
198
199 def test_conflict_file_contains_base_content(self, tmp_path: pathlib.Path) -> None:
200 """The base content must appear in the ||||||| block (diff3 style)."""
201 root, repo_id = _init_repo(tmp_path)
202
203 base_id = _make_commit(root, repo_id, "main",
204 {"hello.txt": b"Version Base\n"}, message="base")
205 (heads_dir(root) / "feat").write_text(base_id)
206 _make_commit(root, repo_id, "feat",
207 {"hello.txt": b"Version B\n"}, parent_id=base_id, message="feat")
208 _make_commit(root, repo_id, "main",
209 {"hello.txt": b"Version A\n"}, parent_id=base_id, message="main")
210
211 runner.invoke(cli, ["merge", "feat"], env=_env(root))
212
213 content = (root / "hello.txt").read_text(encoding="utf-8")
214 assert "Version Base" in content, f"Expected base content in ||||||| block, got:\n{content}"
215
216 def test_non_conflicting_file_has_no_markers(self, tmp_path: pathlib.Path) -> None:
217 """A file only changed on one side must not receive conflict markers."""
218 root, repo_id = _init_repo(tmp_path)
219
220 base_id = _make_commit(root, repo_id, "main", {
221 "shared.txt": b"conflict line\n",
222 "theirs_only.txt": b"stable\n",
223 }, message="base")
224
225 (heads_dir(root) / "feat").write_text(base_id)
226 _make_commit(root, repo_id, "feat", {
227 "shared.txt": b"Version B\n",
228 "theirs_only.txt": b"theirs change\n",
229 }, parent_id=base_id, message="feat")
230
231 _make_commit(root, repo_id, "main", {
232 "shared.txt": b"Version A\n",
233 "theirs_only.txt": b"stable\n",
234 }, parent_id=base_id, message="main")
235
236 runner.invoke(cli, ["merge", "feat"], env=_env(root))
237
238 content = (root / "theirs_only.txt").read_text(encoding="utf-8")
239 assert "<<<<<<<" not in content, (
240 f"No markers expected in theirs-only file, got:\n{content}"
241 )
242
243 def test_binary_conflict_file_not_garbled(self, tmp_path: pathlib.Path) -> None:
244 """Binary files in conflict must not have text markers written into them."""
245 root, repo_id = _init_repo(tmp_path)
246
247 # Create a fake binary blob (null bytes make it binary)
248 base_bytes = b"\x00\x01\x02\x03 base binary"
249 ours_bytes = b"\x00\x01\x02\x03 ours binary"
250 theirs_bytes = b"\x00\x01\x02\x03 theirs binary"
251
252 base_id = _make_commit(root, repo_id, "main",
253 {"data.bin": base_bytes}, message="base")
254 (heads_dir(root) / "feat").write_text(base_id)
255 _make_commit(root, repo_id, "feat",
256 {"data.bin": theirs_bytes}, parent_id=base_id, message="feat")
257 _make_commit(root, repo_id, "main",
258 {"data.bin": ours_bytes}, parent_id=base_id, message="main")
259
260 runner.invoke(cli, ["merge", "feat"], env=_env(root))
261
262 # File should exist and not contain the text marker sequence
263 if (root / "data.bin").exists():
264 content = (root / "data.bin").read_bytes()
265 assert b"<<<<<<<"[:3] not in content or b"<<<<<<< " not in content, (
266 "Binary conflict file must not have text markers injected"
267 )
268
269 def test_merge_state_still_records_conflict_path(self, tmp_path: pathlib.Path) -> None:
270 """MERGE_STATE.json must still record hello.txt as conflicted even after markers are written."""
271 root, repo_id = _init_repo(tmp_path)
272
273 base_id = _make_commit(root, repo_id, "main",
274 {"hello.txt": b"base\n"}, message="base")
275 (heads_dir(root) / "feat").write_text(base_id)
276 _make_commit(root, repo_id, "feat",
277 {"hello.txt": b"Version B\n"}, parent_id=base_id, message="feat")
278 _make_commit(root, repo_id, "main",
279 {"hello.txt": b"Version A\n"}, parent_id=base_id, message="main")
280
281 runner.invoke(cli, ["merge", "feat"], env=_env(root))
282
283 merge_state = json.loads(
284 (muse_dir(root) / "MERGE_STATE.json").read_text()
285 )
286 assert "hello.txt" in merge_state["conflict_paths"], (
287 "MERGE_STATE.json must still list hello.txt as a conflict path"
288 )
289
290 def test_multi_file_conflict_all_files_get_markers(self, tmp_path: pathlib.Path) -> None:
291 """Every conflicting file must receive markers, not just the first one."""
292 root, repo_id = _init_repo(tmp_path)
293
294 base_id = _make_commit(root, repo_id, "main", {
295 "alpha.txt": b"base alpha\n",
296 "beta.txt": b"base beta\n",
297 }, message="base")
298
299 (heads_dir(root) / "feat").write_text(base_id)
300 _make_commit(root, repo_id, "feat", {
301 "alpha.txt": b"theirs alpha\n",
302 "beta.txt": b"theirs beta\n",
303 }, parent_id=base_id, message="feat")
304
305 _make_commit(root, repo_id, "main", {
306 "alpha.txt": b"ours alpha\n",
307 "beta.txt": b"ours beta\n",
308 }, parent_id=base_id, message="main")
309
310 runner.invoke(cli, ["merge", "feat"], env=_env(root))
311
312 for fname in ("alpha.txt", "beta.txt"):
313 content = (root / fname).read_text(encoding="utf-8")
314 assert "<<<<<<<" in content, (
315 f"Expected conflict markers in {fname}, got:\n{content}"
316 )
File History 2 commits
sha256:43c82f6d4fa2e85dd9ed9dd1a31199ec6b481191517aba66dfa9da275dbfa1af Merge branch 'dev' into main Human 2 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 21 days ago