"""muse#92 — code domain's address-keyed merge falsely conflicts when both sides add a trailing comment to different functions in the same file. Root cause: a comment-only edit to a function is AST-identical to the base (content_id is computed via `ast.unparse()`, which strips comments by design — see ast_parser.py's PythonAdapter docstring). `build_diff_ops` sees no symbol-level `child_ops` for such a file and falls back to a whole-file `ReplaceOp` labeled "reformatted — no semantic change" instead of a symbol-addressed `PatchOp`. When BOTH branches independently add a comment to a DIFFERENT function in the same file, `merge_ops()`'s Step 1.5 (independence-aware blob reconstruction) can't find `PatchOp`s for either side and used to just `continue`, skipping reconstruction entirely. Step 2 (coarse OT over the two competing file-level `ReplaceOp`s) then flagged the file as a hard conflict, even though the two edits don't overlap at all. Fix: when symbol-level patch info isn't available for a candidate path, attempt a direct three-way LINE merge on the raw blobs (`three_way_merge_lines` from `muse.core.cohen_transform`, the same primitive `_independence_merge_blob` already uses for genuine independence merges). This is safe regardless of symbol knowledge: if the base/ours/theirs line diffs don't overlap, combining them is correct no matter what the file's AST looks like. A second bug was found while verifying this fix live: Step 2's OT check re-flags the file as conflicting independently of Step 1.5's resolution, so `conflict_addresses` must also be filtered against `independence_resolved` before Step 3. Reproduces the exact scenario via the real CLI (not synthetic objects): two branches, each adding a trailing comment to a different function in the same file, must merge with zero conflicts and both comments present in the committed result. """ from __future__ import annotations import json import os import pathlib from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() def _run(repo: pathlib.Path, *args: str) -> InvokeResult: saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, list(args)) finally: os.chdir(saved) _BASE_CALC = ( "def add(a, b):\n" " return a + b\n" "\n" "def subtract(a, b):\n" " return a - b\n" ) _ALICE_CALC = ( "def add(a, b):\n" " return a + b # comment\n" "\n" "def subtract(a, b):\n" " return a - b\n" ) _BOB_CALC = ( "def add(a, b):\n" " return a + b\n" "\n" "def subtract(a, b):\n" " return a - b # comment\n" ) def test_trailing_comments_on_different_functions_merge_cleanly(tmp_path: pathlib.Path) -> None: repo = tmp_path r = _run(repo, "init") assert r.exit_code == 0, r.stderr (repo / "calc.py").write_text(_BASE_CALC) r = _run(repo, "code", "add", "calc.py") assert r.exit_code == 0, r.stderr r = _run(repo, "commit", "-m", "base") assert r.exit_code == 0, r.stderr r = _run(repo, "branch", "feature/alice") assert r.exit_code == 0, r.stderr r = _run(repo, "checkout", "feature/alice") assert r.exit_code == 0, r.stderr (repo / "calc.py").write_text(_ALICE_CALC) r = _run(repo, "code", "add", "calc.py") assert r.exit_code == 0, r.stderr r = _run(repo, "commit", "-m", "alice comments add") assert r.exit_code == 0, r.stderr r = _run(repo, "checkout", "main") assert r.exit_code == 0, r.stderr r = _run(repo, "branch", "feature/bob") assert r.exit_code == 0, r.stderr r = _run(repo, "checkout", "feature/bob") assert r.exit_code == 0, r.stderr (repo / "calc.py").write_text(_BOB_CALC) r = _run(repo, "code", "add", "calc.py") assert r.exit_code == 0, r.stderr r = _run(repo, "commit", "-m", "bob comments subtract") assert r.exit_code == 0, r.stderr r = _run(repo, "checkout", "main") assert r.exit_code == 0, r.stderr r = _run(repo, "merge", "feature/alice") assert r.exit_code == 0, r.stderr r = _run(repo, "merge", "feature/bob", "--json") assert r.exit_code == 0, r.stderr payload = json.loads(r.output) assert payload["status"] == "merged", ( f"independent comment-only edits to different functions must merge " f"cleanly with zero conflicts, got: {payload}" ) assert payload["conflicts"] == [] r = _run(repo, "cat", "calc.py") assert r.exit_code == 0, r.stderr merged_content = r.output assert "a + b # comment" in merged_content assert "a - b # comment" in merged_content