test_gsb_branch_collision.py python
258 lines 10.0 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Unit tests for §GSB C0 reconcile-before-ensure (§GSB.8 unit tier).
2
3 Covers the seven frozen unit cases: (1) ancestor/equal → FF vs non-ancestor
4 → uniquify classification, (2) ``O_H == B`` → ``T_target`` from configured
5 main, (3) deterministic lowest free ``-N`` uniquify, (4) FF via tip update
6 without checkout-as-FF, (5) cross-history diverged side → uniquify with no
7 partial FF, (6) equal tips never uniquify, (7) frozen ``PatchPlan`` replaced
8 so commit/push/``pr_url`` observe the reconciled name.
9 """
10
11 from __future__ import annotations
12
13 import io
14 from contextlib import redirect_stdout
15 from datetime import date
16 from pathlib import Path
17
18 from cli.kit_root import kit_root
19 from tests.support import (
20 BranchStateRunner,
21 adapter_for,
22 gsw_runner,
23 run_cli,
24 seed_gsw_repo,
25 )
26 from tools.governance_hygiene.engine import (
27 BranchState,
28 _classify_git_branch,
29 _classify_muse_branch,
30 _reconcile_feature_branch,
31 _uniquify_branch,
32 )
33
34
35 def _feature_branch() -> str:
36 return f"feat/governance-sync-{date.today().isoformat()}"
37
38
39 def test_git_classifier_ancestor_equal_and_diverged(git_only_config, repo_root) -> None:
40 """§GSB.3.2 case 1: T_exist ancestor-of / equal-to T_target → FF class;
41 non-ancestor → uniquify class."""
42 branch = _feature_branch()
43
44 # Behind (ancestor): stale tip is an ancestor of main's tip.
45 runner = BranchStateRunner(
46 str(repo_root),
47 existing_git_branches={branch},
48 git_tips={branch: "stale1", "main": "tip2"},
49 git_ancestors={"tip2": {"stale1"}},
50 )
51 probe, error = _classify_git_branch(runner, repo_root, git_only_config, branch, "main")
52 assert error is None
53 assert probe is not None and probe.exists and probe.ancestor
54 assert probe.tip == "stale1" and probe.target == "tip2"
55
56 # Equal tips classify as ancestor (§GSB.3.3 equal-tips rule).
57 runner = BranchStateRunner(
58 str(repo_root),
59 existing_git_branches={branch},
60 git_tips={branch: "tip2", "main": "tip2"},
61 )
62 probe, error = _classify_git_branch(runner, repo_root, git_only_config, branch, "main")
63 assert error is None
64 assert probe is not None and probe.ancestor
65
66 # Diverged (both commits known, no ancestry edge) → uniquify class.
67 runner = BranchStateRunner(
68 str(repo_root),
69 existing_git_branches={branch},
70 git_tips={branch: "divergent", "main": "tip2"},
71 )
72 probe, error = _classify_git_branch(runner, repo_root, git_only_config, branch, "main")
73 assert error is None
74 assert probe is not None and probe.exists and not probe.ancestor
75
76
77 def test_target_resolves_to_configured_main_when_head_on_dated_branch(
78 git_only_config, muse_git_mirror_config, repo_root
79 ) -> None:
80 """§GSB.3.2.1 case 2: O_H == B must not use HEAD as T_target — the
81 configured main is the reconcile base ref on both histories."""
82 branch = _feature_branch()
83
84 runner = BranchStateRunner(
85 str(repo_root),
86 git_branch=branch,
87 existing_git_branches={"main"},
88 git_tips={branch: "stale1", "main": "tip2"},
89 git_ancestors={"tip2": {"stale1"}},
90 )
91 probe, error = _classify_git_branch(runner, repo_root, git_only_config, branch, branch)
92 assert error is None
93 assert probe is not None and probe.target == "tip2" and probe.ancestor
94 assert any(c == "git rev-parse main" for c, _ in runner.calls)
95
96 runner = BranchStateRunner(
97 str(repo_root),
98 muse_branch=branch,
99 existing_muse_branches={"main"},
100 muse_tips={branch: "sha256:stale", "main": "sha256:tip2"},
101 muse_ancestors={"sha256:tip2": {"sha256:stale"}},
102 )
103 adapter = adapter_for(muse_git_mirror_config, repo_root, runner)
104 probe, error = _classify_muse_branch(
105 adapter, runner, repo_root, muse_git_mirror_config, branch, branch
106 )
107 assert error is None
108 assert probe is not None and probe.target == "sha256:tip2" and probe.ancestor
109 assert any(c.endswith("rev-parse main") for c, _ in runner.calls if c.startswith("muse"))
110
111
112 def test_uniquify_picks_lowest_free_suffix_deterministically(
113 muse_git_mirror_config, repo_root
114 ) -> None:
115 """§GSB.3.3 case 3: lowest N≥2 free on ALL applicable histories; same
116 inputs → same suffix across runs."""
117 branch = _feature_branch()
118
119 def build() -> BranchStateRunner:
120 # -2 taken on git only, -3 taken on muse only → -4 is the lowest
121 # candidate free everywhere.
122 return BranchStateRunner(
123 str(repo_root),
124 existing_git_branches={branch, f"{branch}-2"},
125 existing_muse_branches={branch, f"{branch}-3"},
126 )
127
128 first = build()
129 adapter = adapter_for(muse_git_mirror_config, repo_root, first)
130 name_one, error = _uniquify_branch(
131 adapter, first, repo_root, muse_git_mirror_config, branch
132 )
133 assert error is None
134 assert name_one == f"{branch}-4"
135
136 second = build()
137 adapter = adapter_for(muse_git_mirror_config, repo_root, second)
138 name_two, error = _uniquify_branch(
139 adapter, second, repo_root, muse_git_mirror_config, branch
140 )
141 assert error is None
142 assert name_two == name_one
143
144
145 def test_ff_uses_tip_update_never_checkout_of_dated_branch(
146 muse_git_mirror_config, repo_root
147 ) -> None:
148 """§GSB.3.3 case 4: FF path moves tips via allowed forms (update-ref /
149 branch -f / muse -C copy) — never a checkout of B as the FF mechanism."""
150 branch = _feature_branch()
151 runner = BranchStateRunner(
152 str(repo_root),
153 existing_git_branches={branch},
154 existing_muse_branches={branch},
155 git_tips={branch: "gitstale", "main": "tip2"},
156 muse_tips={branch: "sha256:stale", "main": "sha256:tip2"},
157 git_ancestors={"tip2": {"gitstale"}},
158 muse_ancestors={"sha256:tip2": {"sha256:stale"}},
159 )
160 adapter = adapter_for(muse_git_mirror_config, repo_root, runner)
161 state = BranchState(git_branch="main", muse_branch="main")
162 reconciled, error = _reconcile_feature_branch(
163 adapter, runner, repo_root, muse_git_mirror_config, branch, state
164 )
165 assert error is None
166 assert reconciled == branch
167 assert runner.git_tips[branch] == "tip2"
168 assert runner.muse_tips[branch] == "sha256:tip2"
169 commands = [c for c, _ in runner.calls]
170 assert any(c.startswith("muse") and " update-ref " in c for c in commands)
171 assert any(c.startswith("git branch -f ") for c in commands)
172 # C0 spy rule: no Muse or Git checkout at all during reconcile.
173 assert not any("checkout" in c for c in commands)
174 assert not any("--force" in c for c in commands)
175
176
177 def test_cross_history_divergence_uniquifies_without_partial_ff(
178 muse_git_mirror_config, repo_root
179 ) -> None:
180 """§GSB.3.2 case 5: one side diverged → uniquify the shared name; the
181 ancestor side must NOT be fast-forwarded under the original name."""
182 branch = _feature_branch()
183 runner = BranchStateRunner(
184 str(repo_root),
185 existing_git_branches={branch},
186 existing_muse_branches={branch},
187 # Git side is a clean ancestor; Muse side diverged (no ancestry edge).
188 git_tips={branch: "gitstale", "main": "tip2"},
189 muse_tips={branch: "sha256:divergent", "main": "sha256:tip2"},
190 git_ancestors={"tip2": {"gitstale"}},
191 )
192 adapter = adapter_for(muse_git_mirror_config, repo_root, runner)
193 state = BranchState(git_branch="main", muse_branch="main")
194 reconciled, error = _reconcile_feature_branch(
195 adapter, runner, repo_root, muse_git_mirror_config, branch, state
196 )
197 assert error is None
198 assert reconciled == f"{branch}-2"
199 # No partial FF: both existing tips untouched.
200 assert runner.git_tips[branch] == "gitstale"
201 assert runner.muse_tips[branch] == "sha256:divergent"
202 commands = [c for c, _ in runner.calls]
203 assert not any(" update-ref " in c for c in commands)
204 assert not any(c.startswith("git branch -f ") for c in commands)
205
206
207 def test_equal_tips_never_uniquify(muse_git_mirror_config, repo_root) -> None:
208 """§GSB.3.3 case 6: equal tips are the ancestor class — a no-op FF, and
209 never a reason to uniquify."""
210 branch = _feature_branch()
211 runner = BranchStateRunner(
212 str(repo_root),
213 existing_git_branches={branch},
214 existing_muse_branches={branch},
215 git_tips={branch: "tip2", "main": "tip2"},
216 muse_tips={branch: "sha256:tip2", "main": "sha256:tip2"},
217 )
218 adapter = adapter_for(muse_git_mirror_config, repo_root, runner)
219 state = BranchState(git_branch="main", muse_branch="main")
220 reconciled, error = _reconcile_feature_branch(
221 adapter, runner, repo_root, muse_git_mirror_config, branch, state
222 )
223 assert error is None
224 assert reconciled == branch
225 commands = [c for c, _ in runner.calls]
226 assert not any(" update-ref " in c for c in commands)
227 assert not any(c.startswith("git branch -f ") for c in commands)
228 assert not any("checkout" in c for c in commands)
229
230
231 def test_uniquify_replaces_frozen_plan_branch_and_pr_url(tmp_path: Path) -> None:
232 """§GSB.3.3 case 7 (R1-M1/R1-M5): the frozen PatchPlan is replaced so the
233 success-path commit, push, and pr_url all observe the uniquified name."""
234 branch = _feature_branch()
235 seed_gsw_repo(tmp_path, "git-only")
236 runner = gsw_runner(
237 tmp_path,
238 "git-only",
239 existing_git_branches={branch},
240 # Diverged: both tips known to the graph, no ancestry edge.
241 git_tips={branch: "divergent", "main": "feedface"},
242 )
243
244 buf = io.StringIO()
245 with redirect_stdout(buf):
246 code = run_cli(
247 ["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root()
248 )
249 assert code == 0
250 uniquified = f"{branch}-2"
251 assert runner.git_branch == uniquified
252 # Original diverged branch untouched; new commit landed on the -2 name.
253 assert runner.git_tips[branch] == "divergent"
254 push_calls = [c for c, _ in runner.calls if c.startswith("git push")]
255 assert push_calls and all(uniquified in c for c in push_calls)
256 output = buf.getvalue()
257 assert f"governance-sync branch uniquified: {uniquified}" in output
258 assert f"main...{uniquified}?expand=1" in output
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago