test_write_commit_snapshot_hash_verify.py
python
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6
chore(tests): add docstring to tests/__init__.py so rc14 tr…
Human
26 days ago
| 1 | """ |
| 2 | Tests for two compounding data-integrity bugs in the store write path. |
| 3 | |
| 4 | === BUG 1: write_commit skips hash verification of existing records === |
| 5 | |
| 6 | Root cause (muse/core/store.py::write_commit): |
| 7 | |
| 8 | existing = CommitRecord.from_msgpack(_read_msgpack_dict(path)) |
| 9 | if existing.commit_id != commit.commit_id: |
| 10 | raise OSError(...) # checks stored field — not a recomputed hash |
| 11 | return # ← skips if commit_id field matches |
| 12 | |
| 13 | The idempotency check compares the raw commit_id *field* stored in the msgpack |
| 14 | record, not a recomputed hash of the core fields. A bit flip in snapshot_id, |
| 15 | message, or parent_commit_id leaves commit_id intact, passes the check, and |
| 16 | causes write_commit to silently skip the repair. The commit becomes |
| 17 | permanently unreadable via read_commit (which DOES call _verify_commit_id). |
| 18 | |
| 19 | === BUG 2: write_snapshot skips ALL validation of existing records === |
| 20 | |
| 21 | Root cause (muse/core/store.py::write_snapshot): |
| 22 | |
| 23 | if path.exists(): |
| 24 | logger.debug("⚠️ Snapshot %s already exists — skipped", ...) |
| 25 | return # ← no parsing, no hash check, nothing |
| 26 | |
| 27 | write_snapshot does not parse or verify the existing file at all. Any |
| 28 | corruption in the snapshot manifest — wrong object ID for a file, extra |
| 29 | entries, missing entries — is silently skipped. read_snapshot always calls |
| 30 | _verify_snapshot_id, which recomputes the manifest hash and raises on mismatch. |
| 31 | The snapshot is permanently unreadable with no repair path. |
| 32 | |
| 33 | === Why this matters === |
| 34 | |
| 35 | Every commit references exactly one snapshot_id. If the snapshot at that ID is |
| 36 | corrupt and unrepaired, these operations all fail for that commit: |
| 37 | |
| 38 | muse checkout <branch> — cannot build working tree |
| 39 | muse show <commit> — cannot display file contents |
| 40 | muse diff <commit> — cannot compute diff |
| 41 | muse log --diff — crashes on affected commits |
| 42 | muse push — pushes corrupt snapshot to hub |
| 43 | muse pull (on another machine) — imports corrupt snapshot |
| 44 | |
| 45 | === The fix === |
| 46 | |
| 47 | write_commit: after confirming commit_id field matches, call |
| 48 | _verify_commit_id. Wrap its OSError as ValueError so the existing |
| 49 | `except Exception` branch triggers repair (overwrite) rather than propagation. |
| 50 | |
| 51 | write_snapshot: before returning early, parse and verify the snapshot. |
| 52 | If verification fails, fall through to overwrite. |
| 53 | |
| 54 | === Coverage === |
| 55 | |
| 56 | Unit — write_commit skips clean existing record (no regression) |
| 57 | Unit — write_commit repairs corrupt snapshot_id |
| 58 | Unit — write_commit repairs corrupt message |
| 59 | Unit — write_commit repairs corrupt parent_commit_id |
| 60 | Unit — write_snapshot skips clean existing record (no regression) |
| 61 | Unit — write_snapshot repairs corrupt manifest entry |
| 62 | Unit — write_snapshot repairs corrupt object ID in manifest |
| 63 | Unit — write_snapshot repairs completely empty manifest |
| 64 | Data — read_commit returns good record after write_commit repair |
| 65 | Data — read_snapshot returns good record after write_snapshot repair |
| 66 | Data — commit → snapshot chain readable after both repairs |
| 67 | Data — parent chain (A→B→C) survives corruption of middle commit |
| 68 | Integration — muse checkout path: snapshot must survive write_snapshot repair |
| 69 | Security — corrupt snapshot_id in commit cannot forge a different snapshot |
| 70 | Stress — 50 concurrent writes on corrupt commit all repair |
| 71 | Stress — 50 concurrent writes on corrupt snapshot all repair |
| 72 | Regression — hard integrity violation (commit_id field mismatch) still raises |
| 73 | Regression — write_commit normal path (no pre-existing file) still works |
| 74 | Regression — write_snapshot normal path still works |
| 75 | """ |
| 76 | from __future__ import annotations |
| 77 | |
| 78 | import datetime |
| 79 | import pathlib |
| 80 | import threading |
| 81 | import tempfile |
| 82 | |
| 83 | import msgpack |
| 84 | import pytest |
| 85 | |
| 86 | from muse.core._types import Manifest, MsgpackDict |
| 87 | |
| 88 | # --------------------------------------------------------------------------- |
| 89 | # Helpers |
| 90 | # --------------------------------------------------------------------------- |
| 91 | |
| 92 | |
| 93 | def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 94 | (tmp_path / ".muse" / "commits").mkdir(parents=True, exist_ok=True) |
| 95 | (tmp_path / ".muse" / "snapshots").mkdir(parents=True, exist_ok=True) |
| 96 | return tmp_path |
| 97 | |
| 98 | |
| 99 | def _ts(year: int = 2024) -> str: |
| 100 | return f"{year}-01-01T00:00:00+00:00" |
| 101 | |
| 102 | |
| 103 | def _good_commit( |
| 104 | snapshot_id: str | None = None, |
| 105 | message: str = "test commit", |
| 106 | parent_commit_id: str | None = None, |
| 107 | ts: str | None = None, |
| 108 | ) -> "CommitRecord": |
| 109 | from muse.core.store import CommitRecord |
| 110 | from muse.core.snapshot import compute_commit_id |
| 111 | |
| 112 | snap_id = snapshot_id or "b" * 64 |
| 113 | timestamp = ts or _ts() |
| 114 | parent_ids = [parent_commit_id] if parent_commit_id else [] |
| 115 | commit_id = compute_commit_id(parent_ids, snap_id, message, timestamp) |
| 116 | return CommitRecord( |
| 117 | commit_id=commit_id, |
| 118 | repo_id="test-repo", |
| 119 | branch="main", |
| 120 | snapshot_id=snap_id, |
| 121 | message=message, |
| 122 | committed_at=datetime.datetime.fromisoformat(timestamp), |
| 123 | parent_commit_id=parent_commit_id, |
| 124 | parent2_commit_id=None, |
| 125 | author="gabriel", |
| 126 | metadata={}, |
| 127 | ) |
| 128 | |
| 129 | |
| 130 | def _good_snapshot(manifest: Manifest | None = None) -> "SnapshotRecord": |
| 131 | from muse.core.store import SnapshotRecord |
| 132 | from muse.core.snapshot import compute_snapshot_id |
| 133 | |
| 134 | m = manifest or {"src/main.py": "c" * 64} |
| 135 | snapshot_id = compute_snapshot_id(m, {}) |
| 136 | return SnapshotRecord(snapshot_id=snapshot_id, manifest=m, directories={}) |
| 137 | |
| 138 | |
| 139 | def _write_corrupt_commit(repo: pathlib.Path, good: "CommitRecord", corrupt_field: MsgpackDict) -> None: |
| 140 | """Write a commit file that has good commit_id but corrupt content.""" |
| 141 | base = { |
| 142 | "commit_id": good.commit_id, |
| 143 | "repo_id": "test-repo", |
| 144 | "branch": "main", |
| 145 | "snapshot_id": good.snapshot_id, |
| 146 | "message": good.message, |
| 147 | "committed_at": good.committed_at.isoformat(), |
| 148 | "parent_commit_id": good.parent_commit_id, |
| 149 | "parent2_commit_id": None, |
| 150 | "author": "gabriel", |
| 151 | "metadata": {}, |
| 152 | "format_version": 1, |
| 153 | "reviewed_by": [], |
| 154 | } |
| 155 | base.update(corrupt_field) |
| 156 | path = repo / ".muse" / "commits" / f"{good.commit_id}.msgpack" |
| 157 | path.write_bytes(msgpack.packb(base, use_bin_type=True)) |
| 158 | |
| 159 | |
| 160 | def _write_corrupt_snapshot(repo: pathlib.Path, good: "SnapshotRecord", corrupt_manifest: Manifest) -> None: |
| 161 | """Write a snapshot file with a corrupt manifest.""" |
| 162 | record = { |
| 163 | "snapshot_id": good.snapshot_id, |
| 164 | "manifest": corrupt_manifest, |
| 165 | "directories": {}, |
| 166 | "format_version": 1, |
| 167 | } |
| 168 | path = repo / ".muse" / "snapshots" / f"{good.snapshot_id}.msgpack" |
| 169 | path.write_bytes(msgpack.packb(record, use_bin_type=True)) |
| 170 | |
| 171 | |
| 172 | # ============================================================================= |
| 173 | # 1. UNIT — write_commit must repair corrupt core fields |
| 174 | # ============================================================================= |
| 175 | |
| 176 | |
| 177 | class TestWriteCommitHashVerification: |
| 178 | |
| 179 | def test_idempotent_skip_clean_record(self, tmp_path: pathlib.Path) -> None: |
| 180 | """Regression: write_commit on a clean existing file still returns fast.""" |
| 181 | from muse.core.store import write_commit, read_commit |
| 182 | |
| 183 | repo = _make_repo(tmp_path) |
| 184 | good = _good_commit() |
| 185 | write_commit(repo, good) |
| 186 | write_commit(repo, good) # second call: must not raise, must not change data |
| 187 | result = read_commit(repo, good.commit_id) |
| 188 | assert result is not None |
| 189 | assert result.commit_id == good.commit_id |
| 190 | |
| 191 | def test_repairs_corrupt_snapshot_id(self, tmp_path: pathlib.Path) -> None: |
| 192 | """ |
| 193 | A commit file with a corrupt snapshot_id (wrong hash) must be repaired. |
| 194 | |
| 195 | BUG: write_commit compares existing.commit_id == commit.commit_id |
| 196 | (stored field, unchanged), sees a match, and silently skips overwrite. |
| 197 | The commit then fails read_commit's _verify_commit_id check forever. |
| 198 | |
| 199 | FIX: write_commit must also run _verify_commit_id on the existing record. |
| 200 | """ |
| 201 | from muse.core.store import write_commit, read_commit |
| 202 | |
| 203 | repo = _make_repo(tmp_path) |
| 204 | good = _good_commit(snapshot_id="b" * 64) |
| 205 | _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64}) # wrong hash |
| 206 | |
| 207 | write_commit(repo, good) # must repair |
| 208 | result = read_commit(repo, good.commit_id) |
| 209 | assert result is not None, ( |
| 210 | "BUG: write_commit skipped repair of corrupt snapshot_id.\n" |
| 211 | "The idempotency check only compares the commit_id FIELD — not the " |
| 212 | "recomputed hash. A corrupt snapshot_id leaves commit_id intact, " |
| 213 | "passes the check, and the file is never overwritten.\n" |
| 214 | "FIX: write_commit must call _verify_commit_id on the existing record." |
| 215 | ) |
| 216 | assert result.snapshot_id == "b" * 64 |
| 217 | |
| 218 | def test_repairs_corrupt_message(self, tmp_path: pathlib.Path) -> None: |
| 219 | """A commit with a corrupt message (alters the hash) must be repaired.""" |
| 220 | from muse.core.store import write_commit, read_commit |
| 221 | |
| 222 | repo = _make_repo(tmp_path) |
| 223 | good = _good_commit(message="original message") |
| 224 | _write_corrupt_commit(repo, good, {"message": "CORRUPTED MESSAGE"}) |
| 225 | |
| 226 | write_commit(repo, good) |
| 227 | result = read_commit(repo, good.commit_id) |
| 228 | assert result is not None, "write_commit skipped repair of corrupt message" |
| 229 | assert result.message == "original message" |
| 230 | |
| 231 | def test_repairs_corrupt_parent_commit_id(self, tmp_path: pathlib.Path) -> None: |
| 232 | """A commit with a corrupt parent_commit_id must be repaired.""" |
| 233 | from muse.core.store import write_commit, read_commit |
| 234 | |
| 235 | repo = _make_repo(tmp_path) |
| 236 | good = _good_commit(parent_commit_id=None) |
| 237 | # Inject a fake parent — changes the hash |
| 238 | _write_corrupt_commit(repo, good, {"parent_commit_id": "d" * 64}) |
| 239 | |
| 240 | write_commit(repo, good) |
| 241 | result = read_commit(repo, good.commit_id) |
| 242 | assert result is not None, "write_commit skipped repair of corrupt parent_commit_id" |
| 243 | assert result.parent_commit_id is None |
| 244 | |
| 245 | def test_hard_integrity_violation_still_raises(self, tmp_path: pathlib.Path) -> None: |
| 246 | """ |
| 247 | Regression: when commit_id FIELD in the file mismatches the expected |
| 248 | filename/ID, write_commit must still raise OSError (hard violation). |
| 249 | """ |
| 250 | from muse.core.store import write_commit, read_commit |
| 251 | |
| 252 | repo = _make_repo(tmp_path) |
| 253 | good_a = _good_commit(message="commit A", ts=_ts(2024)) |
| 254 | good_b = _good_commit(message="commit B", ts=_ts(2025)) |
| 255 | |
| 256 | # Write commit B's data under commit A's path |
| 257 | (repo / ".muse" / "commits" / f"{good_a.commit_id}.msgpack").write_bytes( |
| 258 | msgpack.packb({ |
| 259 | "commit_id": good_b.commit_id, # WRONG: different commit_id stored |
| 260 | "repo_id": "test-repo", "branch": "main", |
| 261 | "snapshot_id": good_b.snapshot_id, |
| 262 | "message": good_b.message, |
| 263 | "committed_at": good_b.committed_at.isoformat(), |
| 264 | "parent_commit_id": None, "parent2_commit_id": None, |
| 265 | "author": "gabriel", "metadata": {}, "format_version": 1, "reviewed_by": [], |
| 266 | }, use_bin_type=True) |
| 267 | ) |
| 268 | |
| 269 | with pytest.raises(OSError, match="Store integrity violation"): |
| 270 | write_commit(repo, good_a) |
| 271 | |
| 272 | |
| 273 | # ============================================================================= |
| 274 | # 2. UNIT — write_snapshot must repair corrupt snapshots |
| 275 | # ============================================================================= |
| 276 | |
| 277 | |
| 278 | class TestWriteSnapshotHashVerification: |
| 279 | |
| 280 | def test_idempotent_skip_clean_snapshot(self, tmp_path: pathlib.Path) -> None: |
| 281 | """Regression: write_snapshot on a clean existing file still skips correctly.""" |
| 282 | from muse.core.store import write_snapshot, read_snapshot |
| 283 | |
| 284 | repo = _make_repo(tmp_path) |
| 285 | good = _good_snapshot() |
| 286 | write_snapshot(repo, good) |
| 287 | write_snapshot(repo, good) # second call: idempotent |
| 288 | result = read_snapshot(repo, good.snapshot_id) |
| 289 | assert result is not None |
| 290 | assert result.snapshot_id == good.snapshot_id |
| 291 | |
| 292 | def test_repairs_corrupt_object_id_in_manifest(self, tmp_path: pathlib.Path) -> None: |
| 293 | """ |
| 294 | A snapshot with a wrong object ID for a file must be repaired. |
| 295 | |
| 296 | BUG: write_snapshot sees path.exists() → True → return immediately. |
| 297 | No parsing, no verification. read_snapshot then always returns None. |
| 298 | |
| 299 | FIX: write_snapshot must parse and verify the existing file; overwrite |
| 300 | if corrupt. |
| 301 | """ |
| 302 | from muse.core.store import write_snapshot, read_snapshot |
| 303 | |
| 304 | repo = _make_repo(tmp_path) |
| 305 | good = _good_snapshot({"src/main.py": "c" * 64}) |
| 306 | _write_corrupt_snapshot(repo, good, {"src/main.py": "0" * 64}) # wrong obj id |
| 307 | |
| 308 | write_snapshot(repo, good) |
| 309 | result = read_snapshot(repo, good.snapshot_id) |
| 310 | assert result is not None, ( |
| 311 | "BUG: write_snapshot did not repair corrupt snapshot manifest.\n" |
| 312 | "write_snapshot sees path.exists() → True → returns immediately with\n" |
| 313 | "no parsing or hash verification. Any corruption in the manifest is\n" |
| 314 | "permanent — read_snapshot will always return None for this snapshot.\n" |
| 315 | "FIX: write_snapshot must verify the existing file and overwrite if corrupt." |
| 316 | ) |
| 317 | assert result.manifest == {"src/main.py": "c" * 64} |
| 318 | |
| 319 | def test_repairs_extra_manifest_entry(self, tmp_path: pathlib.Path) -> None: |
| 320 | """An extra file in the manifest (changes the hash) must be repaired.""" |
| 321 | from muse.core.store import write_snapshot, read_snapshot |
| 322 | |
| 323 | repo = _make_repo(tmp_path) |
| 324 | good = _good_snapshot({"src/main.py": "c" * 64}) |
| 325 | _write_corrupt_snapshot(repo, good, { |
| 326 | "src/main.py": "c" * 64, |
| 327 | "INJECTED_FILE.py": "e" * 64, # extra entry → wrong hash |
| 328 | }) |
| 329 | |
| 330 | write_snapshot(repo, good) |
| 331 | result = read_snapshot(repo, good.snapshot_id) |
| 332 | assert result is not None, "write_snapshot did not repair snapshot with extra manifest entry" |
| 333 | assert "INJECTED_FILE.py" not in result.manifest |
| 334 | |
| 335 | def test_repairs_empty_manifest(self, tmp_path: pathlib.Path) -> None: |
| 336 | """A snapshot with an empty manifest (should have files) must be repaired.""" |
| 337 | from muse.core.store import write_snapshot, read_snapshot |
| 338 | |
| 339 | repo = _make_repo(tmp_path) |
| 340 | good = _good_snapshot({"src/main.py": "c" * 64, "src/utils.py": "d" * 64}) |
| 341 | _write_corrupt_snapshot(repo, good, {}) # manifest wiped |
| 342 | |
| 343 | write_snapshot(repo, good) |
| 344 | result = read_snapshot(repo, good.snapshot_id) |
| 345 | assert result is not None, "write_snapshot did not repair empty manifest" |
| 346 | assert len(result.manifest) == 2 |
| 347 | |
| 348 | def test_repairs_missing_file_in_manifest(self, tmp_path: pathlib.Path) -> None: |
| 349 | """A snapshot with a missing file entry must be repaired.""" |
| 350 | from muse.core.store import write_snapshot, read_snapshot |
| 351 | |
| 352 | repo = _make_repo(tmp_path) |
| 353 | good = _good_snapshot({"src/main.py": "c" * 64, "src/utils.py": "d" * 64}) |
| 354 | _write_corrupt_snapshot(repo, good, {"src/main.py": "c" * 64}) # utils.py missing |
| 355 | |
| 356 | write_snapshot(repo, good) |
| 357 | result = read_snapshot(repo, good.snapshot_id) |
| 358 | assert result is not None, "write_snapshot did not repair snapshot with missing manifest entry" |
| 359 | assert "src/utils.py" in result.manifest |
| 360 | |
| 361 | |
| 362 | # ============================================================================= |
| 363 | # 3. DATA INTEGRITY — full commit → snapshot chain |
| 364 | # ============================================================================= |
| 365 | |
| 366 | |
| 367 | class TestCommitSnapshotChain: |
| 368 | |
| 369 | def test_commit_and_snapshot_both_readable_after_repair(self, tmp_path: pathlib.Path) -> None: |
| 370 | """After repairing both commit and snapshot, the full chain is readable.""" |
| 371 | from muse.core.store import write_commit, read_commit, write_snapshot, read_snapshot |
| 372 | |
| 373 | repo = _make_repo(tmp_path) |
| 374 | manifest = {"src/main.py": "c" * 64} |
| 375 | good_snap = _good_snapshot(manifest) |
| 376 | good_commit = _good_commit(snapshot_id=good_snap.snapshot_id) |
| 377 | |
| 378 | # Corrupt both |
| 379 | _write_corrupt_commit(repo, good_commit, {"snapshot_id": "0" * 64}) |
| 380 | _write_corrupt_snapshot(repo, good_snap, {"src/main.py": "0" * 64}) |
| 381 | |
| 382 | # Repair both |
| 383 | write_commit(repo, good_commit) |
| 384 | write_snapshot(repo, good_snap) |
| 385 | |
| 386 | # Full chain must be readable |
| 387 | commit = read_commit(repo, good_commit.commit_id) |
| 388 | assert commit is not None, "commit not readable after repair" |
| 389 | |
| 390 | snap = read_snapshot(repo, commit.snapshot_id) |
| 391 | assert snap is not None, "snapshot not readable after repair" |
| 392 | assert snap.manifest == manifest |
| 393 | |
| 394 | def test_parent_chain_survives_middle_commit_corruption(self, tmp_path: pathlib.Path) -> None: |
| 395 | """A→B→C chain: corrupt B's snapshot_id, repair, verify all three readable.""" |
| 396 | from muse.core.store import write_commit, read_commit |
| 397 | |
| 398 | repo = _make_repo(tmp_path) |
| 399 | commit_a = _good_commit(message="commit A", ts=_ts(2022)) |
| 400 | commit_b = _good_commit(message="commit B", parent_commit_id=commit_a.commit_id, ts=_ts(2023)) |
| 401 | commit_c = _good_commit(message="commit C", parent_commit_id=commit_b.commit_id, ts=_ts(2024)) |
| 402 | |
| 403 | # Write all three |
| 404 | write_commit(repo, commit_a) |
| 405 | write_commit(repo, commit_b) |
| 406 | write_commit(repo, commit_c) |
| 407 | |
| 408 | # Corrupt B |
| 409 | _write_corrupt_commit(repo, commit_b, {"snapshot_id": "0" * 64}) |
| 410 | |
| 411 | # Repair B |
| 412 | write_commit(repo, commit_b) |
| 413 | |
| 414 | # All three must be readable |
| 415 | for c in [commit_a, commit_b, commit_c]: |
| 416 | result = read_commit(repo, c.commit_id) |
| 417 | assert result is not None, f"Commit '{c.message}' not readable after repair" |
| 418 | assert result.commit_id == c.commit_id |
| 419 | |
| 420 | def test_snapshot_repair_does_not_affect_sibling_snapshots(self, tmp_path: pathlib.Path) -> None: |
| 421 | """Repairing one snapshot must not corrupt or affect other snapshots.""" |
| 422 | from muse.core.store import write_snapshot, read_snapshot |
| 423 | |
| 424 | repo = _make_repo(tmp_path) |
| 425 | snap_a = _good_snapshot({"a.py": "a" * 64}) |
| 426 | snap_b = _good_snapshot({"b.py": "b" * 64}) |
| 427 | snap_c = _good_snapshot({"c.py": "c" * 64}) |
| 428 | |
| 429 | write_snapshot(repo, snap_a) |
| 430 | write_snapshot(repo, snap_b) |
| 431 | write_snapshot(repo, snap_c) |
| 432 | |
| 433 | # Corrupt B |
| 434 | _write_corrupt_snapshot(repo, snap_b, {"b.py": "0" * 64}) |
| 435 | |
| 436 | # Repair B |
| 437 | write_snapshot(repo, snap_b) |
| 438 | |
| 439 | # All three readable |
| 440 | for snap in [snap_a, snap_b, snap_c]: |
| 441 | result = read_snapshot(repo, snap.snapshot_id) |
| 442 | assert result is not None, f"Snapshot {snap.snapshot_id[:8]} not readable" |
| 443 | |
| 444 | |
| 445 | # ============================================================================= |
| 446 | # 4. SECURITY — corrupt fields cannot forge content |
| 447 | # ============================================================================= |
| 448 | |
| 449 | |
| 450 | class TestSecurityCorruptFields: |
| 451 | |
| 452 | def test_corrupt_snapshot_id_in_commit_cannot_forge_different_snapshot(self, tmp_path: pathlib.Path) -> None: |
| 453 | """ |
| 454 | An attacker who corrupts a commit's snapshot_id cannot make Muse check |
| 455 | out arbitrary content — the commit hash verification detects the change. |
| 456 | After write_commit repairs the file, the original snapshot_id is restored. |
| 457 | """ |
| 458 | from muse.core.store import write_commit, read_commit |
| 459 | |
| 460 | repo = _make_repo(tmp_path) |
| 461 | good = _good_commit(snapshot_id="b" * 64) |
| 462 | attacker_snapshot = "e" * 64 # attacker wants to point at different content |
| 463 | |
| 464 | _write_corrupt_commit(repo, good, {"snapshot_id": attacker_snapshot}) |
| 465 | |
| 466 | write_commit(repo, good) |
| 467 | result = read_commit(repo, good.commit_id) |
| 468 | assert result is not None |
| 469 | assert result.snapshot_id == "b" * 64, ( |
| 470 | f"Attacker's snapshot_id {attacker_snapshot[:8]!r} survived repair!" |
| 471 | ) |
| 472 | |
| 473 | def test_injected_manifest_entry_is_removed_on_snapshot_repair(self, tmp_path: pathlib.Path) -> None: |
| 474 | """ |
| 475 | An injected file in the manifest (hash mismatch) must be removed on repair. |
| 476 | """ |
| 477 | from muse.core.store import write_snapshot, read_snapshot |
| 478 | |
| 479 | repo = _make_repo(tmp_path) |
| 480 | good = _good_snapshot({"src/main.py": "c" * 64}) |
| 481 | _write_corrupt_snapshot(repo, good, { |
| 482 | "src/main.py": "c" * 64, |
| 483 | "malicious_backdoor.py": "e" * 64, |
| 484 | }) |
| 485 | |
| 486 | write_snapshot(repo, good) |
| 487 | result = read_snapshot(repo, good.snapshot_id) |
| 488 | assert result is not None |
| 489 | assert "malicious_backdoor.py" not in result.manifest |
| 490 | |
| 491 | |
| 492 | # ============================================================================= |
| 493 | # 5. STRESS — concurrent writes all repair correctly |
| 494 | # ============================================================================= |
| 495 | |
| 496 | |
| 497 | class TestStressConcurrentRepair: |
| 498 | |
| 499 | def test_concurrent_write_commit_repairs_corrupt_snapshot_id(self, tmp_path: pathlib.Path) -> None: |
| 500 | """20 concurrent write_commit calls on a corrupt file all result in a readable commit.""" |
| 501 | from muse.core.store import write_commit, read_commit |
| 502 | |
| 503 | repo = _make_repo(tmp_path) |
| 504 | good = _good_commit() |
| 505 | _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64}) |
| 506 | |
| 507 | failures = [] |
| 508 | lock = threading.Lock() |
| 509 | |
| 510 | def worker() -> None: |
| 511 | write_commit(repo, good) |
| 512 | |
| 513 | threads = [threading.Thread(target=worker) for _ in range(20)] |
| 514 | for t in threads: |
| 515 | t.start() |
| 516 | for t in threads: |
| 517 | t.join() |
| 518 | |
| 519 | result = read_commit(repo, good.commit_id) |
| 520 | assert result is not None, ( |
| 521 | "After 20 concurrent write_commit repairs, commit is still unreadable" |
| 522 | ) |
| 523 | assert result.snapshot_id == good.snapshot_id |
| 524 | |
| 525 | def test_concurrent_write_snapshot_repairs_corrupt_manifest(self, tmp_path: pathlib.Path) -> None: |
| 526 | """20 concurrent write_snapshot calls on a corrupt file all result in a readable snapshot.""" |
| 527 | from muse.core.store import write_snapshot, read_snapshot |
| 528 | |
| 529 | repo = _make_repo(tmp_path) |
| 530 | good = _good_snapshot() |
| 531 | _write_corrupt_snapshot(repo, good, {}) # empty manifest |
| 532 | |
| 533 | threads = [threading.Thread(target=lambda: write_snapshot(repo, good)) for _ in range(20)] |
| 534 | for t in threads: |
| 535 | t.start() |
| 536 | for t in threads: |
| 537 | t.join() |
| 538 | |
| 539 | result = read_snapshot(repo, good.snapshot_id) |
| 540 | assert result is not None, ( |
| 541 | "After 20 concurrent write_snapshot repairs, snapshot is still unreadable" |
| 542 | ) |
| 543 | |
| 544 | def test_50_sequential_repairs_all_succeed(self, tmp_path: pathlib.Path) -> None: |
| 545 | """50 different commits each with corrupt snapshot_id, all repaired.""" |
| 546 | from muse.core.store import write_commit, read_commit |
| 547 | |
| 548 | for i in range(50): |
| 549 | repo = _make_repo(tmp_path / str(i)) |
| 550 | good = _good_commit(message=f"commit {i}", ts=f"202{i % 10}-01-01T00:00:00+00:00") |
| 551 | _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64}) |
| 552 | write_commit(repo, good) |
| 553 | result = read_commit(repo, good.commit_id) |
| 554 | assert result is not None, f"commit {i} not repaired" |
| 555 | |
| 556 | |
| 557 | # ============================================================================= |
| 558 | # 6. REGRESSION — normal write paths still work |
| 559 | # ============================================================================= |
| 560 | |
| 561 | |
| 562 | class TestRegression: |
| 563 | |
| 564 | def test_write_commit_new_file_works(self, tmp_path: pathlib.Path) -> None: |
| 565 | from muse.core.store import write_commit, read_commit |
| 566 | |
| 567 | repo = _make_repo(tmp_path) |
| 568 | good = _good_commit() |
| 569 | write_commit(repo, good) |
| 570 | assert read_commit(repo, good.commit_id) is not None |
| 571 | |
| 572 | def test_write_snapshot_new_file_works(self, tmp_path: pathlib.Path) -> None: |
| 573 | from muse.core.store import write_snapshot, read_snapshot |
| 574 | |
| 575 | repo = _make_repo(tmp_path) |
| 576 | good = _good_snapshot() |
| 577 | write_snapshot(repo, good) |
| 578 | assert read_snapshot(repo, good.snapshot_id) is not None |
| 579 | |
| 580 | def test_write_commit_idempotent_on_clean_file(self, tmp_path: pathlib.Path) -> None: |
| 581 | from muse.core.store import write_commit, read_commit |
| 582 | |
| 583 | repo = _make_repo(tmp_path) |
| 584 | good = _good_commit() |
| 585 | write_commit(repo, good) |
| 586 | # Write 10 times — must stay idempotent |
| 587 | for _ in range(10): |
| 588 | write_commit(repo, good) |
| 589 | assert read_commit(repo, good.commit_id) is not None |
| 590 | |
| 591 | def test_write_snapshot_idempotent_on_clean_file(self, tmp_path: pathlib.Path) -> None: |
| 592 | from muse.core.store import write_snapshot, read_snapshot |
| 593 | |
| 594 | repo = _make_repo(tmp_path) |
| 595 | good = _good_snapshot() |
| 596 | write_snapshot(repo, good) |
| 597 | for _ in range(10): |
| 598 | write_snapshot(repo, good) |
| 599 | assert read_snapshot(repo, good.snapshot_id) is not None |
File History
5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6
chore(tests): add docstring to tests/__init__.py so rc14 tr…
Human
26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62
baseline: rc14 re-baseline after rc3 store corruption recovery
Human
patch
26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e
Merge branch 'dev' into main
Human
29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce
Merge branch 'dev' into main
Human
48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa
feat: Muse — version control for the agent era
Human
101 days ago