gabriel / muse public
test_snapshot_content_migrate.py python
188 lines 7.1 KB
Raw
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 10 days ago
1 """TDD -- muse#83: unified-store snapshot-content-ID migration.
2
3 Repairs a class of corruption distinct from muse.core.migrate's existing
4 v1->v2 commit-ID-formula and legacy-directory-layout passes: a commit whose
5 declared snapshot_id never matched its own manifest, since creation. See
6 docs/issues/snapshot-content-id-migration-plan.md for the full background
7 (musehub#134's investigation).
8
9 Phase 1 tests: migrate_snapshot_content_ids() -- detection + correction at
10 the snapshot layer, sourced from the unified .muse/objects/ store only.
11 """
12 from __future__ import annotations
13
14 import json
15 import pathlib
16
17 import pytest
18
19 from muse.core.ids import hash_snapshot
20 from muse.core.object_store import object_path
21 from muse.core.paths import muse_dir, objects_dir, snapshots_dir
22 from muse.core.snapshot_content_migrate import migrate_snapshot_content_ids
23
24
25 # ---------------------------------------------------------------------------
26 # Fixtures / helpers
27 # ---------------------------------------------------------------------------
28
29
30 @pytest.fixture
31 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
32 objects_dir(tmp_path).mkdir(parents=True, exist_ok=True)
33 return tmp_path
34
35
36 def _write_unified_snapshot(
37 repo: pathlib.Path,
38 declared_id: str,
39 manifest: dict[str, str],
40 directories: list[str] | None = None,
41 ) -> pathlib.Path:
42 """Write a snapshot object directly to the unified store at the path
43 implied by *declared_id* -- which may or may not actually reproduce
44 the manifest (this is exactly how the real corrupted staging data
45 looks: a file whose own embedded snapshot_id disagrees with a fresh
46 hash_snapshot recomputation of its own manifest)."""
47 payload = json.dumps({
48 "snapshot_id": declared_id,
49 "manifest": manifest,
50 "directories": directories or [],
51 "created_at": "2026-05-27T00:00:00+00:00",
52 }, separators=(",", ":")).encode()
53 header = f"snapshot {len(payload)}\0".encode()
54 path = object_path(repo, declared_id)
55 path.parent.mkdir(parents=True, exist_ok=True)
56 path.write_bytes(header + payload)
57 return path
58
59
60 def _read_unified_snapshot(repo: pathlib.Path, object_id: str) -> dict:
61 path = object_path(repo, object_id)
62 data = path.read_bytes()
63 null_idx = data.index(b"\x00")
64 return json.loads(data[null_idx + 1:])
65
66
67 # ---------------------------------------------------------------------------
68 # SC_01 -- a correct snapshot is left completely untouched
69 # ---------------------------------------------------------------------------
70
71
72 def test_sc_01_correct_snapshot_produces_no_id_map_entry_and_no_write(
73 repo: pathlib.Path,
74 ) -> None:
75 manifest = {"a.txt": "sha256:" + "1" * 64}
76 correct_id = hash_snapshot(manifest, None)
77 path = _write_unified_snapshot(repo, correct_id, manifest)
78 before_mtime = path.stat().st_mtime_ns
79 before_bytes = path.read_bytes()
80
81 result = migrate_snapshot_content_ids(repo, dry_run=False)
82
83 assert correct_id not in result.id_map
84 assert result.snapshots_written == 0
85 assert path.stat().st_mtime_ns == before_mtime
86 assert path.read_bytes() == before_bytes
87
88
89 # ---------------------------------------------------------------------------
90 # SC_02 -- a snapshot whose declared ID doesn't match its manifest is
91 # detected and corrected
92 # ---------------------------------------------------------------------------
93
94
95 def test_sc_02_mismatched_snapshot_detected_and_corrected(repo: pathlib.Path) -> None:
96 manifest = {"b.txt": "sha256:" + "2" * 64}
97 wrong_id = "sha256:" + "0" * 64 # deliberately wrong -- like the real staging bug
98 _write_unified_snapshot(repo, wrong_id, manifest)
99
100 result = migrate_snapshot_content_ids(repo, dry_run=False)
101
102 correct_id = hash_snapshot(manifest, None)
103 assert result.id_map == {wrong_id: correct_id}
104 assert result.snapshots_written == 1
105
106 new_path = object_path(repo, correct_id)
107 assert new_path.exists()
108 corrected = _read_unified_snapshot(repo, correct_id)
109 assert corrected["snapshot_id"] == correct_id
110 assert corrected["manifest"] == manifest
111
112 # Non-destructive: the old (wrong) object is never deleted.
113 old_path = object_path(repo, wrong_id)
114 assert old_path.exists()
115
116
117 # ---------------------------------------------------------------------------
118 # SC_03 -- dry_run computes the id_map but writes nothing
119 # ---------------------------------------------------------------------------
120
121
122 def test_sc_03_dry_run_writes_nothing(repo: pathlib.Path) -> None:
123 manifest = {"c.txt": "sha256:" + "3" * 64}
124 wrong_id = "sha256:" + "1" * 64
125 _write_unified_snapshot(repo, wrong_id, manifest)
126
127 result = migrate_snapshot_content_ids(repo, dry_run=True)
128
129 correct_id = hash_snapshot(manifest, None)
130 assert result.id_map == {wrong_id: correct_id}
131 assert result.snapshots_written == 1 # counted...
132 assert not object_path(repo, correct_id).exists() # ...but never written
133
134
135 # ---------------------------------------------------------------------------
136 # SC_04 -- the legacy .muse/snapshots/ layout is never read by this function
137 # ---------------------------------------------------------------------------
138
139
140 def test_sc_04_legacy_layout_snapshots_are_ignored(repo: pathlib.Path) -> None:
141 """A mismatch that only exists in the legacy layout must produce zero
142 changes -- that's muse.core.migrate.migrate_snapshot_ids's job, and this
143 function must be strictly additive to it, never a replacement."""
144 import msgpack
145
146 manifest = {"legacy.txt": "sha256:" + "4" * 64}
147 wrong_id = "sha256:" + "5" * 64
148 hex_id = wrong_id.split(":", 1)[1]
149 legacy_path = snapshots_dir(repo) / "sha256" / f"{hex_id}.msgpack"
150 legacy_path.parent.mkdir(parents=True, exist_ok=True)
151 legacy_path.write_bytes(msgpack.packb(
152 {"snapshot_id": wrong_id, "manifest": manifest, "created_at": "2026-01-01T00:00:00+00:00"},
153 use_bin_type=True,
154 ))
155
156 result = migrate_snapshot_content_ids(repo, dry_run=False)
157
158 assert result.id_map == {}
159 assert result.snapshots_written == 0
160
161
162 # ---------------------------------------------------------------------------
163 # SC_05 -- correctly derives directories into the hash, not just the manifest
164 # ---------------------------------------------------------------------------
165
166
167 def test_sc_05_directories_are_included_in_recomputation(repo: pathlib.Path) -> None:
168 manifest = {"src/a.txt": "sha256:" + "6" * 64}
169 directories = ["src"]
170 wrong_id = hash_snapshot(manifest, None) # correct WITHOUT dirs, wrong WITH dirs
171 _write_unified_snapshot(repo, wrong_id, manifest, directories=directories)
172
173 result = migrate_snapshot_content_ids(repo, dry_run=False)
174
175 correct_id = hash_snapshot(manifest, directories)
176 assert wrong_id in result.id_map
177 assert result.id_map[wrong_id] == correct_id
178
179
180 # ---------------------------------------------------------------------------
181 # SC_06 -- no .muse/objects/ directory at all is a clean no-op
182 # ---------------------------------------------------------------------------
183
184
185 def test_sc_06_missing_objects_dir_is_a_noop(tmp_path: pathlib.Path) -> None:
186 result = migrate_snapshot_content_ids(tmp_path, dry_run=False)
187 assert result.id_map == {}
188 assert result.snapshots_written == 0
File History 1 commit
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 10 days ago