返回 last30days-skill
test_meta_ads_normalize.py
根目录 / tests / test_meta_ads_normalize.py
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
158 lines PYTHON