gabriel / muse public

test_cmd_branch_copy_reflog.py file-level

at sha256:f · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:6 fix: muse coord shard never resolved 'from X import Y' import edges (mu… · gabriel · Oct 22, 2026
1 """TDD tests for muse#48 Phase 1 — ``muse branch -c``/``-C`` reflog copy parity with git.
2
3 ``git branch -c src dst`` copies ``src``'s reflog to ``dst`` and prepends a
4 "copied from" entry marking the origin. Before this ticket, muse's ``-c``
5 wrote only the branch ref — the destination branch had no reflog at all,
6 even when the source had a rich history.
7
8 Covers BC_01–BC_07.
9 """
10
11 from __future__ import annotations
12
13 import pathlib
14
15 import pytest
16
17 from muse.core.paths import reflog_branch_path
18 from muse.core.reflog import read_reflog
19 from muse.core.types import NULL_COMMIT_ID
20 from tests.test_cmd_branch import _branch, _commit, _invoke, repo # noqa: F401
21
22
23 class TestCopyReflogBasic:
24 def test_bc01_log_copied_byte_for_byte_plus_new_entry(self, repo: pathlib.Path) -> None:
25 """BC_01: the copy is byte-for-byte identical to the source except
26 for the new entry. Reflog files are oldest-first on disk (newest
27 read back as @{0} only after read_reflog reverses them), so the new
28 entry is appended after the copied bytes, not prepended before them."""
29 _branch(repo, "dev")
30 _invoke(repo, ["checkout", "dev"])
31 (repo / "x.py").write_text("x = 1\n")
32 _commit(repo, "-m", "dev commit")
33 _invoke(repo, ["checkout", "main"])
34
35 src_log_path = reflog_branch_path(repo, "dev")
36 src_bytes = src_log_path.read_bytes()
37
38 result = _branch(repo, "-c", "dev", "feat/x")
39 assert result.exit_code == 0
40
41 dst_log_path = reflog_branch_path(repo, "feat/x")
42 dst_bytes = dst_log_path.read_bytes()
43
44 # Destination log = source log (verbatim) + [new line] appended.
45 assert dst_bytes.startswith(src_bytes)
46 appended = dst_bytes[len(src_bytes):]
47 assert appended.count(b"\n") == 1
48
49 def test_bc02_prepended_entry_fields(self, repo: pathlib.Path) -> None:
50 """BC_02: the prepended entry has operation 'branch: copied from
51 <src>', new_id = tip of src, old_id = null ID."""
52 _branch(repo, "dev")
53 tip = _invoke(repo, ["rev-parse", "dev"]).output.strip()
54
55 _branch(repo, "-c", "dev", "feat/y")
56
57 entries = read_reflog(repo, branch="feat/y", limit=10)
58 assert entries[0].operation == "branch: copied from dev"
59 assert entries[0].new_id == tip
60 assert entries[0].old_id == NULL_COMMIT_ID
61
62 def test_bc03_reflog_cli_shows_prepended_then_source_history(
63 self, repo: pathlib.Path
64 ) -> None:
65 """BC_03: ``muse reflog --branch dst`` returns the prepended entry at
66 index 0, source history following."""
67 _branch(repo, "dev")
68 _invoke(repo, ["checkout", "dev"])
69 (repo / "a.py").write_text("a = 1\n")
70 _commit(repo, "-m", "dev change")
71 _invoke(repo, ["checkout", "main"])
72
73 src_entries = read_reflog(repo, branch="dev", limit=100)
74 _branch(repo, "-c", "dev", "backup/dev")
75 dst_entries = read_reflog(repo, branch="backup/dev", limit=100)
76
77 assert dst_entries[0].operation == "branch: copied from dev"
78 assert [e.operation for e in dst_entries[1:]] == [e.operation for e in src_entries]
79
80 def test_bc04_force_copy_replaces_destination_log(self, repo: pathlib.Path) -> None:
81 """BC_04: force-copy (-C) replaces the destination's reflog wholesale
82 — the previous destination log is not preserved."""
83 _branch(repo, "src")
84 _branch(repo, "dst")
85 _invoke(repo, ["checkout", "dst"])
86 (repo / "dst-only.py").write_text("unique = True\n")
87 _commit(repo, "-m", "dst-only commit")
88 _invoke(repo, ["checkout", "main"])
89
90 src_entries = read_reflog(repo, branch="src", limit=100)
91
92 _branch(repo, "-C", "src", "dst")
93 dst_entries_after = read_reflog(repo, branch="dst", limit=100)
94
95 # The dst-only commit entry must be gone — wholesale replacement,
96 # not a merge of old and new history.
97 assert all("dst-only" not in e.operation for e in dst_entries_after)
98 assert dst_entries_after[0].operation == "branch: copied from src"
99 assert len(dst_entries_after) == len(src_entries) + 1
100
101 def test_bc05_source_with_no_reflog_writes_just_origin_entry(
102 self, tmp_path: pathlib.Path
103 ) -> None:
104 """BC_05: if the source branch has no reflog, copy writes just the
105 single origin entry with no prior history."""
106 import os
107
108 saved = os.getcwd()
109 try:
110 os.chdir(tmp_path)
111 from tests.cli_test_helper import CliRunner
112 CliRunner().invoke(None, ["init"])
113 finally:
114 os.chdir(saved)
115
116 # "main" exists with no commits yet — no reflog file.
117 src_log = reflog_branch_path(tmp_path, "main")
118 assert not src_log.exists()
119
120 result = _branch(tmp_path, "-c", "main", "copy-of-main")
121 # No commits yet means no tip to copy — branch -c with an empty
122 # source produces an empty destination branch and no reflog either
123 # (there is no tip to record a "copied from" entry about).
124 assert result.exit_code == 0
125 dst_entries = read_reflog(tmp_path, branch="copy-of-main", limit=10)
126 assert dst_entries == []
127
128 def test_bc05b_source_with_reflog_but_dest_from_scratch(self, repo: pathlib.Path) -> None:
129 """Companion to BC_05: a source *with* a reflog but freshly copied
130 produces exactly source-entries + 1, with no leftover state."""
131 _branch(repo, "dev")
132 src_entries = read_reflog(repo, branch="dev", limit=100)
133 assert len(src_entries) == 1 # just "created from main"
134
135 _branch(repo, "-c", "dev", "fresh/copy")
136 dst_entries = read_reflog(repo, branch="fresh/copy", limit=100)
137 assert len(dst_entries) == len(src_entries) + 1
138
139 def test_bc06_ref_written_before_reflog_copy_is_safe_if_interrupted(
140 self, repo: pathlib.Path
141 ) -> None:
142 """BC_06: if interrupted after the ref write but before the log
143 write, the branch is still valid (just missing the log) — simulated
144 by writing only the ref and confirming read_reflog degrades
145 gracefully rather than raising."""
146 from muse.core.refs import write_branch_ref, get_head_commit_id
147
148 tip = get_head_commit_id(repo, "main")
149 write_branch_ref(repo, "half-copied", tip)
150 # No reflog written yet — simulates an interruption between steps.
151 entries = read_reflog(repo, branch="half-copied", limit=10)
152 assert entries == []
153
154 def test_bc07_integration_five_entries_copy_yields_six(self, repo: pathlib.Path) -> None:
155 """BC_07: a branch with N reflog entries -> muse branch -c -> the
156 destination shows N+1 entries (source history + the copy marker)."""
157 _branch(repo, "dev")
158 _invoke(repo, ["checkout", "dev"])
159 for i in range(4):
160 (repo / f"f{i}.py").write_text(f"v = {i}\n")
161 _commit(repo, "-m", f"change {i}")
162 _invoke(repo, ["checkout", "main"])
163
164 src_entries = read_reflog(repo, branch="dev", limit=100)
165 n = len(src_entries)
166
167 result = _branch(repo, "-c", "dev", "backup/dev")
168 assert result.exit_code == 0
169
170 dst_entries = read_reflog(repo, branch="backup/dev", limit=100)
171 assert len(dst_entries) == n + 1