gabriel / muse public
test_comment_only_independence_merge.py python
127 lines 4.5 KB
Raw
sha256:2084d8cc66f5923f35d286b4c8bc7217c01011a4d2690240b554ef18e99c9ab5 fix: comment-only edits to different functions in same file… Sonnet 5 minor ⚠ breaking 1 day ago
1 """muse#92 — code domain's address-keyed merge falsely conflicts when both
2 sides add a trailing comment to different functions in the same file.
3
4 Root cause: a comment-only edit to a function is AST-identical to the base
5 (content_id is computed via `ast.unparse()`, which strips comments by
6 design — see ast_parser.py's PythonAdapter docstring). `build_diff_ops`
7 sees no symbol-level `child_ops` for such a file and falls back to a
8 whole-file `ReplaceOp` labeled "reformatted — no semantic change" instead
9 of a symbol-addressed `PatchOp`. When BOTH branches independently add a
10 comment to a DIFFERENT function in the same file, `merge_ops()`'s Step 1.5
11 (independence-aware blob reconstruction) can't find `PatchOp`s for either
12 side and used to just `continue`, skipping reconstruction entirely. Step 2
13 (coarse OT over the two competing file-level `ReplaceOp`s) then flagged the
14 file as a hard conflict, even though the two edits don't overlap at all.
15
16 Fix: when symbol-level patch info isn't available for a candidate path,
17 attempt a direct three-way LINE merge on the raw blobs (`three_way_merge_lines`
18 from `muse.core.cohen_transform`, the same primitive `_independence_merge_blob`
19 already uses for genuine independence merges). This is safe regardless of
20 symbol knowledge: if the base/ours/theirs line diffs don't overlap, combining
21 them is correct no matter what the file's AST looks like. A second bug was
22 found while verifying this fix live: Step 2's OT check re-flags the file as
23 conflicting independently of Step 1.5's resolution, so `conflict_addresses`
24 must also be filtered against `independence_resolved` before Step 3.
25
26 Reproduces the exact scenario via the real CLI (not synthetic objects):
27 two branches, each adding a trailing comment to a different function in the
28 same file, must merge with zero conflicts and both comments present in the
29 committed result.
30 """
31 from __future__ import annotations
32
33 import json
34 import os
35 import pathlib
36
37 from tests.cli_test_helper import CliRunner, InvokeResult
38
39 runner = CliRunner()
40
41
42 def _run(repo: pathlib.Path, *args: str) -> InvokeResult:
43 saved = os.getcwd()
44 try:
45 os.chdir(repo)
46 return runner.invoke(None, list(args))
47 finally:
48 os.chdir(saved)
49
50
51 _BASE_CALC = (
52 "def add(a, b):\n"
53 " return a + b\n"
54 "\n"
55 "def subtract(a, b):\n"
56 " return a - b\n"
57 )
58
59 _ALICE_CALC = (
60 "def add(a, b):\n"
61 " return a + b # comment\n"
62 "\n"
63 "def subtract(a, b):\n"
64 " return a - b\n"
65 )
66
67 _BOB_CALC = (
68 "def add(a, b):\n"
69 " return a + b\n"
70 "\n"
71 "def subtract(a, b):\n"
72 " return a - b # comment\n"
73 )
74
75
76 def test_trailing_comments_on_different_functions_merge_cleanly(tmp_path: pathlib.Path) -> None:
77 repo = tmp_path
78 r = _run(repo, "init")
79 assert r.exit_code == 0, r.stderr
80
81 (repo / "calc.py").write_text(_BASE_CALC)
82 r = _run(repo, "code", "add", "calc.py")
83 assert r.exit_code == 0, r.stderr
84 r = _run(repo, "commit", "-m", "base")
85 assert r.exit_code == 0, r.stderr
86
87 r = _run(repo, "branch", "feature/alice")
88 assert r.exit_code == 0, r.stderr
89 r = _run(repo, "checkout", "feature/alice")
90 assert r.exit_code == 0, r.stderr
91 (repo / "calc.py").write_text(_ALICE_CALC)
92 r = _run(repo, "code", "add", "calc.py")
93 assert r.exit_code == 0, r.stderr
94 r = _run(repo, "commit", "-m", "alice comments add")
95 assert r.exit_code == 0, r.stderr
96
97 r = _run(repo, "checkout", "main")
98 assert r.exit_code == 0, r.stderr
99 r = _run(repo, "branch", "feature/bob")
100 assert r.exit_code == 0, r.stderr
101 r = _run(repo, "checkout", "feature/bob")
102 assert r.exit_code == 0, r.stderr
103 (repo / "calc.py").write_text(_BOB_CALC)
104 r = _run(repo, "code", "add", "calc.py")
105 assert r.exit_code == 0, r.stderr
106 r = _run(repo, "commit", "-m", "bob comments subtract")
107 assert r.exit_code == 0, r.stderr
108
109 r = _run(repo, "checkout", "main")
110 assert r.exit_code == 0, r.stderr
111 r = _run(repo, "merge", "feature/alice")
112 assert r.exit_code == 0, r.stderr
113
114 r = _run(repo, "merge", "feature/bob", "--json")
115 assert r.exit_code == 0, r.stderr
116 payload = json.loads(r.output)
117 assert payload["status"] == "merged", (
118 f"independent comment-only edits to different functions must merge "
119 f"cleanly with zero conflicts, got: {payload}"
120 )
121 assert payload["conflicts"] == []
122
123 r = _run(repo, "cat", "calc.py")
124 assert r.exit_code == 0, r.stderr
125 merged_content = r.output
126 assert "a + b # comment" in merged_content
127 assert "a - b # comment" in merged_content
File History 1 commit
sha256:2084d8cc66f5923f35d286b4c8bc7217c01011a4d2690240b554ef18e99c9ab5 fix: comment-only edits to different functions in same file… Sonnet 5 minor ⚠ 1 day ago