test_gsb_branch_collision.py
python
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