Files
hiffi-artist-intelligence/test_multiplatform_ranking.py
T

230 lines
9.6 KiB
Python

"""
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")