"""Phase 4: wire_negotiate reads disk, not DB — TDD (RED → GREEN). Seven tiers: Tier 1 — _commit_exists_on_disk returns True when object file present Tier 2 — _commit_exists_on_disk returns False when object file absent Tier 3 — wire_negotiate acks a have-ID that exists on disk (even if not in DB) Tier 4 — wire_negotiate does NOT ack a have-ID absent from disk (even if in DB) Tier 5 — wire_negotiate acks nothing when have list is empty Tier 6 — wire_negotiate ready=True for full clone (no have, want only) Tier 7 — wire_negotiate uses no DB query for have-set resolution """ from __future__ import annotations import secrets from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch, call import pytest import pytest_asyncio from sqlalchemy.ext.asyncio import AsyncSession from muse.core.types import blob_id, long_id # ── helpers ─────────────────────────────────────────────────────────────────── def _oid() -> str: return long_id(secrets.token_hex(32)) def _write_object(repo_root: Path, object_id: str, content: bytes = b"commit data") -> None: """Write a fake object file at the canonical path for object_id.""" from muse.core.object_store import object_path from muse.core.paths import server_objects_dir p = object_path(repo_root, object_id, objects_base=server_objects_dir(repo_root)) p.parent.mkdir(parents=True, exist_ok=True) p.write_bytes(content) def _repo_root(tmp_path: Path, owner: str = "gabriel", slug: str = "test-repo") -> Path: root = tmp_path / owner / slug (root / "refs" / "heads").mkdir(parents=True, exist_ok=True) (root / "objects").mkdir(parents=True, exist_ok=True) return root # ── Tier 1: _commit_exists_on_disk returns True ─────────────────────────────── class TestCommitExistsOnDiskTrue: def test_returns_true_when_object_file_exists(self, tmp_path: Path) -> None: from musehub.services.musehub_wire import _commit_exists_on_disk repo_root = _repo_root(tmp_path) commit_id = _oid() _write_object(repo_root, commit_id) result = _commit_exists_on_disk(repo_root, commit_id) assert result is True def test_returns_true_for_multiple_commits(self, tmp_path: Path) -> None: from musehub.services.musehub_wire import _commit_exists_on_disk repo_root = _repo_root(tmp_path) ids = [_oid() for _ in range(5)] for oid in ids: _write_object(repo_root, oid) for oid in ids: assert _commit_exists_on_disk(repo_root, oid) is True def test_returns_true_after_explicit_write(self, tmp_path: Path) -> None: from musehub.services.musehub_wire import _commit_exists_on_disk from muse.core.object_store import object_path from muse.core.paths import server_objects_dir repo_root = _repo_root(tmp_path) commit_id = _oid() # Not present yet assert _commit_exists_on_disk(repo_root, commit_id) is False # Write it _write_object(repo_root, commit_id) # Now present assert _commit_exists_on_disk(repo_root, commit_id) is True # ── Tier 2: _commit_exists_on_disk returns False ───────────────────────────── class TestCommitExistsOnDiskFalse: def test_returns_false_for_unknown_commit(self, tmp_path: Path) -> None: from musehub.services.musehub_wire import _commit_exists_on_disk repo_root = _repo_root(tmp_path) assert _commit_exists_on_disk(repo_root, _oid()) is False def test_returns_false_for_empty_repo(self, tmp_path: Path) -> None: from musehub.services.musehub_wire import _commit_exists_on_disk repo_root = _repo_root(tmp_path) for _ in range(3): assert _commit_exists_on_disk(repo_root, _oid()) is False def test_returns_false_after_file_deleted(self, tmp_path: Path) -> None: from musehub.services.musehub_wire import _commit_exists_on_disk from muse.core.object_store import object_path from muse.core.paths import server_objects_dir repo_root = _repo_root(tmp_path) commit_id = _oid() _write_object(repo_root, commit_id) p = object_path(repo_root, commit_id, objects_base=server_objects_dir(repo_root)) p.unlink() assert _commit_exists_on_disk(repo_root, commit_id) is False # ── Tier 3: negotiate acks disk-present commits ─────────────────────────────── class TestNegotiateAcksDiskPresent: @pytest.mark.asyncio async def test_acks_commit_present_on_disk_not_in_db( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """Negotiate acks a have-ID that exists on disk even if DB has no record.""" from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.config import settings monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 Disk Ack") commit_id = _oid() repo_root = tmp_path / repo.owner / repo.slug _write_object(repo_root, commit_id) req = WireNegotiateRequest(have=[commit_id], want=[]) resp = await wire_negotiate(db_session, repo.repo_id, req) assert commit_id in resp.ack @pytest.mark.asyncio async def test_acks_multiple_disk_commits( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.config import settings monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 Multi Disk Ack") ids = [_oid() for _ in range(4)] repo_root = tmp_path / repo.owner / repo.slug for oid in ids: _write_object(repo_root, oid) req = WireNegotiateRequest(have=ids, want=[]) resp = await wire_negotiate(db_session, repo.repo_id, req) for oid in ids: assert oid in resp.ack # ── Tier 4: negotiate does NOT ack disk-absent commits ─────────────────────── class TestNegotiateRejectsDiskAbsent: @pytest.mark.asyncio async def test_does_not_ack_commit_in_db_but_not_on_disk( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """DB-only commit must NOT be acked — disk is the source of truth.""" from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.db.musehub_models import MusehubCommit from musehub.config import settings monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 DB Only") commit_id = _oid() # Write to DB only — not to disk from datetime import datetime, timezone db_commit = MusehubCommit( commit_id=commit_id, repo_id=repo.repo_id, branch="main", message="db-only commit", author="gabriel", parent_ids=[], snapshot_id=_oid(), timestamp=datetime.now(tz=timezone.utc), ) db_session.add(db_commit) await db_session.commit() req = WireNegotiateRequest(have=[commit_id], want=[]) resp = await wire_negotiate(db_session, repo.repo_id, req) assert commit_id not in resp.ack @pytest.mark.asyncio async def test_does_not_ack_unknown_commit( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.config import settings monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 Unknown Commit") unknown_id = _oid() req = WireNegotiateRequest(have=[unknown_id], want=[]) resp = await wire_negotiate(db_session, repo.repo_id, req) assert unknown_id not in resp.ack # ── Tier 5: empty have list ─────────────────────────────────────────────────── class TestNegotiateEmptyHave: @pytest.mark.asyncio async def test_empty_have_yields_empty_ack( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.config import settings monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 Empty Have") req = WireNegotiateRequest(have=[], want=[]) resp = await wire_negotiate(db_session, repo.repo_id, req) assert resp.ack == [] @pytest.mark.asyncio async def test_partial_have_only_acks_disk_present( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.config import settings monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 Partial Have") present_id = _oid() absent_id = _oid() repo_root = tmp_path / repo.owner / repo.slug _write_object(repo_root, present_id) req = WireNegotiateRequest(have=[present_id, absent_id], want=[]) resp = await wire_negotiate(db_session, repo.repo_id, req) assert present_id in resp.ack assert absent_id not in resp.ack # ── Tier 6: ready flag for full clone ──────────────────────────────────────── class TestNegotiateReadyFlag: @pytest.mark.asyncio async def test_ready_true_when_no_have_ids( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """Full clone: no have-IDs → ready=True (server sends everything).""" from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.config import settings monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 Full Clone") req = WireNegotiateRequest(have=[], want=[_oid()]) resp = await wire_negotiate(db_session, repo.repo_id, req) assert resp.ready is True @pytest.mark.asyncio async def test_ack_set_drives_ready_when_have_present( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """When have-IDs are acked and want is specified, ready depends on common_base.""" from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.config import settings monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 Ready With Have") # Write a have-commit to disk but nothing else have_id = _oid() repo_root = tmp_path / repo.owner / repo.slug _write_object(repo_root, have_id) req = WireNegotiateRequest(have=[have_id], want=[_oid()]) resp = await wire_negotiate(db_session, repo.repo_id, req) # have_id acked, but common_base logic still applies assert have_id in resp.ack # ── Tier 7: wire_negotiate never queries musehub_commits for have resolution ── class TestNegotiateNoDB: @pytest.mark.asyncio async def test_no_musehub_commits_query_for_have_ack( self, tmp_path: Path, db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch ) -> None: """The have-set acknowledgment must not query musehub_commits.""" from musehub.services.musehub_wire import wire_negotiate from musehub.models.wire import WireNegotiateRequest from musehub.config import settings import musehub.db.musehub_models as models monkeypatch.setattr(settings, "musehub_repos_dir", str(tmp_path)) repo = await _make_repo(db_session, "Phase4 No DB Have") commit_id = _oid() repo_root = tmp_path / repo.owner / repo.slug _write_object(repo_root, commit_id) # Track SQL queries issued during negotiate queries: list[str] = [] original_execute = db_session.execute async def spy_execute(stmt, *args, **kwargs): q = str(stmt.compile(compile_kwargs={"literal_binds": True})) if hasattr(stmt, 'compile') else str(stmt) queries.append(q) return await original_execute(stmt, *args, **kwargs) monkeypatch.setattr(db_session, "execute", spy_execute) req = WireNegotiateRequest(have=[commit_id], want=[]) await wire_negotiate(db_session, repo.repo_id, req) # None of the queries should touch musehub_commits for have resolution have_resolution_queries = [ q for q in queries if "musehub_commit" in q.lower() and "have" in q.lower() ] assert not have_resolution_queries, ( f"wire_negotiate queried musehub_commits for have resolution: {have_resolution_queries}" ) # ── shared fixture helpers ──────────────────────────────────────────────────── async def _make_repo(db_session: AsyncSession, name: str, owner: str = "gabriel") -> "MusehubRepo": from datetime import datetime, timezone from musehub.db.musehub_models import MusehubRepo, MusehubBranch from musehub.core.genesis import compute_identity_id, compute_repo_id, compute_branch_id owner_user_id = compute_identity_id(owner.encode()) slug = name.lower().replace(" ", "-") created_at = datetime.now(tz=timezone.utc) repo_id = compute_repo_id(owner_user_id, slug, "code", created_at.isoformat()) repo = MusehubRepo( repo_id=repo_id, name=name, owner=owner, slug=slug, visibility="public", owner_user_id=owner_user_id, description="", tags=[], created_at=created_at, ) db_session.add(repo) await db_session.commit() branch = MusehubBranch( branch_id=compute_branch_id(repo_id, "main"), repo_id=repo_id, name="main", ) db_session.add(branch) await db_session.commit() await db_session.refresh(repo) return repo