test_reconciliation.py python
267 lines 12.7 KB
Raw
sha256:106ef9e12cca7f3be3ffb2a6bd3abfa76f032dce453c9e5010d732a8d86f2e14 Reconcile reviewed credential compatibility 32254cd939885d9… Human patch 8 days ago
1 """Divergent legacy history, exact disposition and retry on disposable objects."""
2 import base64
3 from copy import deepcopy
4 import json
5 from pathlib import Path
6 from unittest.mock import patch
7
8 import pytest
9 import yaml
10
11 from cli import v1_mirror as mirror
12 from cli import v1_mirror_history as history
13 from cli.v1_io import Refusal, digest
14 from cli.v1_revision import muse_read
15 from tests.muse.test_handoffs import inventory
16 from tests.muse.test_mirror import case, execute, Delivery
17
18
19 def legacy(case, tmp_path, before=None, unrelated=False):
20 root, plan = case
21 config = yaml.safe_load((root / '.overseer/config.yaml').read_bytes())
22 files = mirror.check_source(root, config, plan)['files']
23 before = deepcopy(files) if before is None else before
24 if 'legacy-only' not in before and before.keys() == files.keys():
25 before['legacy-only'] = {'data': base64.b64encode(b'reviewed removal\n').decode(), 'mode': '100644'}
26 repo = tmp_path / 'history.git'
27 repo.mkdir()
28 mirror.git(repo, 'init', '--bare', '--template=')
29 tree = mirror.tree(repo, before)
30 def commit(message, parent=None):
31 return mirror.git(repo, 'commit-tree', tree, *(['-p', parent] if parent else []),
32 data=(message + '\n').encode()).decode().strip()
33 common = commit('common')
34 base = commit('main only', None if unrelated else common)
35 tip = common
36 for i in range(6):
37 tip = commit('mirror only ' + str(i), tip)
38 for branch, head in (('main', base), ('muse-mirror', tip)):
39 mirror.git(repo, 'update-ref', 'refs/heads/' + branch, head)
40 bundle = tmp_path / 'approved.bundle'
41 mirror.git(repo, 'bundle', 'create', '--version=2', str(bundle), 'refs/heads/main', 'refs/heads/muse-mirror')
42 after = {p: {'sha256': digest(base64.b64decode(v['data'])), 'mode': v['mode']} for p, v in files.items()}
43 plan['destination']['expected_head'] = tip
44 plan['reconciliation'] = {'bundle': str(bundle), 'bundle_sha256': digest(bundle.read_bytes()),
45 'mirror_head': tip, 'base_head': base,
46 'delta': history.delta(history.manifest(repo, tip), after)}
47 return repo
48
49
50 @pytest.fixture
51 def reconciliation(case, tmp_path):
52 legacy(case, tmp_path)
53 return case
54
55
56 def prepared(case):
57 result = execute(case, 'reconcile')
58 assert result['ok'], result
59 return result
60
61
62 def test_exact_110_path_delta_divergent_histories_modes_and_exclusions(case, muse, muse_python, tmp_path):
63 root, plan = case
64 for i in range(31):
65 (root / f'change-{i}').write_text('approved change\n')
66 additions = [f'new-{i}' for i in range(45)] + ['.github/workflows/fixture.yml']
67 for path in additions:
68 file = root / path
69 file.parent.mkdir(parents=True, exist_ok=True)
70 file.write_text('approved addition\n')
71 muse(root, 'code', 'add', '.', '.github/workflows/fixture.yml')
72 muse(root, 'commit', '-m', '110 path source', '--json')
73 plan['source']['revision'] = muse_read(root, str(muse_python))['head']
74 config = yaml.safe_load((root / '.overseer/config.yaml').read_bytes())
75 before = deepcopy(mirror.check_source(root, config, plan)['files'])
76 for path in additions:
77 del before[path]
78 for i in range(31):
79 before[f'change-{i}']['data'] = base64.b64encode(b'old change\n').decode()
80 before['run-tool']['mode'] = '100644'
81 for prefix, count in (('.cursor/local-', 24), ('.overseer/legacy-', 8)):
82 for i in range(count):
83 before[prefix + str(i)] = {'data': base64.b64encode(b'old local binding\n').decode(), 'mode': '100644'}
84 legacy(case, tmp_path, before)
85 delta = plan['reconciliation']['delta']
86 assert len(delta) == 110
87 assert sum(v['before'] is None for v in delta.values()) == 46
88 assert sum(v['after'] is None for v in delta.values()) == 32
89 first = prepared(case)
90 target = Path(plan['target'])
91 parents = mirror.expected_parents(plan)
92 assert mirror.git(target, 'show', '-s', '--format=%P', 'HEAD').decode().strip().split() == parents
93 for parent in parents:
94 mirror.git(target, 'merge-base', '--is-ancestor', parent, first['git_head'])
95 assert mirror.git(target, 'rev-list', '--left-right', '--count', parents[1] + '...' + parents[0]) == b'1\t6\n'
96 paths = mirror.git(target, 'ls-tree', '-r', '--name-only', 'HEAD').decode().splitlines()
97 assert not any(p.startswith(('.cursor/', '.overseer/')) for p in paths)
98 assert mirror.git(target, 'ls-tree', 'HEAD', 'run-tool').startswith(b'100755 ')
99 before_retry = inventory(root), inventory(target)
100 assert prepared(case)['git_head'] == first['git_head']
101 assert execute(case, 'verify')['ok']
102 assert (inventory(root), inventory(target)) == before_retry
103
104
105 @pytest.mark.parametrize('change', ['mirror_head', 'base_head', 'bundle_hash', 'bundle_ref', 'corrupt_pack',
106 'missing_delta', 'changed_delta', 'foreign_target', 'empty_target'])
107 def test_reconciliation_refuses_unreviewed_inputs(reconciliation, change):
108 root, plan = reconciliation
109 r = plan['reconciliation']
110 if change in ('mirror_head', 'base_head'):
111 r[change] = 'a' * 40
112 plan['destination']['expected_head'] = r['mirror_head']
113 elif change == 'bundle_hash': r['bundle_sha256'] = 'a' * 64
114 elif change in ('bundle_ref', 'corrupt_pack'):
115 path = Path(r['bundle'])
116 data = path.read_bytes()
117 data = data.replace(b'refs/heads/main', b'refs/heads/other') if change == 'bundle_ref' else data[:-10]
118 path.write_bytes(data); r['bundle_sha256'] = digest(data)
119 elif change == 'missing_delta': r['delta'] = {}
120 elif change == 'changed_delta': next(iter(r['delta'].values()))['before'] = 'a' * 64
121 else:
122 Path(plan['target']).mkdir()
123 if change == 'foreign_target': (Path(plan['target']) / 'keep').write_text('original')
124 before = inventory(root)
125 assert not execute(reconciliation, 'reconcile')['ok']
126 assert inventory(root) == before
127 if change not in ('foreign_target', 'empty_target'):
128 assert not Path(plan['target']).exists()
129
130
131 def test_unrelated_history_is_refused(case, tmp_path):
132 legacy(case, tmp_path, unrelated=True)
133 result = execute(case, 'reconcile')
134 assert not result['ok'] and result['error'] == 'mirror_history_unrelated_heads'
135 assert not Path(case[1]['target']).exists()
136
137
138 def test_reconciliation_requires_explicit_operation(reconciliation):
139 assert not execute(reconciliation, 'prepare')['ok']
140 assert not Path(reconciliation[1]['target']).exists()
141 reconciliation[1]['destination']['expected_base'] = 'a' * 40
142 with pytest.raises(Refusal, match='reconciliation_head_mismatch'):
143 execute(reconciliation, 'reconcile')
144 assert not Path(reconciliation[1]['target']).exists()
145
146
147 @pytest.mark.parametrize('point', ['import', 'before_ref', 'after_ref'])
148 def test_interrupted_reconciliation_recovers_exact_correspondence(reconciliation, point):
149 if point == 'import':
150 with patch.object(history, 'verify_history', side_effect=OSError('fixture interruption')):
151 assert not execute(reconciliation, 'reconcile')['ok']
152 assert not Path(reconciliation[1]['target']).exists()
153 elif point == 'before_ref':
154 original = mirror.git
155 def fail(target, *args, **kwargs):
156 if args[0] == 'update-ref': raise OSError('fixture interruption')
157 return original(target, *args, **kwargs)
158 with patch.object(mirror, 'git', side_effect=fail):
159 assert not execute(reconciliation, 'reconcile')['ok']
160 else:
161 with patch.object(mirror, 'save_record', side_effect=OSError('fixture interruption')):
162 interrupted = execute(reconciliation, 'reconcile')
163 assert not interrupted['ok']
164 result = prepared(reconciliation)
165 if point == 'after_ref': assert result['git_head'] == interrupted['git_head']
166 assert prepared(reconciliation)['git_head'] == result['git_head']
167
168
169 class LegacyDelivery(Delivery):
170 def __init__(self, plan):
171 super().__init__(plan)
172 self.base = plan['reconciliation']['base_head']
173
174 def remote_head(self, dest):
175 if dest['branch'] == dest['base']:
176 self.calls.append(('base', deepcopy(dest)))
177 return self.base
178 return super().remote_head(dest)
179
180
181 @pytest.mark.parametrize('point', ['initial', 'before_push', 'after_push'])
182 def test_live_base_drift_stops_write_or_reports_partial_result(reconciliation, point):
183 prepared(reconciliation)
184 transport = LegacyDelivery(reconciliation[1])
185 original = transport.remote_head
186 count = 0
187 def changing(dest):
188 nonlocal count
189 if dest['branch'] == dest['base']:
190 count += 1
191 if count == {'initial': 1, 'before_push': 2, 'after_push': 3}[point]:
192 transport.base = 'a' * 40
193 return original(dest)
194 transport.remote_head = changing
195 result = execute(reconciliation, 'deliver', transport)
196 assert not result['ok'] and result['error'] == 'mirror_remote_base_drift'
197 assert not any(c[0] == 'pr' for c in transport.calls)
198 assert sum(c[0] == 'push' for c in transport.calls) == (point == 'after_push')
199
200
201 def test_reconciled_delivery_retry_and_later_single_parent(reconciliation, muse, muse_python):
202 root, plan = reconciliation
203 first = prepared(reconciliation)
204 transport = LegacyDelivery(plan)
205 transport.fail_push = 'before'
206 assert execute(reconciliation, 'deliver', transport)['push'] == 'pending_retry'
207 transport.fail_push = 'after'
208 assert execute(reconciliation, 'deliver', transport)['push'] == 'verified'
209 transport.calls.clear()
210 assert execute(reconciliation, 'deliver', transport)['push'] == 'already_delivered'
211 assert not any(c[0] == 'push' for c in transport.calls)
212 muse(root, 'commit', '--allow-empty', '-m', 'later source', '--json')
213 plan['source']['revision'] = muse_read(root, str(muse_python))['head']
214 plan['expected_target'] = plan['destination']['expected_head'] = first['git_head']
215 # A later reviewed plan can pin an advanced PR base without rewriting the
216 # immutable origin binding or adding a second parent to subsequent exports.
217 plan['destination']['expected_base'] = first['git_head']
218 later = execute(reconciliation)
219 assert later['ok'], later
220 assert mirror.git(Path(plan['target']), 'show', '-s', '--format=%P', later['git_head']).decode().strip() == first['git_head']
221 transport = LegacyDelivery(plan)
222 assert not execute(reconciliation, 'deliver', transport)['ok']
223 assert not any(c[0] in ('push', 'pr') for c in transport.calls)
224 transport.base = first['git_head']
225 assert execute(reconciliation, 'deliver', transport)['ok']
226
227
228 @pytest.mark.parametrize('kind', ['swapped', 'missing', 'extra'])
229 def test_correspondence_refuses_substituted_parents(reconciliation, kind):
230 result = prepared(reconciliation)
231 plan = reconciliation[1]
232 target = Path(plan['target'])
233 parents = mirror.expected_parents(plan)
234 parents = parents[::-1] if kind == 'swapped' else parents[:1] if kind == 'missing' else parents + [
235 mirror.git(target, 'merge-base', *parents).decode().strip()]
236 tree = mirror.git(target, 'rev-parse', 'HEAD^{tree}').decode().strip()
237 message = mirror.git(target, 'cat-file', 'commit', 'HEAD').split(b'\n\n', 1)[1]
238 commit = mirror.git(target, 'commit-tree', tree, *[v for p in parents for v in ('-p', p)], data=message).decode().strip()
239 mirror.git(target, 'update-ref', 'refs/heads/muse-mirror', commit, result['git_head'])
240 refused = execute(reconciliation, 'verify')
241 assert not refused['ok'] and refused['error'] == 'mirror_commit_parent_mismatch'
242
243
244 def test_changed_base_tree_is_not_an_automatic_merge(reconciliation, tmp_path):
245 root, plan = reconciliation
246 repo = tmp_path / 'history.git'
247 empty = mirror.git(repo, 'mktree', data=b'').decode().strip()
248 r = plan['reconciliation']
249 base = mirror.git(repo, 'commit-tree', empty, '-p', r['base_head'], data=b'different base\n').decode().strip()
250 mirror.git(repo, 'update-ref', 'refs/heads/main', base)
251 bundle = Path(r['bundle'])
252 mirror.git(repo, 'bundle', 'create', '--version=2', str(bundle), 'refs/heads/main', 'refs/heads/muse-mirror')
253 r.update(base_head=base, bundle_sha256=digest(bundle.read_bytes()))
254 result = execute(reconciliation, 'reconcile')
255 assert not result['ok'] and result['error'] == 'mirror_history_base_tree_differs'
256 assert not Path(plan['target']).exists()
257
258
259 @pytest.mark.parametrize('path', ['config', 'overseer-mirror.json'])
260 def test_executable_target_metadata_refused(reconciliation, path):
261 prepared(reconciliation)
262 target = Path(reconciliation[1]['target'])
263 (target / path).chmod(0o755)
264 before = inventory(target)
265 result = execute(reconciliation, 'verify')
266 assert not result['ok'] and result['error'] == 'mirror_target_unsafe_path'
267 assert inventory(target) == before
File History 1 commit
sha256:106ef9e12cca7f3be3ffb2a6bd3abfa76f032dce453c9e5010d732a8d86f2e14 Reconcile reviewed credential compatibility 32254cd939885d9… Human patch 8 days ago