test_snapshot_content_migrate_refs.py
python
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be
Merge branch 'fix/hub-user-read-update-path' into dev
Human
10 days ago
| 1 | """TDD -- muse#83 Phase 3: ref/reflog integration. |
| 2 | |
| 3 | No new rewrite logic here -- migrate_snapshot_content_and_cascade() composes |
| 4 | Phase 1 (migrate_snapshot_content_ids), Phase 2 (migrate_commit_ids_cascade), |
| 5 | and muse.core.migrate's existing, already-tested generic ref/reflog passes |
| 6 | (_migrate_refs/_migrate_remote_refs/_migrate_reflogs) unchanged. These tests |
| 7 | prove the composition is wired correctly end to end, not that the generic |
| 8 | passes themselves work (that's muse.core.migrate's own test suite). |
| 9 | """ |
| 10 | from __future__ import annotations |
| 11 | |
| 12 | import datetime |
| 13 | import json |
| 14 | import pathlib |
| 15 | |
| 16 | import pytest |
| 17 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| 18 | |
| 19 | from muse.core.ids import hash_commit, hash_snapshot |
| 20 | from muse.core.object_store import object_path |
| 21 | from muse.core.paths import heads_dir, logs_dir, muse_dir, objects_dir, ref_path, remotes_dir |
| 22 | from muse.core.provenance import provenance_payload, sign_commit_ed25519 |
| 23 | from muse.core.snapshot_content_migrate import migrate_snapshot_content_and_cascade |
| 24 | from muse.core.transport import SigningIdentity |
| 25 | |
| 26 | _AT_ISO = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc).isoformat() |
| 27 | |
| 28 | |
| 29 | @pytest.fixture |
| 30 | def repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 31 | objects_dir(tmp_path).mkdir(parents=True, exist_ok=True) |
| 32 | heads_dir(tmp_path).mkdir(parents=True, exist_ok=True) |
| 33 | return tmp_path |
| 34 | |
| 35 | |
| 36 | def _make_identity(handle: str = "gabriel") -> SigningIdentity: |
| 37 | return SigningIdentity(handle=handle, private_key=Ed25519PrivateKey.generate()) |
| 38 | |
| 39 | |
| 40 | def _sign(commit_id: str, private_key: Ed25519PrivateKey) -> str: |
| 41 | payload = provenance_payload(commit_id=commit_id, author="gabriel", committed_at=_AT_ISO) |
| 42 | return sign_commit_ed25519(payload, private_key) |
| 43 | |
| 44 | |
| 45 | def _write_unified_commit( |
| 46 | repo: pathlib.Path, commit_id: str, *, parent_commit_id: str | None, |
| 47 | snapshot_id: str, message: str, signature: str, |
| 48 | ) -> None: |
| 49 | raw = { |
| 50 | "commit_id": commit_id, "branch": "main", "snapshot_id": snapshot_id, |
| 51 | "message": message, "committed_at": _AT_ISO, "parent_commit_id": parent_commit_id, |
| 52 | "parent2_commit_id": None, "author": "gabriel", "agent_id": "", "model_id": "", |
| 53 | "toolchain_id": "", "prompt_hash": "", "signature": signature, |
| 54 | "signer_public_key": "", "signer_key_id": "", "format_version": 8, |
| 55 | "sem_ver_bump": "none", "breaking_changes": [], |
| 56 | } |
| 57 | payload = json.dumps(raw, separators=(",", ":")).encode() |
| 58 | header = f"commit {len(payload)}\0".encode() |
| 59 | path = object_path(repo, commit_id) |
| 60 | path.parent.mkdir(parents=True, exist_ok=True) |
| 61 | path.write_bytes(header + payload) |
| 62 | |
| 63 | |
| 64 | def _write_unified_snapshot(repo: pathlib.Path, declared_id: str, manifest: dict[str, str]) -> None: |
| 65 | payload = json.dumps({ |
| 66 | "snapshot_id": declared_id, "manifest": manifest, "directories": [], |
| 67 | "created_at": _AT_ISO, |
| 68 | }, separators=(",", ":")).encode() |
| 69 | header = f"snapshot {len(payload)}\0".encode() |
| 70 | path = object_path(repo, declared_id) |
| 71 | path.parent.mkdir(parents=True, exist_ok=True) |
| 72 | path.write_bytes(header + payload) |
| 73 | |
| 74 | |
| 75 | def _build_chain(repo: pathlib.Path, identity: SigningIdentity) -> dict[str, str]: |
| 76 | """A -> B(bad snapshot) -> C, matching the plan doc's minimal repro shape.""" |
| 77 | manifest_a = {"a.txt": "sha256:" + "1" * 64} |
| 78 | snap_a = hash_snapshot(manifest_a, None) |
| 79 | _write_unified_snapshot(repo, snap_a, manifest_a) |
| 80 | |
| 81 | manifest_b = {**manifest_a, "b.txt": "sha256:" + "2" * 64} |
| 82 | wrong_snap_b = "sha256:" + "0" * 64 |
| 83 | _write_unified_snapshot(repo, wrong_snap_b, manifest_b) |
| 84 | |
| 85 | manifest_c = {**manifest_b, "c.txt": "sha256:" + "3" * 64} |
| 86 | snap_c = hash_snapshot(manifest_c, None) |
| 87 | _write_unified_snapshot(repo, snap_c, manifest_c) |
| 88 | |
| 89 | id_a = hash_commit(parent_ids=[], snapshot_id=snap_a, message="A", |
| 90 | committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="") |
| 91 | id_b = hash_commit(parent_ids=[id_a], snapshot_id=wrong_snap_b, message="B", |
| 92 | committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="") |
| 93 | id_c = hash_commit(parent_ids=[id_b], snapshot_id=snap_c, message="C", |
| 94 | committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="") |
| 95 | |
| 96 | _write_unified_commit(repo, id_a, parent_commit_id=None, snapshot_id=snap_a, message="A", |
| 97 | signature=_sign(id_a, identity.private_key)) |
| 98 | _write_unified_commit(repo, id_b, parent_commit_id=id_a, snapshot_id=wrong_snap_b, message="B", |
| 99 | signature=_sign(id_b, identity.private_key)) |
| 100 | _write_unified_commit(repo, id_c, parent_commit_id=id_b, snapshot_id=snap_c, message="C", |
| 101 | signature=_sign(id_c, identity.private_key)) |
| 102 | |
| 103 | return {"id_a": id_a, "id_b": id_b, "id_c": id_c} |
| 104 | |
| 105 | |
| 106 | # --------------------------------------------------------------------------- |
| 107 | # RF_01 -- a branch head pointing at a rewritten commit resolves to the new ID |
| 108 | # --------------------------------------------------------------------------- |
| 109 | |
| 110 | |
| 111 | def test_rf_01_branch_head_updated_to_new_commit_id(repo: pathlib.Path) -> None: |
| 112 | identity = _make_identity() |
| 113 | ids = _build_chain(repo, identity) |
| 114 | ref_path(repo, "main").write_text(f"{ids['id_c']}\n", encoding="utf-8") |
| 115 | |
| 116 | result = migrate_snapshot_content_and_cascade(repo, identity, dry_run=False) |
| 117 | |
| 118 | new_c = result.commit_id_map[ids["id_c"]] |
| 119 | assert ref_path(repo, "main").read_text(encoding="utf-8").strip() == new_c |
| 120 | assert result.refs_updated == 1 |
| 121 | |
| 122 | |
| 123 | # --------------------------------------------------------------------------- |
| 124 | # RF_02 -- a branch head pointing at an untouched commit is byte-identical |
| 125 | # --------------------------------------------------------------------------- |
| 126 | |
| 127 | |
| 128 | def test_rf_02_untouched_branch_ref_unchanged(repo: pathlib.Path) -> None: |
| 129 | identity = _make_identity() |
| 130 | ids = _build_chain(repo, identity) |
| 131 | ref_path(repo, "main").write_text(f"{ids['id_a']}\n", encoding="utf-8") |
| 132 | |
| 133 | result = migrate_snapshot_content_and_cascade(repo, identity, dry_run=False) |
| 134 | |
| 135 | assert ids["id_a"] not in result.commit_id_map |
| 136 | assert ref_path(repo, "main").read_text(encoding="utf-8").strip() == ids["id_a"] |
| 137 | |
| 138 | |
| 139 | # --------------------------------------------------------------------------- |
| 140 | # RF_03 -- remote-tracking refs are updated the same way |
| 141 | # --------------------------------------------------------------------------- |
| 142 | |
| 143 | |
| 144 | def test_rf_03_remote_tracking_ref_updated(repo: pathlib.Path) -> None: |
| 145 | identity = _make_identity() |
| 146 | ids = _build_chain(repo, identity) |
| 147 | remote_ref = remotes_dir(repo) / "staging" / "main" |
| 148 | remote_ref.parent.mkdir(parents=True, exist_ok=True) |
| 149 | remote_ref.write_text(f"{ids['id_c']}\n", encoding="utf-8") |
| 150 | |
| 151 | result = migrate_snapshot_content_and_cascade(repo, identity, dry_run=False) |
| 152 | |
| 153 | new_c = result.commit_id_map[ids["id_c"]] |
| 154 | assert remote_ref.read_text(encoding="utf-8").strip() == new_c |
| 155 | assert result.remote_refs_updated == 1 |
| 156 | |
| 157 | |
| 158 | # --------------------------------------------------------------------------- |
| 159 | # RF_04 -- reflog entries mentioning a rewritten commit are rewritten; |
| 160 | # entries mentioning an untouched commit are not |
| 161 | # --------------------------------------------------------------------------- |
| 162 | |
| 163 | |
| 164 | def test_rf_04_reflog_entries_rewritten_selectively(repo: pathlib.Path) -> None: |
| 165 | identity = _make_identity() |
| 166 | ids = _build_chain(repo, identity) |
| 167 | reflog = logs_dir(repo) / "HEAD" |
| 168 | reflog.parent.mkdir(parents=True, exist_ok=True) |
| 169 | reflog.write_text( |
| 170 | f"{ids['id_a']} {ids['id_b']} gabriel: commit B\n" |
| 171 | f"{ids['id_b']} {ids['id_c']} gabriel: commit C\n", |
| 172 | encoding="utf-8", |
| 173 | ) |
| 174 | |
| 175 | result = migrate_snapshot_content_and_cascade(repo, identity, dry_run=False) |
| 176 | |
| 177 | new_b = result.commit_id_map[ids["id_b"]] |
| 178 | new_c = result.commit_id_map[ids["id_c"]] |
| 179 | content = reflog.read_text(encoding="utf-8") |
| 180 | assert ids["id_a"] in content, "A was never rewritten -- must still appear verbatim" |
| 181 | assert new_b in content |
| 182 | assert new_c in content |
| 183 | assert ids["id_b"] not in content, "old (superseded) B id must not remain in the reflog" |
| 184 | assert ids["id_c"] not in content, "old (superseded) C id must not remain in the reflog" |
| 185 | |
| 186 | |
| 187 | # --------------------------------------------------------------------------- |
| 188 | # RF_05 -- end-to-end: the full composed result matches Phase 1 + Phase 2 |
| 189 | # run independently |
| 190 | # --------------------------------------------------------------------------- |
| 191 | |
| 192 | |
| 193 | def test_rf_05_composed_result_matches_independent_phase_runs(repo: pathlib.Path) -> None: |
| 194 | from muse.core.snapshot_content_migrate import ( |
| 195 | migrate_commit_ids_cascade, |
| 196 | migrate_snapshot_content_ids, |
| 197 | ) |
| 198 | |
| 199 | identity = _make_identity() |
| 200 | ids = _build_chain(repo, identity) |
| 201 | ref_path(repo, "main").write_text(f"{ids['id_c']}\n", encoding="utf-8") |
| 202 | |
| 203 | # Run the phases independently on a throwaway copy's worth of state by |
| 204 | # just calling them in sequence manually (same repo -- both approaches |
| 205 | # must agree since they're pure recomputation over the same on-disk data). |
| 206 | snap_result = migrate_snapshot_content_ids(repo, dry_run=True) |
| 207 | commit_result = migrate_commit_ids_cascade(repo, snap_result.id_map, identity, dry_run=True) |
| 208 | |
| 209 | combined = migrate_snapshot_content_and_cascade(repo, identity, dry_run=True) |
| 210 | |
| 211 | assert combined.snapshot_id_map == snap_result.id_map |
| 212 | assert combined.commit_id_map == commit_result.id_map |
File History
1 commit
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be
Merge branch 'fix/hub-user-read-update-path' into dev
Human
10 days ago