"""Tests for scripts/backfill_marketplace_domain_links.py — musehub#120 Phase 3 (MDL_11-14). Covers the testable core (`backfill_marketplace_domain_links(session, apply=...)`), not the CLI/`main()` wrapper — the wrapper is thin plumbing (argparse + init_db + print) with no branching logic worth a DB-backed test. """ from __future__ import annotations import secrets from datetime import datetime, timezone import pytest from sqlalchemy.ext.asyncio import AsyncSession from musehub.core.genesis import compute_identity_id, compute_repo_id from musehub.db.musehub_domain_models import MusehubDomain from musehub.db.musehub_repo_models import MusehubRepo from musehub.services.musehub_domains import compute_manifest_hash from scripts.backfill_marketplace_domain_links import backfill_marketplace_domain_links async def _db_domain( session: AsyncSession, *, author_slug: str = "alice", slug: str, is_deprecated: bool = False, ) -> MusehubDomain: caps = {"dimensions": [], "merge_semantics": "three_way"} domain = MusehubDomain( domain_id=secrets.token_hex(16), author_user_id=author_slug, author_slug=author_slug, slug=slug, display_name=slug, description="Test domain", version="1.0.0", manifest_hash=compute_manifest_hash(caps), capabilities=caps, viewer_type="generic", install_count=0, is_verified=False, is_deprecated=is_deprecated, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), ) session.add(domain) await session.flush() return domain async def _db_repo( session: AsyncSession, *, owner: str = "bob", domain_id: str, marketplace_domain_id: str | None = None, ) -> MusehubRepo: owner_id = compute_identity_id(owner.encode()) created_at = datetime.now(tz=timezone.utc) slug = f"repo-{secrets.token_hex(4)}" repo = MusehubRepo( repo_id=compute_repo_id(owner_id, slug, domain_id, created_at.isoformat()), name=slug, slug=slug, owner=owner, owner_user_id=owner_id, visibility="public", domain_id=domain_id, marketplace_domain_id=marketplace_domain_id, created_at=created_at, updated_at=created_at, ) session.add(repo) await session.flush() return repo class TestBackfillDryRun: @pytest.mark.asyncio async def test_mdl11_dry_run_reports_without_writing(self, db_session: AsyncSession) -> None: d = await _db_domain(db_session, slug="code") repo = await _db_repo(db_session, domain_id="code") report = await backfill_marketplace_domain_links(db_session, apply=False) assert repo.repo_id in report.linked assert report.skipped_ambiguous == [] assert report.skipped_no_match == [] # Dry-run must not mutate the ORM object at all. refreshed = await db_session.get(MusehubRepo, repo.repo_id) assert refreshed is not None assert refreshed.marketplace_domain_id is None d_refreshed = await db_session.get(MusehubDomain, d.domain_id) assert d_refreshed is not None assert d_refreshed.install_count == 0 class TestBackfillApply: @pytest.mark.asyncio async def test_mdl12_apply_writes_exactly_what_dry_run_reported( self, db_session: AsyncSession ) -> None: d = await _db_domain(db_session, slug="mist") repo = await _db_repo(db_session, domain_id="mist") dry_report = await backfill_marketplace_domain_links(db_session, apply=False) assert repo.repo_id in dry_report.linked apply_report = await backfill_marketplace_domain_links(db_session, apply=True) await db_session.commit() assert apply_report.linked == dry_report.linked refreshed = await db_session.get(MusehubRepo, repo.repo_id) assert refreshed is not None assert refreshed.marketplace_domain_id == d.domain_id d_refreshed = await db_session.get(MusehubDomain, d.domain_id) assert d_refreshed is not None assert d_refreshed.install_count == 1 class TestBackfillIdempotent: @pytest.mark.asyncio async def test_mdl13_already_linked_repos_are_untouched( self, db_session: AsyncSession ) -> None: d = await _db_domain(db_session, slug="identity") already_linked = await _db_repo( db_session, domain_id="identity", marketplace_domain_id=d.domain_id ) report = await backfill_marketplace_domain_links(db_session, apply=True) await db_session.commit() assert already_linked.repo_id not in report.linked assert already_linked.repo_id not in report.skipped_ambiguous assert already_linked.repo_id not in report.skipped_no_match # install_count untouched — this repo was never "newly" linked. d_refreshed = await db_session.get(MusehubDomain, d.domain_id) assert d_refreshed is not None assert d_refreshed.install_count == 0 @pytest.mark.asyncio async def test_mdl13_safe_to_rerun_after_apply(self, db_session: AsyncSession) -> None: d = await _db_domain(db_session, slug="code") repo = await _db_repo(db_session, domain_id="code") await backfill_marketplace_domain_links(db_session, apply=True) await db_session.commit() second_report = await backfill_marketplace_domain_links(db_session, apply=True) await db_session.commit() assert repo.repo_id not in second_report.linked assert repo.repo_id not in second_report.skipped_ambiguous assert repo.repo_id not in second_report.skipped_no_match d_refreshed = await db_session.get(MusehubDomain, d.domain_id) assert d_refreshed is not None assert d_refreshed.install_count == 1 # not double-counted class TestBackfillReportsEverySkip: @pytest.mark.asyncio async def test_mdl14_ambiguous_repo_reported_by_repo_id( self, db_session: AsyncSession ) -> None: await _db_domain(db_session, author_slug="alice", slug="code") await _db_domain(db_session, author_slug="gabriel", slug="code") repo = await _db_repo(db_session, domain_id="code") report = await backfill_marketplace_domain_links(db_session, apply=True) await db_session.commit() assert repo.repo_id in report.skipped_ambiguous assert repo.repo_id not in report.linked refreshed = await db_session.get(MusehubRepo, repo.repo_id) assert refreshed is not None assert refreshed.marketplace_domain_id is None @pytest.mark.asyncio async def test_mdl14_no_match_repo_reported_by_repo_id( self, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session, domain_id="no-such-category") report = await backfill_marketplace_domain_links(db_session, apply=True) await db_session.commit() assert repo.repo_id in report.skipped_no_match assert repo.repo_id not in report.linked