"""TDD -- muse#83 Phase 3: ref/reflog integration. No new rewrite logic here -- migrate_snapshot_content_and_cascade() composes Phase 1 (migrate_snapshot_content_ids), Phase 2 (migrate_commit_ids_cascade), and muse.core.migrate's existing, already-tested generic ref/reflog passes (_migrate_refs/_migrate_remote_refs/_migrate_reflogs) unchanged. These tests prove the composition is wired correctly end to end, not that the generic passes themselves work (that's muse.core.migrate's own test suite). """ from __future__ import annotations import datetime import json import pathlib import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from muse.core.ids import hash_commit, hash_snapshot from muse.core.object_store import object_path from muse.core.paths import heads_dir, logs_dir, muse_dir, objects_dir, ref_path, remotes_dir from muse.core.provenance import provenance_payload, sign_commit_ed25519 from muse.core.snapshot_content_migrate import migrate_snapshot_content_and_cascade from muse.core.transport import SigningIdentity _AT_ISO = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc).isoformat() @pytest.fixture def repo(tmp_path: pathlib.Path) -> pathlib.Path: objects_dir(tmp_path).mkdir(parents=True, exist_ok=True) heads_dir(tmp_path).mkdir(parents=True, exist_ok=True) return tmp_path def _make_identity(handle: str = "gabriel") -> SigningIdentity: return SigningIdentity(handle=handle, private_key=Ed25519PrivateKey.generate()) def _sign(commit_id: str, private_key: Ed25519PrivateKey) -> str: payload = provenance_payload(commit_id=commit_id, author="gabriel", committed_at=_AT_ISO) return sign_commit_ed25519(payload, private_key) def _write_unified_commit( repo: pathlib.Path, commit_id: str, *, parent_commit_id: str | None, snapshot_id: str, message: str, signature: str, ) -> None: raw = { "commit_id": commit_id, "branch": "main", "snapshot_id": snapshot_id, "message": message, "committed_at": _AT_ISO, "parent_commit_id": parent_commit_id, "parent2_commit_id": None, "author": "gabriel", "agent_id": "", "model_id": "", "toolchain_id": "", "prompt_hash": "", "signature": signature, "signer_public_key": "", "signer_key_id": "", "format_version": 8, "sem_ver_bump": "none", "breaking_changes": [], } payload = json.dumps(raw, separators=(",", ":")).encode() header = f"commit {len(payload)}\0".encode() path = object_path(repo, commit_id) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(header + payload) def _write_unified_snapshot(repo: pathlib.Path, declared_id: str, manifest: dict[str, str]) -> None: payload = json.dumps({ "snapshot_id": declared_id, "manifest": manifest, "directories": [], "created_at": _AT_ISO, }, separators=(",", ":")).encode() header = f"snapshot {len(payload)}\0".encode() path = object_path(repo, declared_id) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(header + payload) def _build_chain(repo: pathlib.Path, identity: SigningIdentity) -> dict[str, str]: """A -> B(bad snapshot) -> C, matching the plan doc's minimal repro shape.""" manifest_a = {"a.txt": "sha256:" + "1" * 64} snap_a = hash_snapshot(manifest_a, None) _write_unified_snapshot(repo, snap_a, manifest_a) manifest_b = {**manifest_a, "b.txt": "sha256:" + "2" * 64} wrong_snap_b = "sha256:" + "0" * 64 _write_unified_snapshot(repo, wrong_snap_b, manifest_b) manifest_c = {**manifest_b, "c.txt": "sha256:" + "3" * 64} snap_c = hash_snapshot(manifest_c, None) _write_unified_snapshot(repo, snap_c, manifest_c) id_a = hash_commit(parent_ids=[], snapshot_id=snap_a, message="A", committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="") id_b = hash_commit(parent_ids=[id_a], snapshot_id=wrong_snap_b, message="B", committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="") id_c = hash_commit(parent_ids=[id_b], snapshot_id=snap_c, message="C", committed_at_iso=_AT_ISO, author="gabriel", signer_public_key="") _write_unified_commit(repo, id_a, parent_commit_id=None, snapshot_id=snap_a, message="A", signature=_sign(id_a, identity.private_key)) _write_unified_commit(repo, id_b, parent_commit_id=id_a, snapshot_id=wrong_snap_b, message="B", signature=_sign(id_b, identity.private_key)) _write_unified_commit(repo, id_c, parent_commit_id=id_b, snapshot_id=snap_c, message="C", signature=_sign(id_c, identity.private_key)) return {"id_a": id_a, "id_b": id_b, "id_c": id_c} # --------------------------------------------------------------------------- # RF_01 -- a branch head pointing at a rewritten commit resolves to the new ID # --------------------------------------------------------------------------- def test_rf_01_branch_head_updated_to_new_commit_id(repo: pathlib.Path) -> None: identity = _make_identity() ids = _build_chain(repo, identity) ref_path(repo, "main").write_text(f"{ids['id_c']}\n", encoding="utf-8") result = migrate_snapshot_content_and_cascade(repo, identity, dry_run=False) new_c = result.commit_id_map[ids["id_c"]] assert ref_path(repo, "main").read_text(encoding="utf-8").strip() == new_c assert result.refs_updated == 1 # --------------------------------------------------------------------------- # RF_02 -- a branch head pointing at an untouched commit is byte-identical # --------------------------------------------------------------------------- def test_rf_02_untouched_branch_ref_unchanged(repo: pathlib.Path) -> None: identity = _make_identity() ids = _build_chain(repo, identity) ref_path(repo, "main").write_text(f"{ids['id_a']}\n", encoding="utf-8") result = migrate_snapshot_content_and_cascade(repo, identity, dry_run=False) assert ids["id_a"] not in result.commit_id_map assert ref_path(repo, "main").read_text(encoding="utf-8").strip() == ids["id_a"] # --------------------------------------------------------------------------- # RF_03 -- remote-tracking refs are updated the same way # --------------------------------------------------------------------------- def test_rf_03_remote_tracking_ref_updated(repo: pathlib.Path) -> None: identity = _make_identity() ids = _build_chain(repo, identity) remote_ref = remotes_dir(repo) / "staging" / "main" remote_ref.parent.mkdir(parents=True, exist_ok=True) remote_ref.write_text(f"{ids['id_c']}\n", encoding="utf-8") result = migrate_snapshot_content_and_cascade(repo, identity, dry_run=False) new_c = result.commit_id_map[ids["id_c"]] assert remote_ref.read_text(encoding="utf-8").strip() == new_c assert result.remote_refs_updated == 1 # --------------------------------------------------------------------------- # RF_04 -- reflog entries mentioning a rewritten commit are rewritten; # entries mentioning an untouched commit are not # --------------------------------------------------------------------------- def test_rf_04_reflog_entries_rewritten_selectively(repo: pathlib.Path) -> None: identity = _make_identity() ids = _build_chain(repo, identity) reflog = logs_dir(repo) / "HEAD" reflog.parent.mkdir(parents=True, exist_ok=True) reflog.write_text( f"{ids['id_a']} {ids['id_b']} gabriel: commit B\n" f"{ids['id_b']} {ids['id_c']} gabriel: commit C\n", encoding="utf-8", ) result = migrate_snapshot_content_and_cascade(repo, identity, dry_run=False) new_b = result.commit_id_map[ids["id_b"]] new_c = result.commit_id_map[ids["id_c"]] content = reflog.read_text(encoding="utf-8") assert ids["id_a"] in content, "A was never rewritten -- must still appear verbatim" assert new_b in content assert new_c in content assert ids["id_b"] not in content, "old (superseded) B id must not remain in the reflog" assert ids["id_c"] not in content, "old (superseded) C id must not remain in the reflog" # --------------------------------------------------------------------------- # RF_05 -- end-to-end: the full composed result matches Phase 1 + Phase 2 # run independently # --------------------------------------------------------------------------- def test_rf_05_composed_result_matches_independent_phase_runs(repo: pathlib.Path) -> None: from muse.core.snapshot_content_migrate import ( migrate_commit_ids_cascade, migrate_snapshot_content_ids, ) identity = _make_identity() ids = _build_chain(repo, identity) ref_path(repo, "main").write_text(f"{ids['id_c']}\n", encoding="utf-8") # Run the phases independently on a throwaway copy's worth of state by # just calling them in sequence manually (same repo -- both approaches # must agree since they're pure recomputation over the same on-disk data). snap_result = migrate_snapshot_content_ids(repo, dry_run=True) commit_result = migrate_commit_ids_cascade(repo, snap_result.id_map, identity, dry_run=True) combined = migrate_snapshot_content_and_cascade(repo, identity, dry_run=True) assert combined.snapshot_id_map == snap_result.id_map assert combined.commit_id_map == commit_result.id_map