gabriel / muse public
test_phase7_bundle_atomicity.py python
211 lines 7.6 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 124 days ago
1 """Phase 7 — Bundle atomicity: objects before refs, topological commit order.
2
3 Invariants:
4 1. apply_mpack writes objects → snapshots → commits → (caller advances refs).
5 A crash after objects but before refs leaves reachable but ref-less objects
6 (safe — GC-able). A crash after refs but before objects leaves a ref
7 pointing to a commit whose snapshot has no objects (broken checkout).
8 The safe order is already enforced by apply_mpack: objects first, refs last.
9
10 2. Commits in a bundle may arrive newest-first (BFS order). Phase 2's
11 MissingParentError guard rejects a commit whose parent hasn't been written
12 yet. apply_mpack must retry deferred commits until all parents in the
13 bundle are resolved, or give up if a parent is genuinely absent.
14
15 Testing tiers
16 -------------
17 Unit apply_mpack handles newest-first commit ordering without error
18 Unit apply_mpack retries and eventually writes all commits when parents
19 arrive after children in the bundle
20 Unit apply_mpack logs and skips commits with truly absent parents
21 (not in bundle, not in store)
22 Integration bundle create → unbundle round-trip writes all commits to store
23 Data after unbundle, every commit in the bundle is readable from store
24 """
25
26 from __future__ import annotations
27
28 import datetime
29 import pathlib
30
31 import msgpack
32 import pytest
33
34 from muse.core.pack import apply_mpack, MPackBundle
35 from muse.core.store import (
36 CommitRecord,
37 SnapshotRecord,
38 commit_exists,
39 read_commit,
40 write_commit,
41 write_snapshot,
42 )
43 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
44 from muse.core.types import fake_id, long_id
45
46
47 # ---------------------------------------------------------------------------
48 # Helpers
49 # ---------------------------------------------------------------------------
50
51 _REPO_ID = "repo-phase7-test"
52 _BRANCH = "main"
53
54
55
56 def _make_real_commit(
57 repo: pathlib.Path,
58 tag: str,
59 parent_id: str | None,
60 content: str = "hello",
61 ) -> CommitRecord:
62 """Write a fully content-addressed commit to *repo* and return it."""
63 manifest = {"file.txt": fake_id(f"obj-{content}")}
64 dirs: dict[str, list[str]] = {}
65 snap_id = compute_snapshot_id(manifest, dirs)
66 write_snapshot(repo, SnapshotRecord(snapshot_id=snap_id, manifest=manifest, directories=dirs))
67
68 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
69 commit_id = compute_commit_id(
70 parent_ids=[parent_id] if parent_id else [],
71 snapshot_id=snap_id,
72 message=tag,
73 committed_at_iso=committed_at.isoformat(),
74 author="gabriel",
75 )
76 rec = CommitRecord(
77 repo_id=_REPO_ID,
78 commit_id=commit_id,
79 branch=_BRANCH,
80 snapshot_id=snap_id,
81 message=tag,
82 committed_at=committed_at,
83 parent_commit_id=parent_id,
84 author="gabriel",
85 )
86 write_commit(repo, rec)
87 return rec
88
89
90 # ---------------------------------------------------------------------------
91 # Unit — topological retry: newest-first ordering succeeds
92 # ---------------------------------------------------------------------------
93
94 class TestTopologicalRetry:
95 def test_newest_first_ordering_writes_all_commits(self, tmp_path: pathlib.Path) -> None:
96 """apply_mpack must succeed even when commits arrive newest-first."""
97 root_commit = _make_real_commit(tmp_path, "root", None)
98 child_commit = _make_real_commit(tmp_path, "child", root_commit.commit_id)
99 grandchild = _make_real_commit(tmp_path, "grandchild", child_commit.commit_id)
100
101 # Fresh repo — no commits yet
102 dest = tmp_path / "dest"
103 dest.mkdir()
104
105 # Bundle commits in newest-first order (BFS from tip)
106 bundle: MPackBundle = {
107 "objects": [],
108 "snapshots": [
109 grandchild.__class__.from_msgpack # not used
110 ] if False else [],
111 "commits": [
112 grandchild.to_dict(), # newest — parent not written yet
113 child_commit.to_dict(), # middle
114 root_commit.to_dict(), # oldest — no parent
115 ],
116 }
117
118 result = apply_mpack(dest, bundle)
119
120 assert result["commits_written"] == 3, (
121 f"Expected 3 commits written, got {result['commits_written']}. "
122 "apply_mpack may not be retrying MissingParentError commits."
123 )
124 assert commit_exists(dest, root_commit.commit_id)
125 assert commit_exists(dest, child_commit.commit_id)
126 assert commit_exists(dest, grandchild.commit_id)
127
128 def test_correct_order_still_works(self, tmp_path: pathlib.Path) -> None:
129 """Oldest-first ordering (already correct) must still succeed."""
130 root_commit = _make_real_commit(tmp_path, "root2", None)
131 child_commit = _make_real_commit(tmp_path, "child2", root_commit.commit_id)
132
133 dest = tmp_path / "dest2"
134 dest.mkdir()
135
136 bundle: MPackBundle = {
137 "objects": [],
138 "snapshots": [],
139 "commits": [
140 root_commit.to_dict(),
141 child_commit.to_dict(),
142 ],
143 }
144
145 result = apply_mpack(dest, bundle)
146 assert result["commits_written"] == 2
147
148 def test_absent_parent_skipped_gracefully(self, tmp_path: pathlib.Path) -> None:
149 """A commit whose parent is not in the bundle or store must be skipped,
150 not crash apply_mpack."""
151 root_commit = _make_real_commit(tmp_path, "root3", None)
152 child_commit = _make_real_commit(tmp_path, "child3", root_commit.commit_id)
153
154 dest = tmp_path / "dest3"
155 dest.mkdir()
156
157 # Bundle only has the child — root is absent
158 bundle: MPackBundle = {
159 "objects": [],
160 "snapshots": [],
161 "commits": [
162 child_commit.to_dict(), # parent (root) not in bundle or dest
163 ],
164 }
165
166 # Should not raise — should log and skip
167 result = apply_mpack(dest, bundle)
168
169 assert result["commits_written"] == 0, (
170 "commit with missing parent should have been skipped"
171 )
172 assert not commit_exists(dest, child_commit.commit_id), (
173 "commit with missing parent was written despite dangling parent"
174 )
175
176
177 # ---------------------------------------------------------------------------
178 # Data — objects present after unbundle before refs are advanced
179 # ---------------------------------------------------------------------------
180
181 class TestObjectsBeforeRefs:
182 def test_apply_mpack_writes_commits_before_caller_advances_refs(
183 self, tmp_path: pathlib.Path
184 ) -> None:
185 """apply_mpack (object writes) completes before write_branch_ref is called.
186
187 This is verified structurally: apply_mpack returns successfully before
188 the caller's write_branch_ref call. If objects were not yet written
189 at the time refs were advanced, a checkout immediately after would fail.
190 """
191 root_commit = _make_real_commit(tmp_path, "root4", None)
192
193 dest = tmp_path / "dest4"
194 dest.mkdir()
195
196 bundle: MPackBundle = {
197 "objects": [],
198 "snapshots": [],
199 "commits": [root_commit.to_dict()],
200 }
201
202 # apply_mpack returns — at this point commits are durable
203 result = apply_mpack(dest, bundle)
204 assert result["commits_written"] == 1
205
206 # read_commit must work immediately — no ref advancement needed
207 read_back = read_commit(dest, root_commit.commit_id)
208 assert read_back is not None, (
209 "commit not readable after apply_mpack — write did not complete"
210 )
211 assert read_back.commit_id == root_commit.commit_id
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 124 days ago