""" test_multiplatform_ranking.py Comprehensive QA Governance & Sanity Test Suite for the HIFFI Multi-Platform Pipeline. Enforces zero-imputation policy, production rank preservation, score monotonicity, bounds validation, Instagram candidate heuristics, and Spotify fallback collectors. """ import inspect from pathlib import Path import sys import numpy as np import pandas as pd import pytest PROJECT_ROOT = Path(__file__).resolve().parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) DATASET_PATH = PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv" @pytest.fixture(scope="module") def dataset(): assert DATASET_PATH.exists(), f"Target dataset not found at {DATASET_PATH}" df = pd.read_csv(DATASET_PATH) return df def test_cohort_size(dataset): """Ensure the cohort size matches the 500-artist specification.""" assert len(dataset) == 500, f"Expected 500 artists, got {len(dataset)}" def test_spotify_streaming_governance(dataset): """Enforce zero-imputation: missing streaming metrics must never be filled with 0.""" if "spotify_monthly_listeners" in dataset.columns: zero_listeners = dataset[dataset["spotify_monthly_listeners"] == 0] assert len(zero_listeners) == 0, f"Found {len(zero_listeners)} artists with imputed 0 Spotify listeners" def test_missing_metrics_remain_nan(dataset): """Verify that unindexed/unmatched records remain genuine NaN values.""" if "spotify_monthly_listeners" in dataset.columns: nan_count = dataset["spotify_monthly_listeners"].isna().sum() assert nan_count > 0, "Expected missing long-tail streaming data to be preserved as NaN" def test_production_ranking_preserved(dataset): """Ensure production YouTube baseline ranks and scores are immutable.""" assert "production_hiffi_rank" in dataset.columns, "production_hiffi_rank missing from schema" assert "production_hiffi_score" in dataset.columns, "production_hiffi_score missing from schema" assert dataset["production_hiffi_rank"].notna().all(), "Found null values in production_hiffi_rank" assert dataset["production_hiffi_score"].notna().all(), "Found null values in production_hiffi_score" def test_score_normalization_bounds(dataset): """Validate that experimental HIFFI-XP scores strictly reside in [0.0, 100.0].""" if "hiffi_xp_score" in dataset.columns: scores = dataset["hiffi_xp_score"].dropna() assert (scores >= 0.0).all(), "Found hiffi_xp_score values less than 0.0" assert (scores <= 100.0).all(), "Found hiffi_xp_score values greater than 100.0" def test_rank_monotonicity(dataset): """Check that rank ordering strictly adheres to score monotonicity (higher score -> lower/better rank).""" if "hiffi_xp_score" in dataset.columns and "hiffi_xp_rank" in dataset.columns: sorted_df = dataset.sort_values(by="hiffi_xp_rank", ascending=True) scores = sorted_df["hiffi_xp_score"].values diffs = np.diff(scores) assert (diffs <= 1e-5).all(), "Found ranking inversions violating score monotonicity" def test_schema_provenance_columns(dataset): """Verify that all required provenance, audit, and status fields exist.""" required_cols = [ "name", "production_hiffi_rank", "production_hiffi_score", "hiffi_xp_score", "hiffi_xp_rank", "spotify_status", "instagram_verification_status", "instagram_confidence_score", "instagram_audit_evidence" ] for col in required_cols: assert col in dataset.columns, f"Required governance column '{col}' missing from dataset" def _invoke_instagram_verifier(verifier, artist_name, candidate_handle, profile_data): """Dynamically adapts arguments to match evaluate_candidate or verify_candidate signature.""" method = None for attr in ["evaluate_candidate", "verify_candidate", "verify_artist", "verify"]: if hasattr(verifier, attr): method = getattr(verifier, attr) break if method is None: raise AttributeError("Could not find candidate verification method on verifier.") sig = inspect.signature(method) params = sig.parameters kwargs = {} # Map artist name for p in ["artist_name", "artist", "name"]: if p in params: kwargs[p] = artist_name break # Map handle for p in ["candidate_handle", "handle", "instagram_handle"]: if p in params: kwargs[p] = candidate_handle break # Map profile dictionary or unpack individual keys if "profile_data" in params: kwargs["profile_data"] = profile_data else: for k, v in profile_data.items(): if k in params: kwargs[k] = v elif k == "followers" and "follower_count" in params: kwargs["follower_count"] = v elif k == "full_name" and "display_name" in params: kwargs["display_name"] = v elif k == "biography" and "bio" in params: kwargs["bio"] = v # Pass remaining positional arguments if signature uses *args or plain positional try: return method(**kwargs) except TypeError: # Fallback to positional invocation return method(artist_name, candidate_handle, profile_data.get("followers", 0)) def test_instagram_verifier_candidate_matching(): """Test algorithmic confidence score calculation for Instagram handle matching.""" from collectors.instagram_verifier import InstagramCandidateVerifier verifier = InstagramCandidateVerifier() res = _invoke_instagram_verifier( verifier, artist_name="Kendrick Lamar", candidate_handle="kendricklamar", profile_data={"followers": 15000000, "biography": "pgLang", "full_name": "Kendrick Lamar"} ) status = res.get("status") or res.get("verification_status") or res.get("instagram_verification_status") conf = res.get("confidence_score") or res.get("instagram_confidence_score", 0.0) evidence = str(res.get("evidence") or res.get("audit_evidence") or res.get("instagram_audit_evidence", "")) assert status == "AUTOMATED_VERIFIED" assert conf >= 0.70 assert "kendricklamar" in evidence.lower() or "exact" in evidence.lower() or len(evidence) > 0 def test_instagram_verifier_suspicious_follower_count(): """Ensure handles with fewer than 10,000 followers are quarantined.""" from collectors.instagram_verifier import InstagramCandidateVerifier verifier = InstagramCandidateVerifier() res = _invoke_instagram_verifier( verifier, artist_name="Drake", candidate_handle="drake", profile_data={"followers": 2500, "biography": "Fan club", "full_name": "Aubrey Drake"} ) status = res.get("status") or res.get("verification_status") or res.get("instagram_verification_status") followers = res.get("followers") or res.get("instagram_followers") assert status in ("UNCERTAIN_NEEDS_REVIEW", "REJECTED_FAN_PAGE", "QUARANTINE_LOW_FOLLOWERS") assert followers is None or status != "AUTOMATED_VERIFIED" def test_spotify_fallback_null_safety(): """Verify Spotify embed fallback safely returns structured null dictionary on unindexed items.""" from collectors.spotify_fallback_collector import SpotifyFallbackCollector fallback = SpotifyFallbackCollector() res = fallback.fetch_artist_fallback("Unknown Artist", "spotify:artist:invalid_or_missing_id_999999") assert res["spotify_monthly_listeners"] is None assert res["spotify_daily_change"] is None assert "UNAVAILABLE" in str(res["spotify_status"]) or "EMBED" in str(res["spotify_status"]) def test_spotify_fallback_mocked_network_error(monkeypatch): """Verify Spotify fallback circuit breaker / exception handling gracefully returns null values.""" from collectors.spotify_fallback_collector import SpotifyFallbackCollector import requests def mock_get(*args, **kwargs): raise requests.exceptions.ConnectionError("Mocked network failure") monkeypatch.setattr(requests.Session, "get", mock_get) fallback = SpotifyFallbackCollector() res = fallback.fetch_artist_fallback("Test Artist", "spotify:artist:0du5cEVh5yTK9QJze8zA0C") assert res["spotify_monthly_listeners"] is None assert res["spotify_status"] in ("FETCH_EXCEPTION", "ERROR_NETWORK_FAILURE", "ERROR_FETCH_EXCEPTION") def test_duplicate_records_handled_safely(dataset): """Ensure all artist names in the cohort are unique with zero duplicate rows.""" duplicate_names = dataset[dataset.duplicated(subset=["name"], keep=False)] assert len(duplicate_names) == 0, f"Found duplicate artist rows in dataset: {duplicate_names['name'].tolist()}" def test_spotify_api_collector_unconfigured(monkeypatch): """Verify that unconfigured credentials safely return null values under zero-imputation.""" from collectors.spotify_api_collector import SpotifyAPICollector monkeypatch.delenv("SPOTIFY_CLIENT_ID", raising=False) monkeypatch.delenv("SPOTIFY_CLIENT_SECRET", raising=False) collector = SpotifyAPICollector(client_id="", client_secret="") res = collector.fetch_artist("test_id") assert res["spotify_status"] == "API_NOT_CONFIGURED" assert res["spotify_followers"] is None assert res["spotify_popularity"] is None def test_spotify_api_collector_empty_id(): """Verify that empty or invalid artist IDs safely yield None without throwing errors.""" from collectors.spotify_api_collector import SpotifyAPICollector collector = SpotifyAPICollector(client_id="dummy", client_secret="dummy") res = collector.fetch_artist("") assert res["spotify_followers"] is None assert res["spotify_status"] in ("INVALID_ID", "API_NOT_CONFIGURED")