gabriel / muse public
test_phase2_parent_existence.py python
196 lines 7.6 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 122 days ago
1 """Phase 2 — Parent existence validation in write_commit.
2
3 Invariant: a commit whose parent_commit_id or parent2_commit_id does not
4 exist in the store must be rejected before any bytes are written to disk.
5 A dangling parent pointer is undetectable at read time and silently truncates
6 history traversal — walks stop at the gap instead of at the true root.
7
8 Testing tiers
9 -------------
10 Unit MissingParentError raised for unknown parent / parent2
11 Unit No error for root commits (None parents)
12 Unit No MissingParentError when parent exists in the store
13 Integration commit_exists returns False after a rejected write
14 Data no commit file appears on disk after a MissingParentError
15 Security only MissingParentError (ValueError subclass) is raised — not
16 silent success that writes a corrupt file
17 """
18
19 from __future__ import annotations
20
21 import datetime
22 import pathlib
23
24 import msgpack
25 import pytest
26
27 from muse.core.types import long_id
28 from muse.core.store import (
29 CommitRecord,
30 MissingParentError,
31 commit_exists,
32 commit_path,
33 write_commit,
34 )
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41 _REPO_ID = "repo-phase2-test"
42 _BRANCH = "main"
43 _SNAP_ID = long_id("a" * 64)
44
45
46 def _fake_commit_id(tag: str) -> str:
47 return long_id(tag.encode().hex().ljust(64, "0")[:64])
48
49
50 def _make_commit(
51 commit_id: str,
52 parent_commit_id: str | None = None,
53 parent2_commit_id: str | None = None,
54 ) -> CommitRecord:
55 return CommitRecord(
56 commit_id=commit_id,
57 repo_id=_REPO_ID,
58 branch=_BRANCH,
59 snapshot_id=_SNAP_ID,
60 message="test",
61 committed_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
62 parent_commit_id=parent_commit_id,
63 parent2_commit_id=parent2_commit_id,
64 )
65
66
67 def _plant_commit_file(repo: pathlib.Path, commit_id: str) -> None:
68 """Write a minimal commit file so commit_exists() returns True."""
69 path = commit_path(repo, commit_id)
70 path.parent.mkdir(parents=True, exist_ok=True)
71 path.write_bytes(msgpack.packb({
72 "commit_id": commit_id,
73 "repo_id": _REPO_ID,
74 "branch": _BRANCH,
75 "snapshot_id": _SNAP_ID,
76 "message": "planted",
77 "committed_at": "2026-01-01T00:00:00+00:00",
78 }))
79
80
81 # ---------------------------------------------------------------------------
82 # Unit — root commits (no parent) never raise MissingParentError
83 # ---------------------------------------------------------------------------
84
85 class TestRootCommitNeverRaisesParentError:
86 def test_none_parents_not_rejected_for_parent_reason(self, tmp_path: pathlib.Path) -> None:
87 """Root commit with no parents must never raise MissingParentError."""
88 cid = _fake_commit_id("root")
89 rec = _make_commit(cid, parent_commit_id=None, parent2_commit_id=None)
90 # Hash mismatch (content-address check) is expected — we only verify the
91 # PARENT existence check does not fire.
92 with pytest.raises((ValueError, OSError)) as exc_info:
93 write_commit(tmp_path, rec)
94 assert not isinstance(exc_info.value, MissingParentError), (
95 "Root commit raised MissingParentError — parent check incorrectly fired"
96 )
97
98
99 # ---------------------------------------------------------------------------
100 # Unit — missing parent_commit_id → MissingParentError before disk write
101 # ---------------------------------------------------------------------------
102
103 class TestMissingParentRejected:
104 def test_unknown_parent_raises_missing_parent_error(self, tmp_path: pathlib.Path) -> None:
105 parent_id = _fake_commit_id("ghost-parent")
106 cid = _fake_commit_id("child")
107 rec = _make_commit(cid, parent_commit_id=parent_id)
108
109 with pytest.raises(MissingParentError, match="parent_commit_id"):
110 write_commit(tmp_path, rec)
111
112 def test_unknown_parent_produces_no_file(self, tmp_path: pathlib.Path) -> None:
113 """After a MissingParentError, the commit file must not exist on disk."""
114 parent_id = _fake_commit_id("ghost-parent2")
115 cid = _fake_commit_id("child2")
116 rec = _make_commit(cid, parent_commit_id=parent_id)
117
118 with pytest.raises(MissingParentError):
119 write_commit(tmp_path, rec)
120
121 assert not commit_path(tmp_path, cid).exists(), (
122 "commit file was written even though parent is missing"
123 )
124
125 def test_known_parent_does_not_raise_missing_parent_error(self, tmp_path: pathlib.Path) -> None:
126 """write_commit must NOT raise MissingParentError when parent exists."""
127 parent_id = _fake_commit_id("real-parent")
128 _plant_commit_file(tmp_path, parent_id)
129
130 cid = _fake_commit_id("valid-child")
131 rec = _make_commit(cid, parent_commit_id=parent_id)
132
133 with pytest.raises((ValueError, OSError)) as exc_info:
134 write_commit(tmp_path, rec)
135 assert not isinstance(exc_info.value, MissingParentError), (
136 f"Commit with existing parent raised MissingParentError: {exc_info.value}"
137 )
138
139
140 # ---------------------------------------------------------------------------
141 # Unit — missing parent2_commit_id (merge commits) → MissingParentError
142 # ---------------------------------------------------------------------------
143
144 class TestMissingParent2Rejected:
145 def test_unknown_parent2_raises_missing_parent_error(self, tmp_path: pathlib.Path) -> None:
146 parent_id = _fake_commit_id("p1-exists")
147 _plant_commit_file(tmp_path, parent_id)
148
149 parent2_id = _fake_commit_id("p2-ghost")
150 cid = _fake_commit_id("merge-child")
151 rec = _make_commit(cid, parent_commit_id=parent_id, parent2_commit_id=parent2_id)
152
153 with pytest.raises(MissingParentError, match="parent2_commit_id"):
154 write_commit(tmp_path, rec)
155
156 def test_both_parents_known_does_not_raise_missing_parent_error(self, tmp_path: pathlib.Path) -> None:
157 p1 = _fake_commit_id("p1-real")
158 p2 = _fake_commit_id("p2-real")
159 _plant_commit_file(tmp_path, p1)
160 _plant_commit_file(tmp_path, p2)
161
162 cid = _fake_commit_id("merge-ok")
163 rec = _make_commit(cid, parent_commit_id=p1, parent2_commit_id=p2)
164
165 with pytest.raises((ValueError, OSError)) as exc_info:
166 write_commit(tmp_path, rec)
167 assert not isinstance(exc_info.value, MissingParentError), (
168 f"Merge commit with both parents present raised MissingParentError: {exc_info.value}"
169 )
170
171
172 # ---------------------------------------------------------------------------
173 # Data integrity — commit_exists returns False after a rejected write
174 # ---------------------------------------------------------------------------
175
176 class TestDataIntegrity:
177 def test_commit_id_not_in_store_after_missing_parent_reject(self, tmp_path: pathlib.Path) -> None:
178 ghost = _fake_commit_id("ghost-data")
179 cid = _fake_commit_id("orphaned")
180 rec = _make_commit(cid, parent_commit_id=ghost)
181
182 with pytest.raises(MissingParentError):
183 write_commit(tmp_path, rec)
184
185 assert not commit_exists(tmp_path, cid), (
186 "commit_exists returned True for a commit whose parent was missing"
187 )
188
189 def test_missing_parent_error_is_value_error_subclass(self, tmp_path: pathlib.Path) -> None:
190 """MissingParentError is a ValueError — callers using broad ValueError catches still work."""
191 ghost = _fake_commit_id("ghost-ve")
192 cid = _fake_commit_id("child-ve")
193 rec = _make_commit(cid, parent_commit_id=ghost)
194
195 with pytest.raises(ValueError):
196 write_commit(tmp_path, rec)
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 122 days ago