"""muse#33 — call Harmony inside the ``muse rebase`` replay loop. Before this fix, ``muse rebase``'s real per-commit replay loop (``_run_replay_loop`` / ``replay_one`` in ``muse/core/rebase.py``) never consulted Harmony at all: a conflict always paused the rebase, even when Harmony already held a saved resolution for the exact same conflict pattern. And once a rebase conflict WAS resolved manually via ``muse resolve`` + ``muse rebase --continue``, the resolution was never recorded — ``--continue`` wrote the new commit directly, bypassing the ``record_resolutions`` call that ``muse commit`` makes for an equivalent ``muse merge`` conflict. ``MERGE_STATE.json`` was also left behind on disk after a rebase completed, since nothing ever cleared it. This test drives the real CLI (not synthetic objects) through both halves of the fix: 1. A rebase conflict, resolved manually, must be recorded by Harmony using the replayed commit's OWN ours/theirs context (HR_01, HR_02, RB_03) — not any squash-equivalent snapshot (RB_04) — and must clear MERGE_STATE.json once finalised. 2. The identical conflict pattern, replayed again in a fresh rebase, must be auto-resolved by Harmony without pausing (RB_02). """ 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) def _commit_calc(repo: pathlib.Path, body: str, message: str) -> None: (repo / "calc.py").write_text(f"def add(a, b):\n return {body}\n") r = _run(repo, "code", "add", "calc.py") assert r.exit_code == 0, r.stderr r = _run(repo, "commit", "-m", message) assert r.exit_code == 0, r.stderr def _branch_from(repo: pathlib.Path, base: str, name: str) -> None: r = _run(repo, "checkout", base) assert r.exit_code == 0, r.stderr r = _run(repo, "branch", name) assert r.exit_code == 0, r.stderr r = _run(repo, "checkout", name) assert r.exit_code == 0, r.stderr def test_rebase_conflict_resolution_is_recorded_and_merge_state_cleared( tmp_path: pathlib.Path, ) -> None: repo = tmp_path r = _run(repo, "init") assert r.exit_code == 0, r.stderr _commit_calc(repo, "a + b", "base") # feat: the branch whose commit will be replayed ("theirs" during rebase). _branch_from(repo, "main", "feat") _commit_calc(repo, "a + b + 1", "feat changes add") # main moves on independently ("ours" — the new rebase target tip). r = _run(repo, "checkout", "main") assert r.exit_code == 0, r.stderr _commit_calc(repo, "a + b + 100", "main changes add") r = _run(repo, "checkout", "feat") assert r.exit_code == 0, r.stderr r = _run(repo, "rebase", "main", "--json") assert r.exit_code == 1, r.output payload = json.loads(r.output) assert payload["status"] == "conflict" assert payload["conflicts"] == ["calc.py"] # MERGE_STATE.json must exist while the rebase is paused. assert (repo / ".muse" / "MERGE_STATE.json").exists() # Resolve manually to a THIRD value (neither side) and continue. (repo / "calc.py").write_text("def add(a, b):\n return a + b + 101\n") r = _run(repo, "resolve", "calc.py") assert r.exit_code == 0, r.stderr r = _run(repo, "rebase", "--continue", "--json") assert r.exit_code == 0, r.output payload = json.loads(r.output) assert payload["status"] == "completed" # MERGE_STATE.json must be cleared once the conflict's commit lands. assert not (repo / ".muse" / "MERGE_STATE.json").exists() # Harmony must have recorded exactly this resolution. r = _run(repo, "harmony", "list", "--json") patterns = json.loads(r.output)["patterns"] assert len(patterns) == 1 assert patterns[0]["resolution_count"] == 1 r = _run(repo, "cat", "calc.py") assert r.exit_code == 0, r.stderr assert "+ 101" in r.output def test_rebase_auto_resolves_identical_conflict_via_harmony( tmp_path: pathlib.Path, ) -> None: repo = tmp_path r = _run(repo, "init") assert r.exit_code == 0, r.stderr _commit_calc(repo, "a + b", "base") # Stable pointer at the original base commit — both feat/a and feat/b # must branch from here (not from main's moving tip) so their replay # produces the identical base/ours/theirs blob triple both times. r = _run(repo, "branch", "base") assert r.exit_code == 0, r.stderr # First occurrence — same shape as the test above — teaches harmony. _branch_from(repo, "base", "feat/a") _commit_calc(repo, "a + b + 1", "a changes add") r = _run(repo, "checkout", "main") assert r.exit_code == 0, r.stderr _commit_calc(repo, "a + b + 100", "main changes add (a)") r = _run(repo, "checkout", "feat/a") assert r.exit_code == 0, r.stderr r = _run(repo, "rebase", "main", "--json") assert json.loads(r.output)["status"] == "conflict" (repo / "calc.py").write_text("def add(a, b):\n return a + b + 101\n") r = _run(repo, "resolve", "calc.py") assert r.exit_code == 0, r.stderr r = _run(repo, "rebase", "--continue", "--json") assert r.exit_code == 0, r.output assert json.loads(r.output)["status"] == "completed" r = _run(repo, "harmony", "list", "--json") patterns = json.loads(r.output)["patterns"] assert len(patterns) == 1 assert patterns[0]["resolution_count"] == 1 # Second occurrence — identical conflict pattern: feat/b branches from # the same original "base" pointer (not main's tip, which has already # moved to "a + b + 100") and gets the identical "a + b + 1" content, # reproducing the exact same base/ours/theirs blob triple as feat/a. _branch_from(repo, "base", "feat/b") _commit_calc(repo, "a + b + 1", "b changes add") r = _run(repo, "rebase", "main", "--json") assert r.exit_code == 0, r.output payload = json.loads(r.output) assert payload["status"] == "completed", ( "harmony should have auto-resolved this during replay -- rebase " f"must report 'completed', not pause: {payload}" ) # No conflict ever paused this rebase, so no MERGE_STATE.json should # ever have existed for it. assert not (repo / ".muse" / "MERGE_STATE.json").exists() r = _run(repo, "cat", "calc.py") assert r.exit_code == 0, r.stderr assert "+ 101" in r.output assert "+ 1\n" not in r.output.replace("+ 101", "") assert "+ 100" not in r.output