gabriel / muse public
test_phase2_parent_existence.py python
192 lines 7.7 KB
Raw
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402 Merge branch 'dev' into main Human 20 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 pytest
25
26 from muse.core.types import long_id
27 from muse.core.object_store import object_path
28 from muse.core.commits import (
29 CommitRecord,
30 MissingParentError,
31 commit_exists,
32 write_commit,
33 )
34
35
36 # ---------------------------------------------------------------------------
37 # Helpers
38 # ---------------------------------------------------------------------------
39
40 _REPO_ID = "repo-phase2-test"
41 _BRANCH = "main"
42 _SNAP_ID = long_id("a" * 64)
43
44
45 def _fake_commit_id(tag: str) -> str:
46 return long_id(tag.encode().hex().ljust(64, "0")[:64])
47
48
49 def _make_commit(
50 commit_id: str,
51 parent_commit_id: str | None = None,
52 parent2_commit_id: str | None = None,
53 ) -> CommitRecord:
54 return CommitRecord(
55 commit_id=commit_id,
56 branch=_BRANCH,
57 snapshot_id=_SNAP_ID,
58 message="test",
59 committed_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
60 parent_commit_id=parent_commit_id,
61 parent2_commit_id=parent2_commit_id,
62 )
63
64
65 def _plant_commit_file(repo: pathlib.Path, commit_id: str) -> None:
66 """Write a stub commit directly into the unified object store so commit_exists() returns True.
67
68 Bypasses hash verification intentionally — these are fake IDs used only to
69 satisfy the parent-existence check in write_commit.
70 """
71 path = object_path(repo, commit_id)
72 path.parent.mkdir(parents=True, exist_ok=True)
73 payload = b'{"commit_id":"' + commit_id.encode() + b'"}'
74 path.write_bytes(b"commit " + str(len(payload)).encode() + b"\0" + payload)
75
76
77 # ---------------------------------------------------------------------------
78 # Unit — root commits (no parent) never raise MissingParentError
79 # ---------------------------------------------------------------------------
80
81 class TestRootCommitNeverRaisesParentError:
82 def test_none_parents_not_rejected_for_parent_reason(self, tmp_path: pathlib.Path) -> None:
83 """Root commit with no parents must never raise MissingParentError."""
84 cid = _fake_commit_id("root")
85 rec = _make_commit(cid, parent_commit_id=None, parent2_commit_id=None)
86 # Hash mismatch (content-address check) is expected — we only verify the
87 # PARENT existence check does not fire.
88 with pytest.raises((ValueError, OSError)) as exc_info:
89 write_commit(tmp_path, rec)
90 assert not isinstance(exc_info.value, MissingParentError), (
91 "Root commit raised MissingParentError — parent check incorrectly fired"
92 )
93
94
95 # ---------------------------------------------------------------------------
96 # Unit — missing parent_commit_id → MissingParentError before disk write
97 # ---------------------------------------------------------------------------
98
99 class TestMissingParentRejected:
100 def test_unknown_parent_raises_missing_parent_error(self, tmp_path: pathlib.Path) -> None:
101 parent_id = _fake_commit_id("ghost-parent")
102 cid = _fake_commit_id("child")
103 rec = _make_commit(cid, parent_commit_id=parent_id)
104
105 with pytest.raises(MissingParentError, match="parent_commit_id"):
106 write_commit(tmp_path, rec)
107
108 def test_unknown_parent_produces_no_file(self, tmp_path: pathlib.Path) -> None:
109 """After a MissingParentError, the commit file must not exist on disk."""
110 parent_id = _fake_commit_id("ghost-parent2")
111 cid = _fake_commit_id("child2")
112 rec = _make_commit(cid, parent_commit_id=parent_id)
113
114 with pytest.raises(MissingParentError):
115 write_commit(tmp_path, rec)
116
117 assert not commit_exists(tmp_path, cid), (
118 "commit file was written even though parent is missing"
119 )
120
121 def test_known_parent_does_not_raise_missing_parent_error(self, tmp_path: pathlib.Path) -> None:
122 """write_commit must NOT raise MissingParentError when parent exists."""
123 parent_id = _fake_commit_id("real-parent")
124 _plant_commit_file(tmp_path, parent_id)
125
126 cid = _fake_commit_id("valid-child")
127 rec = _make_commit(cid, parent_commit_id=parent_id)
128
129 with pytest.raises((ValueError, OSError)) as exc_info:
130 write_commit(tmp_path, rec)
131 assert not isinstance(exc_info.value, MissingParentError), (
132 f"Commit with existing parent raised MissingParentError: {exc_info.value}"
133 )
134
135
136 # ---------------------------------------------------------------------------
137 # Unit — missing parent2_commit_id (merge commits) → MissingParentError
138 # ---------------------------------------------------------------------------
139
140 class TestMissingParent2Rejected:
141 def test_unknown_parent2_raises_missing_parent_error(self, tmp_path: pathlib.Path) -> None:
142 parent_id = _fake_commit_id("p1-exists")
143 _plant_commit_file(tmp_path, parent_id)
144
145 parent2_id = _fake_commit_id("p2-ghost")
146 cid = _fake_commit_id("merge-child")
147 rec = _make_commit(cid, parent_commit_id=parent_id, parent2_commit_id=parent2_id)
148
149 with pytest.raises(MissingParentError, match="parent2_commit_id"):
150 write_commit(tmp_path, rec)
151
152 def test_both_parents_known_does_not_raise_missing_parent_error(self, tmp_path: pathlib.Path) -> None:
153 p1 = _fake_commit_id("p1-real")
154 p2 = _fake_commit_id("p2-real")
155 _plant_commit_file(tmp_path, p1)
156 _plant_commit_file(tmp_path, p2)
157
158 cid = _fake_commit_id("merge-ok")
159 rec = _make_commit(cid, parent_commit_id=p1, parent2_commit_id=p2)
160
161 with pytest.raises((ValueError, OSError)) as exc_info:
162 write_commit(tmp_path, rec)
163 assert not isinstance(exc_info.value, MissingParentError), (
164 f"Merge commit with both parents present raised MissingParentError: {exc_info.value}"
165 )
166
167
168 # ---------------------------------------------------------------------------
169 # Data integrity — commit_exists returns False after a rejected write
170 # ---------------------------------------------------------------------------
171
172 class TestDataIntegrity:
173 def test_commit_id_not_in_store_after_missing_parent_reject(self, tmp_path: pathlib.Path) -> None:
174 ghost = _fake_commit_id("ghost-data")
175 cid = _fake_commit_id("orphaned")
176 rec = _make_commit(cid, parent_commit_id=ghost)
177
178 with pytest.raises(MissingParentError):
179 write_commit(tmp_path, rec)
180
181 assert not commit_exists(tmp_path, cid), (
182 "commit_exists returned True for a commit whose parent was missing"
183 )
184
185 def test_missing_parent_error_is_value_error_subclass(self, tmp_path: pathlib.Path) -> None:
186 """MissingParentError is a ValueError — callers using broad ValueError catches still work."""
187 ghost = _fake_commit_id("ghost-ve")
188 cid = _fake_commit_id("child-ve")
189 rec = _make_commit(cid, parent_commit_id=ghost)
190
191 with pytest.raises(ValueError):
192 write_commit(tmp_path, rec)
File History 1 commit
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402 Merge branch 'dev' into main Human 20 days ago