test_phase2_parent_existence.py
python
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