gabriel / muse public
test_harmony_merge_commit_integrity.py python
142 lines 5.6 KB
Raw
sha256:aa1523d6dd71d33a7ad57b3c6430556bdfba58cefb95cc85b95a3904932d2749 fix: harmony auto-apply reports success but silently discar… Sonnet 5 16 hours ago
1 """muse#93 — harmony auto-apply reports "auto-resolved" but silently
2 discards the recorded resolution's content.
3
4 Root cause: `harmony_resolved` (returned by `auto_apply`) is keyed by the
5 *original conflict path*, which for the code domain is a symbol address
6 ("calc.py::add"), not a plain file path. `merged_files` -- the manifest
7 that actually gets committed -- is keyed by plain file paths, same as
8 every other manifest in `muse/cli/commands/merge.py`. The old
9 `merged_files.update(harmony_resolved)` inserted a bogus
10 "calc.py::add"-keyed entry while leaving the real "calc.py" entry
11 untouched at its pre-harmony value. The CLI printed
12 "✔ [harmony] auto-resolved: calc.py::add" and incremented the
13 resolution's `applied_count`, but the committed file content stayed
14 "ours" -- the recorded, human-verified resolution was never actually
15 applied to the thing that got committed.
16
17 This test reproduces the exact scenario from the ticket end-to-end
18 through the real CLI (not synthetic objects): two branches conflict on
19 the same symbol, resolve manually to a THIRD value (differing from both
20 "ours" and "theirs"), confirm harmony recorded it, then reproduce the
21 identical conflict pattern on a fresh pair of branches and assert the
22 merged file's *committed* content equals the recorded resolution --
23 not either original side.
24 """
25 from __future__ import annotations
26
27 import json
28 import os
29 import pathlib
30
31 import pytest
32
33 from tests.cli_test_helper import CliRunner, InvokeResult
34
35 runner = CliRunner()
36
37
38 def _run(repo: pathlib.Path, *args: str) -> InvokeResult:
39 saved = os.getcwd()
40 try:
41 os.chdir(repo)
42 return runner.invoke(None, list(args))
43 finally:
44 os.chdir(saved)
45
46
47 def _commit_calc(repo: pathlib.Path, body: str, message: str) -> None:
48 (repo / "calc.py").write_text(f"def add(a, b):\n return {body}\n")
49 r = _run(repo, "code", "add", "calc.py")
50 assert r.exit_code == 0, r.stderr
51 r = _run(repo, "commit", "-m", message)
52 assert r.exit_code == 0, r.stderr
53
54
55 def _branch_from(repo: pathlib.Path, base: str, name: str, body: str, message: str) -> None:
56 r = _run(repo, "checkout", base)
57 assert r.exit_code == 0, r.stderr
58 r = _run(repo, "branch", name)
59 assert r.exit_code == 0, r.stderr
60 r = _run(repo, "checkout", name)
61 assert r.exit_code == 0, r.stderr
62 _commit_calc(repo, body, message)
63
64
65 def test_harmony_auto_apply_commits_the_recorded_resolution_not_ours(tmp_path: pathlib.Path) -> None:
66 repo = tmp_path
67 r = _run(repo, "init")
68 assert r.exit_code == 0, r.stderr
69 _commit_calc(repo, "a + b", "base")
70
71 # First occurrence: a real symbol-level conflict, resolved manually to a
72 # THIRD value that is neither "ours" (+1) nor "theirs" (+100).
73 _branch_from(repo, "main", "feat/a", "a + b + 1", "a changes add")
74 _branch_from(repo, "main", "feat/b", "a + b + 100", "b changes add")
75
76 r = _run(repo, "checkout", "main")
77 assert r.exit_code == 0, r.stderr
78 r = _run(repo, "merge", "feat/a")
79 assert r.exit_code == 0, r.stderr
80
81 r = _run(repo, "merge", "feat/b", "--json")
82 payload = json.loads(r.output)
83 assert payload["status"] == "conflict"
84 assert payload["conflicts"] == ["calc.py::add"]
85
86 (repo / "calc.py").write_text("def add(a, b):\n return a + b + 101\n")
87 r = _run(repo, "resolve", "calc.py::add")
88 assert r.exit_code == 0, r.stderr
89 r = _run(repo, "commit", "-m", "resolve add conflict")
90 assert r.exit_code == 0, r.stderr
91
92 # Confirm harmony actually recorded the resolution before testing replay.
93 r = _run(repo, "harmony", "list", "--json")
94 patterns = json.loads(r.output)["patterns"]
95 assert len(patterns) == 1
96 assert patterns[0]["resolution_count"] == 1
97
98 # Second occurrence: the IDENTICAL conflict pattern on a fresh pair of
99 # branches off the same original base. Harmony should auto-apply the
100 # recorded resolution, not silently fall back to "ours".
101 _branch_from(repo, "main", "feat/c", "a + b + 1", "c changes add")
102 _branch_from(repo, "main", "feat/d", "a + b + 100", "d changes add")
103
104 r = _run(repo, "checkout", "main")
105 assert r.exit_code == 0, r.stderr
106 r = _run(repo, "merge", "feat/c")
107 assert r.exit_code == 0, r.stderr
108
109 r = _run(repo, "merge", "feat/d", "--json")
110 assert r.exit_code == 0, r.stderr
111 payload = json.loads(r.output)
112 assert payload["status"] == "merged", (
113 "harmony should have auto-resolved this -- merge must report 'merged', "
114 "not 'conflict'"
115 )
116
117 # The acceptance bar from the ticket: check BOTH the working tree and the
118 # actual committed snapshot object -- not just a working-tree artifact.
119 working_tree_content = (repo / "calc.py").read_text()
120 assert "+ 101" in working_tree_content, (
121 f"working tree has the wrong content after harmony auto-apply: {working_tree_content!r}"
122 )
123
124 r = _run(repo, "read", "--json", "--manifest")
125 assert r.exit_code == 0, r.stderr
126 manifest = json.loads(r.output)["manifest"]
127 committed_object_id = manifest["calc.py"]
128
129 r = _run(repo, "cat-object", committed_object_id, "--json")
130 assert r.exit_code == 0, r.stderr
131 committed_bytes_info = json.loads(r.output)
132
133 r = _run(repo, "cat", "calc.py")
134 assert r.exit_code == 0, r.stderr
135 committed_content = r.output
136 assert "+ 101" in committed_content, (
137 f"COMMITTED snapshot has the wrong content -- harmony reported "
138 f"auto-resolved but committed 'ours' or 'theirs' instead of the "
139 f"recorded resolution: {committed_content!r}"
140 )
141 assert "+ 1\n" not in committed_content.replace("+ 101", "")
142 assert "+ 100" not in committed_content
File History 1 commit
sha256:aa1523d6dd71d33a7ad57b3c6430556bdfba58cefb95cc85b95a3904932d2749 fix: harmony auto-apply reports success but silently discar… Sonnet 5 16 hours ago