test_reconciliation.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 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 |