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