| 1 | """Normalization, identity, and ranking behavior for Meta Ad Library items.""" |
| 2 | |
| 3 | from lib import fusion, normalize, planner, rerank, schema, signals |
| 4 | |
| 5 | FROM_DATE = "2026-08-15" |
| 6 | TO_DATE = "2026-09-14" |
| 7 | |
| 8 | |
| 9 | def raw_ad(**over): |
| 10 | base = { |
| 11 | "id": "1000000000000001", |
| 12 | "title": "Better mornings start here.", |
| 13 | "text": "Better mornings start here. Use code SUMMER30 on the Brightpan kettle.", |
| 14 | "url": "https://www.facebook.com/ads/library/?id=1000000000000001", |
| 15 | "date": "2026-09-01", |
| 16 | "advertiser": "Brightpan", |
| 17 | "page_id": "300000000000001", |
| 18 | "is_active": True, |
| 19 | "ended_on": None, |
| 20 | "display_format": "DCO", |
| 21 | "cta": "Shop now", |
| 22 | "landing_url": "https://brightpan.example/collections/kettles", |
| 23 | "placements": ["FACEBOOK", "INSTAGRAM"], |
| 24 | "promo_code": "SUMMER30", |
| 25 | "variants": 3, |
| 26 | "has_video": False, |
| 27 | "transcript": "", |
| 28 | } |
| 29 | base.update(over) |
| 30 | return base |
| 31 | |
| 32 | |
| 33 | def normalize_one(**over): |
| 34 | items = normalize.normalize_source_items( |
| 35 | "meta_ads", [raw_ad(**over)], FROM_DATE, TO_DATE |
| 36 | ) |
| 37 | assert len(items) == 1 |
| 38 | return items[0] |
| 39 | |
| 40 | |
| 41 | class TestNormalizer: |
| 42 | def test_source_is_registered(self): |
| 43 | # A missing dispatch entry raises "Unsupported source" at runtime. |
| 44 | assert normalize.normalize_source_items("meta_ads", [], FROM_DATE, TO_DATE) == [] |
| 45 | |
| 46 | def test_identity_is_the_ad_library_permalink(self): |
| 47 | item = normalize_one() |
| 48 | assert item.url.endswith("id=1000000000000001") |
| 49 | assert item.metadata["landing_url"] == "https://brightpan.example/collections/kettles" |
| 50 | |
| 51 | def test_launch_date_is_the_published_date(self): |
| 52 | assert normalize_one().published_at == "2026-09-01" |
| 53 | |
| 54 | def test_creative_fields_ride_in_metadata(self): |
| 55 | meta = normalize_one().metadata |
| 56 | assert meta["advertiser"] == "Brightpan" |
| 57 | assert meta["cta"] == "Shop now" |
| 58 | assert meta["promo_code"] == "SUMMER30" |
| 59 | assert meta["display_format"] == "DCO" |
| 60 | assert meta["placements"] == ["FACEBOOK", "INSTAGRAM"] |
| 61 | assert meta["variants"] == 3 |
| 62 | |
| 63 | def test_variant_count_is_the_engagement_proxy(self): |
| 64 | assert normalize_one().engagement == {"variants": 3} |
| 65 | |
| 66 | def test_transcript_rides_on_the_item(self): |
| 67 | item = normalize_one(has_video=True, transcript="Stop making boring drinks.") |
| 68 | assert item.metadata["transcript_snippet"].startswith("Stop making") |
| 69 | assert "Stop making boring drinks." in item.snippet |
| 70 | |
| 71 | def test_items_are_grounding_exempt(self): |
| 72 | assert normalize_one().metadata["grounding_exempt"] is True |
| 73 | |
| 74 | def test_pre_window_creative_is_filtered_out(self): |
| 75 | # The adapter already classifies, but the shared window filter is the |
| 76 | # backstop and must agree with it rather than drop everything. |
| 77 | items = normalize.normalize_source_items( |
| 78 | "meta_ads", [raw_ad(date="2026-04-03")], FROM_DATE, TO_DATE |
| 79 | ) |
| 80 | assert items == [] |
| 81 | |
| 82 | def test_missing_advertiser_still_yields_a_reason(self): |
| 83 | assert normalize_one(advertiser="").why_relevant == "Running paid creative" |
| 84 | |
| 85 | |
| 86 | class TestIdentityThroughFusion: |
| 87 | def test_creatives_sharing_a_landing_page_stay_distinct(self): |
| 88 | # Ten creatives for one product commonly share a landing URL. Keying |
| 89 | # identity on it would collapse the campaign into a single candidate. |
| 90 | first = normalize_one(id="1", url="https://www.facebook.com/ads/library/?id=1") |
| 91 | second = normalize_one(id="2", url="https://www.facebook.com/ads/library/?id=2") |
| 92 | assert fusion.candidate_key(first) != fusion.candidate_key(second) |
| 93 | |
| 94 | def test_same_creative_from_two_streams_shares_one_key(self): |
| 95 | one = normalize_one() |
| 96 | two = normalize_one(variants=4) |
| 97 | assert fusion.candidate_key(one) == fusion.candidate_key(two) |
| 98 | |
| 99 | |
| 100 | class TestRankingRegistries: |
| 101 | def test_grounding_exemption_skips_the_entity_miss_demotion(self): |
| 102 | # Ad copy that never names the brand is the normal case, so the |
| 103 | # entity-miss demotion must not reach these items. |
| 104 | item = normalize_one(text="Stop making boring drinks.", title="Boring drinks") |
| 105 | candidate = schema.Candidate( |
| 106 | candidate_id="c1", |
| 107 | item_id=item.item_id, |
| 108 | source="meta_ads", |
| 109 | title=item.title, |
| 110 | url=item.url, |
| 111 | snippet=item.snippet, |
| 112 | subquery_labels=[], |
| 113 | native_ranks={}, |
| 114 | local_relevance=0.1, |
| 115 | freshness=1, |
| 116 | engagement=None, |
| 117 | source_quality=signals.source_quality("meta_ads"), |
| 118 | rrf_score=0.0, |
| 119 | source_items=[item], |
| 120 | ) |
| 121 | assert rerank._is_grounding_exempt(candidate) is True |
| 122 | |
| 123 | def test_source_quality_is_registered(self): |
| 124 | assert signals.source_quality("meta_ads") == 0.72 |
| 125 | |
| 126 | def test_engagement_weights_use_variants(self): |
| 127 | assert signals.ENGAGEMENT_WEIGHTS["meta_ads"] == [("variants", 1.0)] |
| 128 | |
| 129 | def test_planner_capabilities_are_pinned(self): |
| 130 | assert planner.SOURCE_CAPABILITIES["meta_ads"] == { |
| 131 | "reference", |
| 132 | "company_signal", |
| 133 | "product_signal", |
| 134 | } |
| 135 | |
| 136 | |
| 137 | class TestActivityWording: |
| 138 | """Ended-in-window creatives are kept on purpose, so say so accurately.""" |
| 139 | |
| 140 | def test_running_creative_reads_as_running(self): |
| 141 | assert normalize_one(is_active=True).why_relevant.startswith( |
| 142 | "Running paid creative" |
| 143 | ) |
| 144 | |
| 145 | def test_ended_creative_is_not_called_active(self): |
| 146 | item = normalize_one(is_active=False, ended_on="2026-09-05") |
| 147 | assert "Running" not in item.why_relevant |
| 148 | assert "2026-09-05" in item.why_relevant |
| 149 | |
| 150 | def test_ended_creative_without_a_date_still_reads_as_ended(self): |
| 151 | item = normalize_one(is_active=False, ended_on=None) |
| 152 | assert "since ended" in item.why_relevant |
| 153 | |
| 154 | def test_activity_state_is_preserved_in_metadata(self): |
| 155 | meta = normalize_one(is_active=False, ended_on="2026-09-05").metadata |
| 156 | assert meta["is_active"] is False |
| 157 | assert meta["ended_on"] == "2026-09-05" |
| 158 |