test_gsb_reconcile_write.py python
172 lines 7.4 KB
Raw
sha256:0e9549ec7b463911bc08b7d586dc320b1ac9b1f5c943ee7e3865dcc6cb0f6f83 chore(governance): sync handover+roadmap to 84db8c8 (drift:… Human 9 hours ago
1 """Integration: injected-runner ``--write`` through §GSB C0 reconcile (§GSB.8).
2
3 Fixture 1 (per regime): existing ``feat/governance-sync-<today>`` behind
4 ``T_target`` → FF then successful ensure, plan/push under the original dated
5 name. Fixture 2: diverged tip → uniquified ``-2`` used for commit / push /
6 ``pr_url``. Fixture 3 (``muse+git-mirror``): Muse HEAD already stranded on
7 the stale dated branch while Git HEAD is on advanced main → C0 still
8 reconciles both histories so C1 does not exit ``2``.
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 gsw_runner, run_cli, seed_gsw_repo
20
21
22 def _feature_branch() -> str:
23 return f"feat/governance-sync-{date.today().isoformat()}"
24
25
26 def test_git_only_existing_branch_behind_ff_then_ensure(tmp_path: Path) -> None:
27 branch = _feature_branch()
28 handover_path, _ = seed_gsw_repo(tmp_path, "git-only")
29 runner = gsw_runner(
30 tmp_path,
31 "git-only",
32 existing_git_branches={branch},
33 git_tips={branch: "stale1"}, # main defaults to feedface
34 git_ancestors={"feedface": {"stale1"}},
35 )
36 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
37 assert code == 0
38 assert runner.git_branch == branch
39 # FF happened via an ancestor-validated tip move, then one sync commit.
40 assert any(c.startswith("git branch -f ") for c, _ in runner.calls)
41 assert runner.git_commit_count == 1
42 tip = runner.git_tips[branch]
43 assert "feedface" in runner.git_ancestors[tip]
44 # Push targets the original dated name — no uniquify on the FF path.
45 push_calls = [c for c, _ in runner.calls if c.startswith("git push")]
46 assert push_calls and all(branch in c and f"{branch}-" not in c for c in push_calls)
47 assert not any(c.startswith("muse") for c, _ in runner.calls)
48 assert "cafebabe" in handover_path.read_text(encoding="utf-8")
49
50
51 def test_muse_only_existing_branch_behind_ff_then_ensure(tmp_path: Path) -> None:
52 branch = _feature_branch()
53 handover_path, _ = seed_gsw_repo(tmp_path, "muse-only")
54 original = handover_path.read_text(encoding="utf-8")
55 runner = gsw_runner(
56 tmp_path,
57 "muse-only",
58 existing_muse_branches={branch},
59 muse_tips={branch: "sha256:stale"}, # main defaults to sha256:musetip
60 muse_ancestors={"sha256:musetip": {"sha256:stale"}},
61 )
62 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
63 assert code == 0
64 assert runner.muse_branch == branch
65 assert any(
66 c.startswith("muse") and f" update-ref {branch} sha256:musetip" in c
67 for c, _ in runner.calls
68 )
69 assert runner.muse_commit_count == 1
70 tip = runner.muse_tips[branch]
71 assert "sha256:musetip" in runner.muse_ancestors[tip]
72 assert handover_path.read_text(encoding="utf-8") != original
73 # §GSB.8 least privilege: muse-only never invokes git/gh.
74 assert not any(c.startswith(("git ", "gh ")) for c, _ in runner.calls)
75
76
77 def test_muse_git_mirror_existing_branch_behind_ff_both_histories(tmp_path: Path) -> None:
78 branch = _feature_branch()
79 seed_gsw_repo(tmp_path, "muse+git-mirror")
80 runner = gsw_runner(
81 tmp_path,
82 "muse+git-mirror",
83 existing_git_branches={branch},
84 existing_muse_branches={branch},
85 git_tips={branch: "gitstale"},
86 muse_tips={branch: "sha256:stale"},
87 git_ancestors={"feedface": {"gitstale"}},
88 muse_ancestors={"sha256:musetip": {"sha256:stale"}},
89 )
90 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
91 assert code == 0
92 assert runner.git_branch == branch
93 assert runner.muse_branch == branch
94 assert any(c.startswith("git branch -f ") for c, _ in runner.calls)
95 assert any(c.startswith("muse") and " update-ref " in c for c, _ in runner.calls)
96 push_calls = [c for c, _ in runner.calls if c.startswith("git push")]
97 assert push_calls and all(branch in c for c in push_calls)
98 assert not any("--force" in c for c, _ in runner.calls)
99
100
101 def test_diverged_tip_uniquifies_commit_push_and_pr_url(tmp_path: Path) -> None:
102 """Fixture 2: a diverged same-day branch keeps its tip; commit/push/pr_url
103 all use the deterministic ``-2`` name (frozen-plan replace)."""
104 branch = _feature_branch()
105 seed_gsw_repo(tmp_path, "muse+git-mirror")
106 runner = gsw_runner(
107 tmp_path,
108 "muse+git-mirror",
109 existing_git_branches={branch},
110 existing_muse_branches={branch},
111 # Muse side diverged; git side behind — cross-history rule → uniquify.
112 git_tips={branch: "gitstale"},
113 muse_tips={branch: "sha256:divergent"},
114 git_ancestors={"feedface": {"gitstale"}},
115 )
116 buf = io.StringIO()
117 with redirect_stdout(buf):
118 code = run_cli(
119 ["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root()
120 )
121 assert code == 0
122 uniquified = f"{branch}-2"
123 assert runner.git_branch == uniquified
124 assert runner.muse_branch == uniquified
125 # Diverged original tips untouched (no partial FF, no rewind).
126 assert runner.git_tips[branch] == "gitstale"
127 assert runner.muse_tips[branch] == "sha256:divergent"
128 push_calls = [c for c, _ in runner.calls if c.startswith("git push")]
129 assert push_calls and all(uniquified in c for c in push_calls)
130 assert f"main...{uniquified}?expand=1" in buf.getvalue()
131
132
133 def test_mirror_muse_head_stranded_on_stale_branch_reconciles(tmp_path: Path) -> None:
134 """Fixture 3 (R1-M2): Muse HEAD already on the stale dated branch while
135 Git HEAD is on advanced main — T_target falls back to configured main on
136 the Muse side, C0 fast-forwards both histories, and C1 succeeds."""
137 branch = _feature_branch()
138 seed_gsw_repo(tmp_path, "muse+git-mirror")
139 runner = gsw_runner(
140 tmp_path,
141 "muse+git-mirror",
142 muse_branch=branch,
143 # Session work in progress on the shared tree — also keeps the KH2
144 # muse-sync gate (muse-dirty + git-clean → pending) out of the way so
145 # the reconcile path itself is what's under test.
146 git_dirty=True,
147 existing_git_branches={branch},
148 existing_muse_branches={"main"},
149 git_tips={branch: "gitstale"},
150 muse_tips={branch: "sha256:stale", "main": "sha256:musetip"},
151 git_ancestors={"feedface": {"gitstale"}},
152 muse_ancestors={"sha256:musetip": {"sha256:stale"}},
153 # Shared worktree holds the advanced-main bytes; the stale tips hold
154 # distinct day-1 content (the live PLS land-b shape).
155 content_map={
156 "sha256:stale": "content:day1-muse",
157 "gitstale": "content:day1-git",
158 },
159 )
160 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
161 assert code == 0
162 assert runner.git_branch == branch
163 assert runner.muse_branch == branch
164 # Muse FF used update-ref against configured main — not HEAD-equal skip.
165 assert any(
166 c.startswith("muse") and f" update-ref {branch} sha256:musetip" in c
167 for c, _ in runner.calls
168 )
169 assert any(c.startswith("git branch -f ") for c, _ in runner.calls)
170 # The Git checkout was never refused by leftover stale bytes.
171 assert not any("--force" in c for c, _ in runner.calls)
172 assert runner.muse_commit_count == 1
File History 1 commit
sha256:6abcf1fa82a7a621ccbc945f19acdba5bc0db54569599404a1452fb4a096a199 fix(ISR): default require_independent_second_reviewer to require Human minor 9 hours ago