"""Unit tests for musehub/services/musehub_reflog.py (muse#54 item 4). Tests the service layer directly (no HTTP): append-only store/list round-trip, per-branch isolation, dedup via content-hash entry_id, and default handling for missing/empty fields. """ from __future__ import annotations from datetime import datetime, timezone from sqlalchemy.ext.asyncio import AsyncSession from musehub.services import musehub_reflog as reflog_svc from tests.factories import create_repo def _entry_dict(old_id: str, new_id: str, **overrides: str) -> dict: base = { "old_id": old_id, "new_id": new_id, "author": "gabriel", "operation": "commit: c1", "timestamp": datetime.now(timezone.utc).isoformat(), } base.update(overrides) return base async def test_store_and_list_round_trip(db_session: AsyncSession) -> None: repo = await create_repo(db_session, visibility="public") stored, skipped = await reflog_svc.store_reflog_entries( db_session, str(repo.repo_id), "main", [_entry_dict("0" * 64, "a" * 64)], ) await db_session.commit() assert stored == 1 assert skipped == 0 entries = await reflog_svc.list_reflog_entries(db_session, str(repo.repo_id), "main") assert len(entries) == 1 assert entries[0]["old_id"] == "0" * 64 assert entries[0]["new_id"] == "a" * 64 assert entries[0]["operation"] == "commit: c1" async def test_repush_same_entry_is_skipped(db_session: AsyncSession) -> None: """Re-pushing an identical entry (same content hash) is a true no-op -- it must not duplicate the row.""" repo = await create_repo(db_session, visibility="public") entry = _entry_dict("0" * 64, "a" * 64, timestamp="2026-01-01T00:00:00+00:00") await reflog_svc.store_reflog_entries(db_session, str(repo.repo_id), "main", [entry]) await db_session.commit() stored, skipped = await reflog_svc.store_reflog_entries( db_session, str(repo.repo_id), "main", [entry], ) await db_session.commit() assert stored == 0 assert skipped == 1 entries = await reflog_svc.list_reflog_entries(db_session, str(repo.repo_id), "main") assert len(entries) == 1 async def test_entries_isolated_per_branch(db_session: AsyncSession) -> None: repo = await create_repo(db_session, visibility="public") await reflog_svc.store_reflog_entries( db_session, str(repo.repo_id), "main", [_entry_dict("0" * 64, "a" * 64)], ) await reflog_svc.store_reflog_entries( db_session, str(repo.repo_id), "dev", [_entry_dict("0" * 64, "b" * 64)], ) await db_session.commit() main_entries = await reflog_svc.list_reflog_entries(db_session, str(repo.repo_id), "main") dev_entries = await reflog_svc.list_reflog_entries(db_session, str(repo.repo_id), "dev") assert len(main_entries) == 1 assert len(dev_entries) == 1 assert main_entries[0]["new_id"] == "a" * 64 assert dev_entries[0]["new_id"] == "b" * 64 async def test_list_ordered_oldest_first(db_session: AsyncSession) -> None: repo = await create_repo(db_session, visibility="public") await reflog_svc.store_reflog_entries( db_session, str(repo.repo_id), "main", [ _entry_dict("0" * 64, "a" * 64, timestamp="2026-01-01T00:00:00+00:00"), _entry_dict("a" * 64, "b" * 64, timestamp="2026-01-02T00:00:00+00:00"), ], ) await db_session.commit() entries = await reflog_svc.list_reflog_entries(db_session, str(repo.repo_id), "main") assert len(entries) == 2 assert entries[0]["new_id"] == "a" * 64 assert entries[1]["new_id"] == "b" * 64 async def test_empty_entries_list_is_noop(db_session: AsyncSession) -> None: repo = await create_repo(db_session, visibility="public") stored, skipped = await reflog_svc.store_reflog_entries( db_session, str(repo.repo_id), "main", [], ) assert (stored, skipped) == (0, 0) async def test_entry_missing_new_id_is_skipped_silently(db_session: AsyncSession) -> None: repo = await create_repo(db_session, visibility="public") stored, skipped = await reflog_svc.store_reflog_entries( db_session, str(repo.repo_id), "main", [_entry_dict("0" * 64, "")], ) await db_session.commit() assert stored == 0 assert skipped == 0 entries = await reflog_svc.list_reflog_entries(db_session, str(repo.repo_id), "main") assert entries == []