gabriel / muse public
test_integrity_I5_commit_integrity.py python
720 lines 30.0 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Phase 1.5 — Commit record integrity on re-read.
2
3 Tests cover:
4 - write_commit idempotency: silent drop of duplicate ID
5 - write_commit collision detection: existing file is corrupt → CRITICAL + overwrite
6 - write_commit integrity violation: existing record has mismatched commit_id
7 - read_commit: WARNING→CRITICAL upgrade for corrupt files
8 - read_commit_result: discriminated union (ok / not_found / corrupt)
9 - read_snapshot / read_snapshot_result: same guarantees
10 - get_all_commits / get_all_tags: CRITICAL on corrupt (previously silent)
11 - list_releases: CRITICAL on corrupt (previously silent)
12 - verify-pack integration after write_commit
13 - Concurrent write with same ID: first writer always wins (idempotency at scale)
14 - Regression: corrupt file must log CRITICAL (level 50), never WARNING (level 30)
15 """
16
17 from __future__ import annotations
18
19 import datetime
20 import hashlib
21 import json
22 import logging
23 import pathlib
24 import threading
25 import uuid
26
27 import msgpack
28 import pytest
29
30 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
31
32 from muse.core._types import Manifest
33 from muse.core.store import (
34 CommitReadCorrupt,
35 CommitReadNotFound,
36 CommitReadOk,
37 CommitRecord,
38 ReleaseRecord,
39 SemVerTag,
40 SnapshotReadCorrupt,
41 SnapshotReadNotFound,
42 SnapshotReadOk,
43 SnapshotRecord,
44 TagRecord,
45 commit_read_is_corrupt,
46 commit_read_is_not_found,
47 commit_read_is_ok,
48 get_all_commits,
49 get_all_tags,
50 list_releases,
51 read_commit,
52 read_commit_result,
53 read_snapshot,
54 read_snapshot_result,
55 snapshot_read_is_corrupt,
56 snapshot_read_is_ok,
57 write_commit,
58 write_release,
59 write_snapshot,
60 write_tag,
61 )
62
63 # ---------------------------------------------------------------------------
64 # Helpers
65 # ---------------------------------------------------------------------------
66
67 def _sha(text: str) -> str:
68 return hashlib.sha256(text.encode()).hexdigest()
69
70
71 def _make_commit(
72 root: pathlib.Path,
73 message: str = "msg",
74 branch: str = "main",
75 parent: str | None = None,
76 write: bool = True,
77 ) -> CommitRecord:
78 """Create a CommitRecord with a content-addressed commit_id.
79
80 Uses ``compute_commit_id`` so every record passes ``_verify_commit_id``
81 on read-back. ``write=False`` builds the record without persisting it —
82 useful for testing concurrent or idempotent write scenarios.
83 """
84 committed_at = datetime.datetime(2026, 3, 1, tzinfo=datetime.timezone.utc)
85 snap_id = compute_snapshot_id({})
86 parent_ids = [parent] if parent else []
87 cid = compute_commit_id(parent_ids, snap_id, message, committed_at.isoformat())
88 c = CommitRecord(
89 commit_id=cid,
90 repo_id="test-repo",
91 branch=branch,
92 snapshot_id=snap_id,
93 message=message,
94 committed_at=committed_at,
95 author="tester",
96 parent_commit_id=parent,
97 parent2_commit_id=None,
98 )
99 if write:
100 write_commit(root, c)
101 return c
102
103
104 def _make_snapshot(
105 root: pathlib.Path, manifest: Manifest | None = None
106 ) -> SnapshotRecord:
107 """Create a SnapshotRecord with a content-addressed snapshot_id.
108
109 Pass distinct ``manifest`` dicts to get distinct snapshot_ids — e.g.
110 ``{"file-A.py": "a" * 64}`` vs ``{"file-B.py": "b" * 64}``.
111 """
112 m = manifest or {}
113 sid = compute_snapshot_id(m)
114 s = SnapshotRecord(
115 snapshot_id=sid,
116 manifest=m,
117 created_at=datetime.datetime(2026, 3, 1, tzinfo=datetime.timezone.utc),
118 )
119 write_snapshot(root, s)
120 return s
121
122
123 def _make_tag(root: pathlib.Path, tag_name: str) -> TagRecord:
124 t = TagRecord(
125 tag_id=_sha(tag_name),
126 repo_id="test-repo",
127 commit_id="00" * 32,
128 tag=tag_name,
129 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
130 )
131 write_tag(root, t)
132 return t
133
134
135 def _make_release(root: pathlib.Path, tag: str, semver: SemVerTag) -> ReleaseRecord:
136 r = ReleaseRecord(
137 release_id=str(uuid.uuid4()),
138 repo_id="test-repo",
139 tag=tag,
140 semver=semver,
141 channel="stable",
142 commit_id="00" * 32,
143 snapshot_id=_sha(tag),
144 title=tag,
145 body="",
146 changelog=[],
147 )
148 write_release(root, r)
149 return r
150
151
152 def _commit_path(root: pathlib.Path, commit_id: str) -> pathlib.Path:
153 return root / ".muse" / "commits" / f"{commit_id}.msgpack"
154
155
156 def _snap_path(root: pathlib.Path, snap_id: str) -> pathlib.Path:
157 return root / ".muse" / "snapshots" / f"{snap_id}.msgpack"
158
159
160 def _tag_path(root: pathlib.Path, tag_id: str) -> pathlib.Path:
161 return root / ".muse" / "tags" / "test-repo" / f"{tag_id}.msgpack"
162
163
164 # ---------------------------------------------------------------------------
165 # Fixtures
166 # ---------------------------------------------------------------------------
167
168 @pytest.fixture()
169 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
170 muse = tmp_path / ".muse"
171 (muse / "commits").mkdir(parents=True)
172 (muse / "snapshots").mkdir(parents=True)
173 (muse / "refs" / "heads").mkdir(parents=True)
174 (muse / "tags" / "test-repo").mkdir(parents=True)
175 (muse / "releases" / "test-repo").mkdir(parents=True)
176 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
177 (muse / "HEAD").write_text("ref: refs/heads/main\n")
178 (muse / "refs" / "heads" / "main").write_text("")
179 return tmp_path
180
181
182 # ===========================================================================
183 # 1. write_commit — idempotency
184 # ===========================================================================
185
186 class TestWriteCommitIdempotency:
187 def test_first_writer_wins(self, repo: pathlib.Path) -> None:
188 """A record with wrong incoming hash is rejected before it can overwrite anything.
189
190 The old "first writer wins via silent drop" path is superseded by incoming
191 hash verification: a record whose commit_id doesn't match its content hash
192 raises ValueError immediately — the good file on disk is never touched.
193 """
194 c1 = _make_commit(repo, message="first-wins")
195 # Construct a record with the same commit_id but different content —
196 # the hash won't match, so write_commit must raise before touching disk.
197 c2 = CommitRecord(
198 commit_id=c1.commit_id,
199 repo_id="test-repo",
200 branch="main",
201 snapshot_id=c1.snapshot_id,
202 message="second-attempt",
203 committed_at=c1.committed_at,
204 author="tester",
205 parent_commit_id=None,
206 parent2_commit_id=None,
207 )
208 with pytest.raises(ValueError):
209 write_commit(repo, c2)
210 loaded = read_commit(repo, c1.commit_id)
211 assert loaded is not None
212 assert loaded.message == "first-wins", "bad incoming record must not overwrite good file"
213
214 def test_exact_duplicate_emits_no_critical(
215 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
216 ) -> None:
217 """Writing the exact same record twice must not log CRITICAL."""
218 c = _make_commit(repo, message="exact-dup-no-critical")
219 with caplog.at_level(logging.DEBUG, logger="muse.core.store"):
220 write_commit(repo, c)
221 assert not any(r.levelno >= logging.CRITICAL for r in caplog.records)
222
223 def test_idempotent_round_trip_preserves_all_fields(self, repo: pathlib.Path) -> None:
224 c = _make_commit(repo, message="preserve-me", branch="feat/x")
225 write_commit(repo, c) # second write — must be completely harmless
226 loaded = read_commit(repo, c.commit_id)
227 assert loaded is not None
228 assert loaded.message == "preserve-me"
229 assert loaded.branch == "feat/x"
230
231
232 # ===========================================================================
233 # 2. write_commit — corrupt existing file → CRITICAL + overwrite
234 # ===========================================================================
235
236 class TestWriteCommitCorruptExistingFile:
237 def test_corrupt_existing_is_overwritten(
238 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
239 ) -> None:
240 """Corrupt existing commit file is replaced with the incoming good record."""
241 c = _make_commit(repo, message="original-overwrite")
242 _commit_path(repo, c.commit_id).write_bytes(b"\xff\xfe\x00bad-data\x99")
243 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
244 write_commit(repo, c)
245 crits = [r for r in caplog.records if r.levelno >= logging.CRITICAL]
246 assert crits, "Must log CRITICAL when overwriting corrupt file"
247 loaded = read_commit(repo, c.commit_id)
248 assert loaded is not None
249 assert loaded.message == "original-overwrite"
250
251 def test_empty_existing_is_overwritten(
252 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
253 ) -> None:
254 """Zero-byte commit file (crash during write) is replaced with good record."""
255 c = _make_commit(repo, message="after-crash-overwrite")
256 _commit_path(repo, c.commit_id).write_bytes(b"")
257 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
258 write_commit(repo, c)
259 loaded = read_commit(repo, c.commit_id)
260 assert loaded is not None
261 assert loaded.message == "after-crash-overwrite"
262
263 def test_truncated_msgpack_is_overwritten(
264 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
265 ) -> None:
266 """Partially-written (truncated) msgpack file is replaced."""
267 c = _make_commit(repo, message="after-truncation-overwrite")
268 path = _commit_path(repo, c.commit_id)
269 good_bytes = path.read_bytes()
270 path.write_bytes(good_bytes[: len(good_bytes) // 2])
271 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
272 write_commit(repo, c)
273 loaded = read_commit(repo, c.commit_id)
274 assert loaded is not None
275 assert loaded.message == "after-truncation-overwrite"
276
277
278 # ===========================================================================
279 # 3. write_commit — store integrity violation
280 # ===========================================================================
281
282 class TestWriteCommitIntegrityViolation:
283 def test_commit_id_mismatch_raises_os_error(self, repo: pathlib.Path) -> None:
284 """Stored record's commit_id does not match filename → OSError."""
285 # Write a legitimate commit so the file exists on disk.
286 c_legit = _make_commit(repo, message="legitimate-mismatch")
287 # Build a different commit (different content → different commit_id).
288 c_impostor = _make_commit(repo, message="impostor-mismatch")
289 # Overwrite the legitimate file with the impostor's msgpack bytes —
290 # the file path says c_legit.commit_id but the bytes claim c_impostor.commit_id.
291 impostor_bytes = msgpack.packb(c_impostor.to_dict(), use_bin_type=True)
292 _commit_path(repo, c_legit.commit_id).write_bytes(impostor_bytes)
293 # Now re-write the original c_legit — it passes incoming hash verification
294 # (its commit_id matches its content), but the file on disk now contains
295 # the impostor's bytes. write_commit must detect the mismatch and raise OSError.
296 with pytest.raises(OSError, match="Store integrity violation"):
297 write_commit(repo, c_legit)
298
299
300 # ===========================================================================
301 # 4. read_commit — CRITICAL log for corrupt
302 # ===========================================================================
303
304 class TestReadCommitCriticalLogging:
305 def test_corrupt_file_logs_critical(
306 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
307 ) -> None:
308 c = _make_commit(repo, message="garbage-payload")
309 _commit_path(repo, c.commit_id).write_bytes(b"\x00\x01garbage\xff")
310 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
311 result = read_commit(repo, c.commit_id)
312 assert result is None
313 crits = [r for r in caplog.records if r.levelno >= logging.CRITICAL]
314 assert crits, "Must log CRITICAL for corrupt commit file"
315 assert any("Corrupt" in r.message for r in crits)
316
317 def test_missing_file_returns_none_no_log(
318 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
319 ) -> None:
320 with caplog.at_level(logging.DEBUG, logger="muse.core.store"):
321 result = read_commit(repo, "no" * 32)
322 assert result is None
323 assert not any(r.levelno >= logging.WARNING for r in caplog.records)
324
325 def test_valid_file_returns_record_no_critical(
326 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
327 ) -> None:
328 c = _make_commit(repo, message="clean-read")
329 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
330 result = read_commit(repo, c.commit_id)
331 assert result is not None
332 assert result.message == "clean-read"
333 assert not any(r.levelno >= logging.CRITICAL for r in caplog.records)
334
335 def test_corrupt_log_references_filename(
336 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
337 ) -> None:
338 c = _make_commit(repo, message="not-msgpack-content")
339 _commit_path(repo, c.commit_id).write_bytes(b"not-msgpack")
340 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
341 read_commit(repo, c.commit_id)
342 messages = " ".join(r.message + str(r.args) for r in caplog.records)
343 assert c.commit_id[:8] in messages or c.commit_id in messages
344
345
346 # ===========================================================================
347 # 5. read_commit_result — discriminated union
348 # ===========================================================================
349
350 class TestReadCommitResult:
351 def test_ok_status_on_valid_record(self, repo: pathlib.Path) -> None:
352 c = _make_commit(repo, message="typed-ok")
353 r = read_commit_result(repo, c.commit_id)
354 assert commit_read_is_ok(r)
355 assert isinstance(r["commit"], CommitRecord)
356 assert r["commit"].message == "typed-ok"
357
358 def test_not_found_status_when_missing(self, repo: pathlib.Path) -> None:
359 r = read_commit_result(repo, "ff" * 32)
360 assert commit_read_is_not_found(r)
361
362 def test_corrupt_status_on_bad_bytes(
363 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
364 ) -> None:
365 c = _make_commit(repo, message="corrupt-bytes")
366 _commit_path(repo, c.commit_id).write_bytes(b"\xff\x00garbage")
367 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
368 r = read_commit_result(repo, c.commit_id)
369 assert commit_read_is_corrupt(r)
370 assert r["path"] != ""
371 assert r["error"] != ""
372 crits = [rec for rec in caplog.records if rec.levelno >= logging.CRITICAL]
373 assert crits
374
375 def test_corrupt_result_path_contains_commit_id(self, repo: pathlib.Path) -> None:
376 c = _make_commit(repo, message="path-in-corrupt")
377 _commit_path(repo, c.commit_id).write_bytes(b"")
378 r = read_commit_result(repo, c.commit_id)
379 assert commit_read_is_corrupt(r)
380 assert c.commit_id in r["path"]
381
382 def test_ok_result_roundtrips_all_metadata(self, repo: pathlib.Path) -> None:
383 # Build with a real content-addressed ID so _verify_commit_id passes.
384 snap_id = _sha("snap-meta-roundtrip")
385 committed_at = datetime.datetime(2026, 3, 15, tzinfo=datetime.timezone.utc)
386 cid = compute_commit_id([], snap_id, "full metadata", committed_at.isoformat())
387 c = CommitRecord(
388 commit_id=cid,
389 repo_id="test-repo",
390 branch="dev",
391 snapshot_id=snap_id,
392 message="full metadata",
393 committed_at=committed_at,
394 author="alice",
395 parent_commit_id=None,
396 parent2_commit_id=None,
397 metadata={"key": "val"},
398 )
399 write_commit(repo, c)
400 r = read_commit_result(repo, cid)
401 assert commit_read_is_ok(r)
402 assert r["commit"].branch == "dev"
403 assert r["commit"].author == "alice"
404 assert r["commit"].metadata == {"key": "val"}
405
406 def test_status_field_is_string(self, repo: pathlib.Path) -> None:
407 """Status values are plain strings — easy for agents to pattern-match."""
408 c = _make_commit(repo, message="status-str-check")
409 r = read_commit_result(repo, c.commit_id)
410 assert isinstance(r["status"], str)
411
412 def test_not_found_has_only_status_key(self, repo: pathlib.Path) -> None:
413 r = read_commit_result(repo, "90" * 32)
414 assert set(r.keys()) == {"status"}
415
416 def test_three_outcomes_are_mutually_exclusive(self, repo: pathlib.Path) -> None:
417 """Confirm all three outcome strings are distinct and unambiguous."""
418 c_ok = _make_commit(repo, message="outcome-ok")
419 c_corrupt = _make_commit(repo, message="outcome-corrupt")
420 _commit_path(repo, c_corrupt.commit_id).write_bytes(b"bad")
421 statuses = {
422 read_commit_result(repo, c_ok.commit_id)["status"],
423 read_commit_result(repo, "cc" * 32)["status"],
424 read_commit_result(repo, c_corrupt.commit_id)["status"],
425 }
426 assert statuses == {"ok", "not_found", "corrupt"}
427
428
429 # ===========================================================================
430 # 6. read_snapshot / read_snapshot_result
431 # ===========================================================================
432
433 class TestReadSnapshotIntegrity:
434 def test_corrupt_snapshot_logs_critical(
435 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
436 ) -> None:
437 s = _make_snapshot(repo, manifest={"snap-critical.py": "a" * 64})
438 _snap_path(repo, s.snapshot_id).write_bytes(b"\xde\xad\xbe\xef")
439 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
440 result = read_snapshot(repo, s.snapshot_id)
441 assert result is None
442 assert any(r.levelno >= logging.CRITICAL for r in caplog.records)
443
444 def test_snapshot_result_ok(self, repo: pathlib.Path) -> None:
445 s = _make_snapshot(repo, manifest={"snap-ok.py": "b" * 64})
446 r = read_snapshot_result(repo, s.snapshot_id)
447 assert snapshot_read_is_ok(r)
448 assert isinstance(r["snapshot"], SnapshotRecord)
449
450 def test_snapshot_result_not_found(self, repo: pathlib.Path) -> None:
451 r = read_snapshot_result(repo, _sha("no-snap"))
452 assert r["status"] == "not_found"
453 assert set(r.keys()) == {"status"}
454
455 def test_snapshot_result_corrupt(
456 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
457 ) -> None:
458 s = _make_snapshot(repo, manifest={"snap-corrupt.py": "c" * 64})
459 _snap_path(repo, s.snapshot_id).write_bytes(b"garbage-bytes\x00")
460 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
461 r = read_snapshot_result(repo, s.snapshot_id)
462 assert snapshot_read_is_corrupt(r)
463 assert r["path"] != ""
464 assert r["error"] != ""
465
466 def test_missing_snapshot_no_log(
467 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
468 ) -> None:
469 with caplog.at_level(logging.DEBUG, logger="muse.core.store"):
470 result = read_snapshot(repo, _sha("missing"))
471 assert result is None
472 assert not any(r.levelno >= logging.WARNING for r in caplog.records)
473
474
475 # ===========================================================================
476 # 7. get_all_commits — CRITICAL on corrupt (previously silent)
477 # ===========================================================================
478
479 class TestGetAllCommitsCorruptLogging:
480 def test_one_corrupt_skipped_with_critical(
481 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
482 ) -> None:
483 """Corrupt commit is skipped; good commits returned; CRITICAL emitted."""
484 c_good = _make_commit(repo, message="good-survives")
485 c_bad = _make_commit(repo, message="will-corrupt")
486 _commit_path(repo, c_bad.commit_id).write_bytes(b"\xff\x00")
487 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
488 commits = get_all_commits(repo)
489 ids = {c.commit_id for c in commits}
490 assert c_good.commit_id in ids, "good commit must still appear"
491 assert c_bad.commit_id not in ids, "corrupt commit must be excluded"
492 assert any(r.levelno >= logging.CRITICAL for r in caplog.records)
493
494 def test_all_corrupt_returns_empty_with_critical(
495 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
496 ) -> None:
497 written = [_make_commit(repo, message=f"c{i}") for i in range(3)]
498 for c in written:
499 _commit_path(repo, c.commit_id).write_bytes(b"bad")
500 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
501 commits = get_all_commits(repo)
502 assert commits == []
503 crits = [r for r in caplog.records if r.levelno >= logging.CRITICAL]
504 assert len(crits) == 3
505
506 def test_empty_store_returns_empty_no_log(
507 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
508 ) -> None:
509 with caplog.at_level(logging.DEBUG, logger="muse.core.store"):
510 commits = get_all_commits(repo)
511 assert commits == []
512 assert not any(r.levelno >= logging.WARNING for r in caplog.records)
513
514 def test_mixed_good_and_corrupt_correct_count(self, repo: pathlib.Path) -> None:
515 good = [_make_commit(repo, message=f"g{i}") for i in range(5)]
516 bad = [_make_commit(repo, message=f"b{i}") for i in range(3)]
517 for c in bad:
518 _commit_path(repo, c.commit_id).write_bytes(b"corrupt")
519 commits = get_all_commits(repo)
520 assert len(commits) == len(good)
521
522
523 # ===========================================================================
524 # 8. get_all_tags — CRITICAL on corrupt (previously silent)
525 # ===========================================================================
526
527 class TestGetAllTagsCorruptLogging:
528 def test_corrupt_tag_skipped_with_critical(
529 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
530 ) -> None:
531 t1 = _make_tag(repo, "v1.0.0")
532 t2 = _make_tag(repo, "v2.0.0")
533 _tag_path(repo, t2.tag_id).write_bytes(b"\x00bad")
534 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
535 tags = get_all_tags(repo, "test-repo")
536 tag_values = {t.tag for t in tags}
537 assert "v1.0.0" in tag_values
538 assert "v2.0.0" not in tag_values
539 assert any(r.levelno >= logging.CRITICAL for r in caplog.records)
540
541 def test_good_tags_all_returned(self, repo: pathlib.Path) -> None:
542 _make_tag(repo, "v0.1")
543 _make_tag(repo, "v0.2")
544 tags = get_all_tags(repo, "test-repo")
545 assert len(tags) == 2
546
547 def test_all_corrupt_tags_returns_empty_with_critical(
548 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
549 ) -> None:
550 for name in ("v1", "v2", "v3"):
551 t = _make_tag(repo, name)
552 _tag_path(repo, t.tag_id).write_bytes(b"bad")
553 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
554 tags = get_all_tags(repo, "test-repo")
555 assert tags == []
556 crits = [r for r in caplog.records if r.levelno >= logging.CRITICAL]
557 assert len(crits) == 3
558
559
560 # ===========================================================================
561 # 9. list_releases — CRITICAL on corrupt (previously silent)
562 # ===========================================================================
563
564 class TestListReleasesCorruptLogging:
565 def test_corrupt_release_skipped_with_critical(
566 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
567 ) -> None:
568 good = _make_release(
569 repo, "v1.0.0", SemVerTag(major=1, minor=0, patch=0, pre="", build="")
570 )
571 bad = _make_release(
572 repo, "v2.0.0", SemVerTag(major=2, minor=0, patch=0, pre="", build="")
573 )
574 release_path = repo / ".muse" / "releases" / "test-repo" / f"{bad.release_id}.msgpack"
575 release_path.write_bytes(b"\xff\x00garbage")
576 with caplog.at_level(logging.CRITICAL, logger="muse.core.store"):
577 releases = list_releases(repo, "test-repo")
578 ids = {r.release_id for r in releases}
579 assert good.release_id in ids
580 assert bad.release_id not in ids
581 assert any(r.levelno >= logging.CRITICAL for r in caplog.records)
582
583 def test_all_releases_good_returns_all(self, repo: pathlib.Path) -> None:
584 _make_release(repo, "v1.0.0", SemVerTag(major=1, minor=0, patch=0, pre="", build=""))
585 _make_release(repo, "v1.1.0", SemVerTag(major=1, minor=1, patch=0, pre="", build=""))
586 releases = list_releases(repo, "test-repo")
587 assert len(releases) == 2
588
589
590 # ===========================================================================
591 # 10. verify-pack integration after write_commit
592 # ===========================================================================
593
594 class TestVerifyPackAfterWriteCommit:
595 def test_plumbing_read_commit_roundtrip(self, repo: pathlib.Path) -> None:
596 """``muse plumbing read-commit`` must succeed for every written commit."""
597 from tests.cli_test_helper import CliRunner
598
599 c = _make_commit(repo, message="plumbing-check")
600
601 runner = CliRunner()
602 result = runner.invoke(
603 None,
604 ["read-commit", c.commit_id, "--json"],
605 env={"MUSE_REPO_ROOT": str(repo)},
606 )
607 assert result.exit_code == 0
608 import json as _json
609 data = _json.loads(result.output)
610 assert data["commit_id"] == c.commit_id
611 assert data["message"] == "plumbing-check"
612
613
614 # ===========================================================================
615 # 11. Concurrent idempotency — 50 threads race to write the same commit
616 # ===========================================================================
617
618 class TestConcurrentIdempotentWrite:
619 def test_50_threads_same_commit_id_first_wins(self, repo: pathlib.Path) -> None:
620 """50 threads writing the EXACT same commit — idempotent, exactly one file written."""
621 # In a content-addressed system, identical content → identical commit_id.
622 c = _make_commit(repo, message="concurrent-idempotent", write=False)
623 errors: list[Exception] = []
624
625 def write_one() -> None:
626 try:
627 write_commit(repo, c)
628 except Exception as exc:
629 errors.append(exc)
630
631 threads = [threading.Thread(target=write_one) for _ in range(50)]
632 for t in threads:
633 t.start()
634 for t in threads:
635 t.join()
636
637 assert not errors, f"Unexpected errors in same-ID concurrent writes: {errors[:3]}"
638
639 loaded = read_commit(repo, c.commit_id)
640 assert loaded is not None
641 assert loaded.commit_id == c.commit_id
642 assert loaded.message == "concurrent-idempotent"
643
644 def test_50_threads_distinct_ids_all_survive(self, repo: pathlib.Path) -> None:
645 """50 threads writing distinct commit IDs must all persist without errors."""
646 errors: list[Exception] = []
647
648 def write_unique(i: int) -> None:
649 # _make_commit uses compute_commit_id so the hash always matches content.
650 c = _make_commit(repo, message=f"unique {i}", write=False)
651 try:
652 write_commit(repo, c)
653 except Exception as exc:
654 errors.append(exc)
655
656 threads = [threading.Thread(target=write_unique, args=(i,)) for i in range(50)]
657 for t in threads:
658 t.start()
659 for t in threads:
660 t.join()
661
662 assert not errors, f"Unexpected errors in distinct-ID concurrent writes: {errors[:3]}"
663 commits = get_all_commits(repo)
664 assert len(commits) == 50
665
666
667 # ===========================================================================
668 # 12. Regression: WARNING→CRITICAL upgrade is permanent
669 # ===========================================================================
670
671 class TestRegressionCorruptLevelUpgrade:
672 """Confirm the upgrade from WARNING to CRITICAL is permanent and precise."""
673
674 def test_corrupt_commit_logs_at_critical_not_warning(
675 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
676 ) -> None:
677 c = _make_commit(repo, message="level-upgrade-commit")
678 _commit_path(repo, c.commit_id).write_bytes(b"trash")
679 with caplog.at_level(logging.DEBUG, logger="muse.core.store"):
680 read_commit(repo, c.commit_id)
681 levels = [r.levelno for r in caplog.records]
682 assert any(lvl == logging.CRITICAL for lvl in levels), (
683 f"Expected CRITICAL (50) but got levels: {levels}"
684 )
685 assert not any(lvl == logging.WARNING for lvl in levels), (
686 "Must not downgrade corruption to WARNING — only CRITICAL is acceptable"
687 )
688
689 def test_corrupt_snapshot_logs_at_critical_not_warning(
690 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
691 ) -> None:
692 s = _make_snapshot(repo, manifest={"snap-level.py": "d" * 64})
693 _snap_path(repo, s.snapshot_id).write_bytes(b"bad")
694 with caplog.at_level(logging.DEBUG, logger="muse.core.store"):
695 read_snapshot(repo, s.snapshot_id)
696 levels = [r.levelno for r in caplog.records]
697 assert any(lvl == logging.CRITICAL for lvl in levels)
698 assert not any(lvl == logging.WARNING for lvl in levels)
699
700 def test_get_all_commits_logs_corrupt_at_critical(
701 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
702 ) -> None:
703 c = _make_commit(repo, message="level-upgrade-get-all")
704 _commit_path(repo, c.commit_id).write_bytes(b"trash")
705 with caplog.at_level(logging.DEBUG, logger="muse.core.store"):
706 get_all_commits(repo)
707 levels = [r.levelno for r in caplog.records]
708 assert any(lvl == logging.CRITICAL for lvl in levels)
709 assert not any(lvl == logging.WARNING for lvl in levels)
710
711 def test_get_all_tags_logs_corrupt_at_critical(
712 self, repo: pathlib.Path, caplog: pytest.LogCaptureFixture
713 ) -> None:
714 t = _make_tag(repo, "v-crit")
715 _tag_path(repo, t.tag_id).write_bytes(b"trash")
716 with caplog.at_level(logging.DEBUG, logger="muse.core.store"):
717 get_all_tags(repo, "test-repo")
718 levels = [r.levelno for r in caplog.records]
719 assert any(lvl == logging.CRITICAL for lvl in levels)
720 assert not any(lvl == logging.WARNING for lvl in levels)
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 100 days ago