gabriel / muse public
test BREAKING feat/29-merge-strategy-matrix-tdd #1 / 1
AI Agent gabriel · 1 day ago · Oct 22, 2026 · Diff

test: comprehensive merge strategy x history x conflict-level TDD suite (muse#29)

New tests/test_merge_matrix.py — ground-truth suite for "Muse shines at merges" per the ticket's own framing: if it's green, the claim is proved; if red, we know exactly what's broken. Per the user's explicit ask, corrects the ticket's own stale strategy matrix and adds octopus:

- The ticket's matrix named a "state_merge" strategy that doesn't exist — the real three-way conflict-detecting strategy is "recursive" (`muse merge --help`: recursive, overlay, snapshot, replay, ours, theirs). Phase 2 exercises recursive x {merge,squash,rebase} and overlay x {merge,squash,rebase} — the two strategies whose behavior meaningfully differs under real conflicts. snapshot/replay/ours/theirs and the full scenario grid (untouched/convergent/divergent/add-add/ delete-modify) are already covered per-strategy by the pre-existing tests/test_phase3_strategy_matrix.py (SM_01-24) — not duplicated. - Octopus (N-way) merge (muse#105), which the ticket predates entirely, is added as its own section: a different code path that ignores --strategy/--history/--on-conflict completely and refuses outright on any path collision (no resolution, matching git). Engine-level depth already exists in tests/test_octopus_merge.py; this file adds the "no work lost" / "ref doesn't move on refusal" / "verify clean" checks the matrix ticket asks for.

make_conflict_repo() builds ONE repo with all three conflict levels the ticket's matrix asks for simultaneously: a genuine symbol-level conflict (calc.py::add, both sides diverge), file-level non-overlapping edits to the same file (shared.py — must auto-merge cleanly, neither side lost), and directory-level additions + a deletion in the same directory (dir_a/ — must auto-merge cleanly, every addition present, deletion applied).

Three real bugs found and fixed while building this TDD-first (per the ticket's own acceptance criterion — "implementation fixed to make tests pass"):

1. muse/cli/commands/merge.py — Case 3 (the auto-resolve path used by overlay/ours/theirs strategies) always wrote a two-parent commit, completely ignoring --history. `muse merge feat --strategy overlay --history squash` reported `"history": "squash"` in its own JSON output while silently writing a two-parent commit anyway. Added the same squash/rebase single-parent branch Case 4 (recursive) already had.

2. muse/core/merge_engine.py + muse/cli/commands/commit.py — completing a conflicted merge via `muse resolve` + `muse commit` always produced a two-parent commit, even when `--history squash`/`--history rebase` was requested on the original `muse merge` — the history mode was silently dropped the moment a conflict forced completion through `muse commit` instead of `muse merge`'s own clean-path commit-writing. Fixed by threading `history` through MERGE_STATE.json (new field, preserved across `muse resolve` calls same as `original_conflict_paths`) and gating commit.py's merge_parent2 logic on it.

3. muse/cli/commands/commit.py — the ticket's own named regression, confirmed reproducible and WORSE than described: if a resolved conflict's content happened to exactly match HEAD's current snapshot (e.g. resolving by keeping "ours"), `muse commit` took the "nothing to commit, working tree clean" early return — exit 0, looks completely benign — but never created the merge commit, never recorded the second parent, and never cleared MERGE_STATE.json. The repo was left PERMANENTLY STUCK: every subsequent `muse merge` re-surfaced the identical already-resolved conflict forever, silently discarding the entire merge rather than just "ours-side only" as the ticket described. Fixed by skipping that early return whenever a merge is actually being completed, regardless of --allow-empty — agents no longer need to know to pass --allow-empty in this case at all.

Also covers (Phase 3): --dry-run mutates nothing (ref, working tree, MERGE_STATE.json, reflog all untouched); muse resolve stages the merged body, not conflict-marker text; Harmony records a resolution after manual resolve (resolution_count == 1); muse verify --json reports all_ok after every scenario.

Confirmed genuine RED for all three fixes by reverting them together and re-running: 7 of 13 tests failed, each landing exactly on one of the three bugs (squash/rebase-after-conflict x2, overlay Case-3 history x2, the stuck-merge regression, and its two parametrized history variants). Restored fixes — all 13 green.

Known gaps, flagged rather than silently worked around: - tests/test_phase3_merge_state_ordering.py::TestCallOrder:: test_apply_manifest_called_before_write_merge_state fails on a clean dev checkout, unrelated to this change (confirmed by shelving these fixes and re-running — identical failure). Pre-existing, not caused or fixed here; left for a separate ticket. - `muse code test --json` reports coverage_fraction: 0.0 / zero selected tests for these changed addresses even after `code index rebuild` — likely requires a committed baseline the coverage mapper can diff against, which doesn't exist for uncommitted changes. Worked around by running the specific targeted test files directly (1352 tests, listed in the regression line below) rather than relying on automatic selection for this verification pass.

Regression (targeted, not full suite per --strategy matrix scope): test_merge_matrix.py, test_cmd_commit*.py, test_cmd_merge*.py, test_phase3_strategy_matrix.py, test_phase6_unified_merge_engine.py, test_core_merge_engine.py, test_merge_data_integrity.py, test_merge_conflict_markers.py, test_merge_supercharge.py, test_phase1_merge_engine.py, test_phase4_harmony_history.py, test_phase7_merge_correctness.py, test_harmony_merge_commit_integrity.py, test_octopus_merge.py, test_addressed_merge_plugin.py, test_comment_only_independence_merge.py, test_merge_base_supercharge.py, test_merge_tree_supercharge.py, test_op_merge.py, test_property_merge_invariants.py, and the full harmony_* suite — 1352 tests, all green, no regressions.

sha256:26088198afa1ef5bf63a4b37349344742d40762144235e27ef26f3e7d9856cae sha
+30 ~7 symbols
4 changed · 1255 in snapshot files
sha256:07c4f2a5debdc594b78b96b72dda4a95bf7a5f4c5ccd1b882e105249bd6d8ea2 snapshot
+30
symbols added
~7
symbols modified
4
files changed
1255
files in snapshot
0
dead code introduced
Semantic Changes 37 symbols
~ tests/test_merge_matrix.py .py 30 symbols added
+ TestAllowEmptySnapshotRegression class class TestAllowEmptySnapshotRegression L353–389
+ test_resolve_matching_ours_still_completes_merge_without_allow_empty method method test_resolve_matching_ours_still_completes_merge_without_allow_empty L357–389
+ TestDryRunNoMutation class class TestDryRunNoMutation L392–422
+ test_dry_run_mutates_nothing method method test_dry_run_mutates_nothing L395–422
+ TestHarmonyLearnsFromResolution class class TestHarmonyLearnsFromResolution L425–442
+ test_harmony_records_resolution_after_manual_resolve method method test_harmony_records_resolution_after_manual_resolve L428–442
+ TestHistoryModeSurvivesConflictCompletion class class TestHistoryModeSurvivesConflictCompletion L445–465
+ test_history_mode_honoured_after_manual_resolve method method test_history_mode_honoured_after_manual_resolve L450–465
+ TestOctopusMerge class class TestOctopusMerge L275–345
+ test_octopus_clean_three_way_no_work_lost method method test_octopus_clean_three_way_no_work_lost L279–308
+ test_octopus_refuses_on_path_collision_no_partial_commit method method test_octopus_refuses_on_path_collision_no_partial_commit L310–345
+ TestOverlayStrategyMatrix class class TestOverlayStrategyMatrix L247–267
+ test_overlay_matrix_cell method method test_overlay_matrix_cell L251–267
+ TestRecursiveStrategyMatrix class class TestRecursiveStrategyMatrix L200–244
+ test_recursive_matrix_cell method method test_recursive_matrix_cell L208–244
+ _branch_from function function _branch_from L82–85
+ _run function function _run L72–73
+ _run_ok function function _run_ok L76–79
+ assert_merge_history function function assert_merge_history L169–185
+ assert_no_work_lost function function assert_no_work_lost L142–157
+ assert_symbol_conflict_listed function function assert_symbol_conflict_listed L160–166
+ assert_verify_clean function function assert_verify_clean L188–192
+ CliRunner import import CliRunner L62–62
+ InvokeResult import import InvokeResult L62–62
+ annotations import import annotations L55–55
+ os import import os L57–57
+ pathlib import import pathlib L58–58
+ pytest import import pytest L60–60
+ make_conflict_repo function function make_conflict_repo L88–139
+ runner variable variable runner L64–64
~ muse/cli/commands/commit.py .py 1 symbol modified
~ run
~ muse/cli/commands/merge.py .py 1 symbol modified
Files Changed
+1 ~3
1255 in snapshot
← Older Oldest on feat/29-merge-strategy-matrix-tdd
All commits
Newer → Latest on feat/29-merge-strategy-matrix-tdd

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:26088198afa1ef5bf63a4b37349344742d40762144235e27ef26f3e7d9856cae --body "your comment"