返回 last30days-skill
test_meta_ads.py
根目录 / tests / test_meta_ads.py
1 """Tests for the Meta Ad Library source.
2
3 Nothing here spawns a subprocess or touches the network. Fixture shapes mirror
4 live ScrapeCreators payloads captured 2026-09-14 (envelope keys, snapshot
5 fields, the searchResults/results asymmetry, null collation ids); brand names
6 are invented.
7 """
8
9 import datetime
10 from unittest import mock
11
12 import pytest
13
14 from lib import meta_ads
15 from lib.meta_ads import (
16 NO_CANDIDATES,
17 RESOLVED,
18 UNRESOLVED,
19 build_item,
20 dedupe_key,
21 extract_promo_code,
22 has_video,
23 launch_date,
24 names_match,
25 resolve_page,
26 search_meta_ads,
27 )
28
29 FROM_DATE = "2026-08-15"
30 TO_DATE = "2026-09-14"
31 TOKEN = "fake-sc-key"
32
33
34 def ad_row(**over):
35 """One ad row in the live shape (trimmed of media blobs)."""
36 base = {
37 "ad_archive_id": "1000000000000001",
38 "collation_id": "col-1",
39 "collation_count": 3,
40 "page_id": "300000000000001",
41 "page_name": "Brightpan",
42 "is_active": True,
43 "start_date_string": "2026-09-01T07:00:00.000Z",
44 "end_date_string": "2026-09-14T07:00:00.000Z",
45 "publisher_platform": ["FACEBOOK", "INSTAGRAM", "THREADS"],
46 "url": "https://www.facebook.com/ads/library/?id=1000000000000001",
47 "reach_estimate": None,
48 "spend": None,
49 "snapshot": {
50 "body": {"text": "Better mornings start here. Shop the Brightpan kettle."},
51 "title": "",
52 "cta_text": "Shop now",
53 "display_format": "DCO",
54 "link_url": "https://brightpan.example/collections/kettles",
55 "cards": [],
56 "videos": [],
57 },
58 }
59 base.update(over)
60 return base
61
62
63 def envelope(rows, key="searchResults", total=None, cursor=""):
64 return {
65 "success": True,
66 "credits_remaining": 999,
67 "credits_charged": 1,
68 key: rows,
69 "searchResultsCount": len(rows) if total is None else total,
70 "cursor": cursor,
71 }
72
73
74 def company_row(page_id, name, likes=100):
75 return {
76 "page_id": page_id,
77 "name": name,
78 "likes": likes,
79 "category": "Appliances",
80 "verification": "NOT_VERIFIED",
81 "ig_username": "",
82 }
83
84
85 class TestNameMatching:
86 def test_exact_brand_matches(self):
87 assert names_match("Brightpan", "Brightpan")
88
89 def test_product_page_matches_umbrella_topic_by_shared_token(self):
90 # The umbrella brand advertises under product-line pages; containment
91 # in either direction is what resolves them.
92 assert names_match("BrightpanCo", "Brightpan Kitchen")
93
94 def test_short_token_never_matches(self):
95 # "ai" is below the four-character floor, so an unrelated advertiser
96 # that shares only that word must not match.
97 assert not names_match("Vantage AI", "Jasper AI")
98
99 def test_no_synonym_expansion(self):
100 # The shared scoring tokenizer expands "ai" to artificial/intelligence.
101 # Matching must not, or this pair would resolve as the same brand.
102 assert not names_match("Vantage AI", "Artificial Intelligence Labs")
103
104 def test_unrelated_names_do_not_match(self):
105 assert not names_match("Brightpan", "Coastal Realty Group")
106
107 def test_case_and_spacing_are_normalized(self):
108 assert names_match("Bright iQ", "brightiq")
109
110
111 class TestResolvePage:
112 def test_umbrella_brand_resolves_dominant_product_page(self):
113 rows = (
114 [ad_row(page_id="1", page_name="Brightpan Kitchen") for _ in range(14)]
115 + [ad_row(page_id="2", page_name="Brightpan Beauty") for _ in range(5)]
116 + [ad_row(page_id="3", page_name="Brightpan Home") for _ in range(4)]
117 + [ad_row(page_id="9", page_name="Unrelated Deals Co") for _ in range(20)]
118 )
119 page, runner_ups, top, _strength = resolve_page("BrightpanCo", rows)
120 assert page["name"] == "Brightpan Kitchen"
121 assert page["id"] == "1"
122 assert runner_ups == ["Brightpan Beauty", "Brightpan Home"]
123 assert top == ""
124
125 def test_exact_name_beats_busier_partial_match(self):
126 # A reseller running more ads than the brand must not claim the topic.
127 rows = [ad_row(page_id="1", page_name="Brightpan") for _ in range(11)] + [
128 ad_row(page_id="2", page_name="Brightpan Outlet") for _ in range(18)
129 ]
130 page, _runner_ups, _top, strength = resolve_page("Brightpan", rows)
131 assert page["name"] == "Brightpan"
132 assert page["id"] == "1"
133 assert strength == meta_ads.MATCH_EXACT
134
135 def test_no_match_returns_top_unmatched_candidate(self):
136 rows = [ad_row(page_id="9", page_name="Jasper AI") for _ in range(30)]
137 page, runner_ups, top, _strength = resolve_page("Vantage AI", rows)
138 assert page is None
139 assert runner_ups == []
140 assert top == "Jasper AI"
141
142 def test_no_rows_returns_no_candidate_name(self):
143 page, runner_ups, top, _strength = resolve_page("Brightpan", [])
144 assert page is None
145 assert runner_ups == []
146 assert top == ""
147
148 def test_rows_without_page_id_are_ignored(self):
149 page, _runner_ups, top, _strength = resolve_page(
150 "Brightpan", [ad_row(page_id="", page_name="Brightpan")]
151 )
152 assert page is None
153 assert top == ""
154
155
156 class TestRowFields:
157 def test_launch_date_from_iso_string(self):
158 assert launch_date(ad_row()) == "2026-09-01"
159
160 def test_launch_date_falls_back_to_epoch(self):
161 epoch = int(
162 datetime.datetime(2026, 9, 1, tzinfo=datetime.timezone.utc).timestamp()
163 )
164 row = ad_row(start_date_string="", start_date=epoch)
165 assert launch_date(row) == "2026-09-01"
166
167 def test_launch_date_missing_is_none(self):
168 assert launch_date(ad_row(start_date_string="", start_date=None)) is None
169
170 def test_dedupe_key_prefers_collation(self):
171 assert dedupe_key(ad_row(collation_id="col-9")) == "collation:col-9"
172
173 def test_dedupe_key_falls_back_to_archive_id(self):
174 row = ad_row(collation_id=None, ad_archive_id="777")
175 assert dedupe_key(row) == "ad:777"
176
177 def test_has_video_detects_snapshot_videos(self):
178 row = ad_row()
179 row["snapshot"]["videos"] = [{"video_hd_url": "https://cdn.example/v.mp4"}]
180 assert has_video(row)
181
182 def test_has_video_detects_card_video(self):
183 row = ad_row()
184 row["snapshot"]["cards"] = [{"video_sd_url": "https://cdn.example/v.mp4"}]
185 assert has_video(row)
186
187 def test_has_video_false_for_image_ad(self):
188 assert not has_video(ad_row())
189
190 def test_promo_code_found_after_code_keyword(self):
191 assert extract_promo_code("Take 30% off sitewide. Use code SUMMER30.") == "SUMMER30"
192
193 def test_promo_code_ignores_model_numbers(self):
194 assert extract_promo_code("The Brightpan E-325 is here.") is None
195
196 def test_promo_code_ignores_lowercase_words_after_code(self):
197 assert extract_promo_code("Our code of conduct is published.") is None
198
199 def test_build_item_carries_every_rendered_field(self):
200 item = build_item(ad_row(), {"id": "300000000000001", "name": "Brightpan"})
201 assert item["title"].startswith("Better mornings")
202 assert item["date"] == "2026-09-01"
203 assert item["cta"] == "Shop now"
204 assert item["display_format"] == "DCO"
205 assert item["landing_url"] == "https://brightpan.example/collections/kettles"
206 assert item["placements"] == ["FACEBOOK", "INSTAGRAM", "THREADS"]
207 assert item["variants"] == 3
208 assert item["advertiser"] == "Brightpan"
209 assert item["url"].endswith("id=1000000000000001")
210
211 def test_build_item_uses_card_link_when_snapshot_link_absent(self):
212 row = ad_row()
213 row["snapshot"]["link_url"] = ""
214 row["snapshot"]["cards"] = [{"link_url": "https://brightpan.example/p/1"}]
215 item = build_item(row, {"id": "1", "name": "Brightpan"})
216 assert item["landing_url"] == "https://brightpan.example/p/1"
217
218 def test_build_item_synthesizes_permalink_when_url_missing(self):
219 item = build_item(ad_row(url=""), {"id": "1", "name": "Brightpan"})
220 assert item["url"] == "https://www.facebook.com/ads/library/?id=1000000000000001"
221
222
223 class TestSearchMetaAds:
224 def _run(self, responses, **kwargs):
225 """Drive the lane with a scripted sequence of http.get returns."""
226 calls = []
227
228 def fake_get(url, **call_kwargs):
229 calls.append((url, call_kwargs.get("params") or {}))
230 nxt = responses.pop(0)
231 if isinstance(nxt, Exception):
232 raise nxt
233 return nxt
234
235 with mock.patch("lib.meta_ads.http.get", side_effect=fake_get):
236 result = search_meta_ads(
237 kwargs.pop("topic", "Brightpan"),
238 FROM_DATE,
239 TO_DATE,
240 token=kwargs.pop("token", TOKEN),
241 **kwargs,
242 )
243 return result, calls
244
245 def test_missing_token_makes_no_calls(self):
246 with mock.patch("lib.meta_ads.http.get") as get:
247 result = search_meta_ads("Brightpan", FROM_DATE, TO_DATE, token="")
248 get.assert_not_called()
249 assert result["ads"] == []
250 assert result["page"] is None
251
252 def test_happy_path_resolves_and_classifies(self):
253 discovery = envelope([ad_row() for _ in range(11)])
254 window = envelope(
255 [
256 ad_row(ad_archive_id="1", collation_id="a", start_date_string="2026-09-01"),
257 ad_row(ad_archive_id="2", collation_id="b", start_date_string="2026-08-20"),
258 ad_row(ad_archive_id="3", collation_id="c", start_date_string="2026-04-03"),
259 ],
260 key="results",
261 )
262 result, calls = self._run([discovery, window])
263 assert result["page"]["name"] == "Brightpan"
264 assert [item["date"] for item in result["ads"]] == ["2026-09-01", "2026-08-20"]
265 tally = result["tally"]
266 assert tally["resolution"] == RESOLVED
267 assert tally["launched_in_window"] == 2
268 assert tally["still_running"] == 1
269 assert tally["promo_codes"] == []
270 assert tally["placements"] == ["FACEBOOK", "INSTAGRAM", "THREADS"]
271 assert calls[0][0] == meta_ads.SEARCH_ADS_URL
272 assert calls[1][0] == meta_ads.COMPANY_ADS_URL
273
274 def test_window_fetch_requests_all_statuses(self):
275 # Without this the endpoint's ACTIVE default hides creatives that
276 # launched inside the window and already ended.
277 result, calls = self._run(
278 [envelope([ad_row()]), envelope([ad_row()], key="results")]
279 )
280 assert calls[1][1]["status"] == meta_ads.ENRICHMENT_STATUS
281 assert calls[1][1]["start_date"] == FROM_DATE
282 assert calls[1][1]["end_date"] == TO_DATE
283 assert result["tally"]["launched_in_window"] == 1
284
285 def test_ended_in_window_creative_is_kept_and_flagged(self):
286 window = envelope(
287 [ad_row(is_active=False, end_date_string="2026-09-05T07:00:00.000Z")],
288 key="results",
289 )
290 result, _calls = self._run([envelope([ad_row()]), window])
291 assert len(result["ads"]) == 1
292 assert result["ads"][0]["is_active"] is False
293 assert result["ads"][0]["ended_on"] == "2026-09-05"
294
295 def test_company_search_fallback_resolves(self):
296 discovery = envelope([ad_row(page_id="9", page_name="Unrelated Deals Co")])
297 companies = envelope(
298 [company_row("55", "Brightpan"), company_row("56", "Other Co")],
299 key="results",
300 )
301 window = envelope([ad_row()], key="results")
302 result, calls = self._run([discovery, companies, window])
303 assert result["page"]["id"] == "55"
304 assert calls[1][0] == meta_ads.SEARCH_COMPANIES_URL
305 assert calls[2][1]["pageId"] == "55"
306
307 def test_unresolved_names_top_candidate_and_skips_enrichment(self):
308 discovery = envelope(
309 [ad_row(page_id="9", page_name="Jasper AI") for _ in range(30)]
310 )
311 companies = envelope([company_row("7", "Jasper AI")], key="results")
312 result, calls = self._run([discovery, companies], topic="Vantage AI")
313 assert result["ads"] == []
314 assert result["page"] is None
315 assert result["tally"]["resolution"] == UNRESOLVED
316 assert result["tally"]["top_candidate"] == "Jasper AI"
317 assert all(call[0] != meta_ads.COMPANY_ADS_URL for call in calls)
318
319 def test_zero_candidates_is_its_own_state(self):
320 result, calls = self._run(
321 [envelope([]), envelope([], key="results")], topic="Nonexistent Brand"
322 )
323 assert result["tally"]["resolution"] == NO_CANDIDATES
324 assert result["tally"]["top_candidate"] == ""
325 assert len(calls) == 2
326
327 def test_page_override_skips_resolution(self):
328 window = envelope([ad_row()], key="results")
329 result, calls = self._run([window], page_override="300000000000001")
330 assert len(calls) == 1
331 assert calls[0][0] == meta_ads.COMPANY_ADS_URL
332 assert result["page"]["id"] == "300000000000001"
333
334 def test_variants_of_one_creative_collapse(self):
335 window = envelope(
336 [
337 ad_row(ad_archive_id="1", collation_id="same", collation_count=4),
338 ad_row(ad_archive_id="2", collation_id="same", collation_count=4),
339 ],
340 key="results",
341 )
342 result, _calls = self._run([envelope([ad_row()]), window])
343 assert len(result["ads"]) == 1
344 assert result["ads"][0]["variants"] == 4
345
346 def test_null_collation_ids_stay_distinct(self):
347 window = envelope(
348 [
349 ad_row(ad_archive_id="1", collation_id=None),
350 ad_row(ad_archive_id="2", collation_id=None),
351 ],
352 key="results",
353 )
354 result, _calls = self._run([envelope([ad_row()]), window])
355 assert len(result["ads"]) == 2
356
357 def test_pagination_stops_at_depth_cap_and_flags_more(self):
358 discovery = envelope([ad_row()])
359 pages = [
360 envelope([ad_row(ad_archive_id=str(i), collation_id=f"c{i}")],
361 key="results", total=222, cursor=f"cur{i}")
362 for i in range(5)
363 ]
364 result, calls = self._run([discovery] + pages, depth="default")
365 window_calls = [c for c in calls if c[0] == meta_ads.COMPANY_ADS_URL]
366 assert len(window_calls) == 2 # default depth cap
367 assert result["tally"]["cursor_remaining"] is True
368 assert result["tally"]["endpoint_total"] == 222
369
370 def test_pagination_stops_when_cursor_repeats(self):
371 discovery = envelope([ad_row()])
372 repeated = envelope(
373 [ad_row(ad_archive_id="1", collation_id="c1")], key="results", cursor="same"
374 )
375 result, calls = self._run([discovery, repeated, repeated, repeated], depth="deep")
376 window_calls = [c for c in calls if c[0] == meta_ads.COMPANY_ADS_URL]
377 assert len(window_calls) == 2
378 assert result["tally"]["cursor_remaining"] is False
379
380 def test_pagination_stops_on_empty_cursor(self):
381 discovery = envelope([ad_row()])
382 window = envelope([ad_row()], key="results", cursor="")
383 _result, calls = self._run([discovery, window], depth="deep")
384 assert len([c for c in calls if c[0] == meta_ads.COMPANY_ADS_URL]) == 1
385
386 @pytest.mark.parametrize("status", [401, 402, 403, 429])
387 def test_fatal_status_stops_the_lane(self, status):
388 err = meta_ads.http.HTTPError(f"boom {status}", status_code=status)
389 result, calls = self._run([err])
390 assert len(calls) == 1
391 assert result["ads"] == []
392 assert str(status) in result["error"]
393
394 def test_fatal_status_during_enrichment_reports_error(self):
395 err = meta_ads.http.HTTPError("rate limited", status_code=429)
396 result, calls = self._run([envelope([ad_row()]), err])
397 assert len(calls) == 2
398 assert "429" in result["error"]
399 assert result["ads"] == []
400
401 def test_empty_success_response_is_not_an_error(self):
402 result, _calls = self._run([envelope([]), envelope([], key="results")])
403 assert "error" not in result
404 assert result["tally"]["resolution"] == NO_CANDIDATES
405
406 def test_transcripts_cover_newest_videos_across_pages(self):
407 discovery = envelope([ad_row()])
408 older_video = ad_row(
409 ad_archive_id="old", collation_id="old", start_date_string="2026-08-20"
410 )
411 older_video["snapshot"]["videos"] = [{"video_hd_url": "https://cdn.example/a.mp4"}]
412 newer_video = ad_row(
413 ad_archive_id="new", collation_id="new", start_date_string="2026-09-10"
414 )
415 newer_video["snapshot"]["videos"] = [{"video_hd_url": "https://cdn.example/b.mp4"}]
416 page_one = envelope([older_video], key="results", cursor="c1")
417 page_two = envelope([newer_video], key="results", cursor="")
418 transcript = {"transcript_available": True, "transcript": "Say what you need to say."}
419 result, calls = self._run(
420 [discovery, page_one, page_two, transcript, transcript], depth="default"
421 )
422 transcript_calls = [c for c in calls if c[0] == meta_ads.AD_TRANSCRIPT_URL]
423 # Newest first: the page-two creative is transcribed before the older one.
424 assert transcript_calls[0][1]["id"] == "new"
425 assert result["tally"]["transcribed"] == 2
426 assert result["ads"][0]["transcript"].startswith("Say what")
427
428 def test_unavailable_transcript_is_not_an_error(self):
429 discovery = envelope([ad_row()])
430 video = ad_row()
431 video["snapshot"]["videos"] = [{"video_hd_url": "https://cdn.example/a.mp4"}]
432 window = envelope([video], key="results")
433 result, _calls = self._run(
434 [discovery, window, {"transcript_available": False, "transcript": None}]
435 )
436 assert "error" not in result
437 assert result["tally"]["transcribed"] == 0
438 assert result["ads"][0]["transcript"] == ""
439
440 def test_quick_depth_pulls_no_transcripts(self):
441 discovery = envelope([ad_row()])
442 video = ad_row()
443 video["snapshot"]["videos"] = [{"video_hd_url": "https://cdn.example/a.mp4"}]
444 _result, calls = self._run(
445 [discovery, envelope([video], key="results")], depth="quick"
446 )
447 assert all(c[0] != meta_ads.AD_TRANSCRIPT_URL for c in calls)
448
449 def test_rate_limit_retries_are_disabled_on_every_call(self):
450 # The shared client retries a 429 twice by default, which would both
451 # contradict the no-further-calls contract and burn the lane budget.
452 _result, _calls = self._run(
453 [envelope([ad_row()]), envelope([ad_row()], key="results")]
454 )
455 with mock.patch("lib.meta_ads.http.get") as get:
456 get.return_value = envelope([])
457 search_meta_ads("Brightpan", FROM_DATE, TO_DATE, token=TOKEN)
458 assert get.call_args.kwargs["max_429_retries"] == 0
459 assert get.call_args.kwargs["deadline_monotonic"] is not None
460
461 def test_exhausted_budget_keeps_what_was_fetched_and_reports_partial(self):
462 # Running out of time must not discard creatives already in hand: thin
463 # coverage caused by our own clock would otherwise read as a finding
464 # about how little the advertiser is running.
465 discovery = envelope([ad_row()])
466 window = envelope([ad_row()], key="results", cursor="more")
467 ticks = {"n": 0}
468 real_monotonic = meta_ads.time.monotonic
469
470 def creeping_clock():
471 ticks["n"] += 1
472 # Jump past the lane budget once the first window page is in.
473 return real_monotonic() + (0 if ticks["n"] < 6 else 10_000)
474
475 with mock.patch("lib.meta_ads.time.monotonic", side_effect=creeping_clock):
476 with mock.patch("lib.meta_ads.http.get", side_effect=[discovery, window]):
477 result = search_meta_ads(
478 "Brightpan", FROM_DATE, TO_DATE, token=TOKEN, depth="deep"
479 )
480 assert result.get("partial") is True
481 assert "budget" in result["error"]
482 assert len(result["ads"]) == 1
483 assert result["page"]["name"] == "Brightpan"
484 assert result["tally"]["cursor_remaining"] is True
485
486 def test_country_override_is_passed_through(self):
487 _result, calls = self._run(
488 [envelope([ad_row()]), envelope([ad_row()], key="results")], country="GB"
489 )
490 assert calls[0][1]["country"] == "GB"
491 assert calls[1][1]["country"] == "GB"
492
493
494 class TestWindowBoundaries:
495 def test_creative_launched_on_window_edges_is_included(self):
496 window = envelope(
497 [
498 ad_row(ad_archive_id="1", collation_id="a", start_date_string=FROM_DATE),
499 ad_row(ad_archive_id="2", collation_id="b", start_date_string=TO_DATE),
500 ],
501 key="results",
502 )
503 with mock.patch(
504 "lib.meta_ads.http.get", side_effect=[envelope([ad_row()]), window]
505 ):
506 result = search_meta_ads("Brightpan", FROM_DATE, TO_DATE, token=TOKEN)
507 assert result["tally"]["launched_in_window"] == 2
508
509 def test_undated_creative_counts_as_still_running(self):
510 window = envelope(
511 [ad_row(start_date_string="", start_date=None)], key="results"
512 )
513 with mock.patch(
514 "lib.meta_ads.http.get", side_effect=[envelope([ad_row()]), window]
515 ):
516 result = search_meta_ads("Brightpan", FROM_DATE, TO_DATE, token=TOKEN)
517 assert result["ads"] == []
518 assert result["tally"]["still_running"] == 1
519
520
521 class TestTransientVersusFatal:
522 """R11: only credential/account statuses discard the lane's work."""
523
524 def _drive(self, responses, depth="default"):
525 def fake_get(url, **kwargs):
526 nxt = responses.pop(0)
527 if isinstance(nxt, Exception):
528 raise nxt
529 return nxt
530
531 with mock.patch("lib.meta_ads.http.get", side_effect=fake_get):
532 return search_meta_ads(
533 "Brightpan", FROM_DATE, TO_DATE, token=TOKEN, depth=depth
534 )
535
536 def test_transient_error_mid_pagination_keeps_fetched_creatives(self):
537 # A 500 on page two must not throw away page one: those creatives were
538 # already paid for and are real evidence.
539 page_one = envelope([ad_row()], key="results", cursor="more")
540 boom = meta_ads.http.HTTPError("upstream hiccup", status_code=500)
541 result = self._drive([envelope([ad_row()]), page_one, boom], depth="deep")
542 assert len(result["ads"]) == 1
543 assert result["page"]["name"] == "Brightpan"
544 assert result.get("partial") is True
545 assert "500" in result["error"]
546
547 def test_transient_error_during_transcripts_keeps_creatives(self):
548 video = ad_row()
549 video["snapshot"]["videos"] = [{"video_hd_url": "https://cdn.example/a.mp4"}]
550 boom = meta_ads.http.HTTPError("transcoder down", status_code=503)
551 result = self._drive(
552 [envelope([ad_row()]), envelope([video], key="results"), boom]
553 )
554 assert len(result["ads"]) == 1
555 assert result.get("partial") is True
556
557 def test_fatal_status_mid_enrichment_keeps_paid_creatives(self):
558 # A rate limit or expired credential means "stop calling", not "the
559 # pages that already returned 200 were wrong". Discarding them throws
560 # away evidence the user already paid for.
561 page_one = envelope([ad_row()], key="results", cursor="more")
562 boom = meta_ads.http.HTTPError("rate limited", status_code=429)
563 result = self._drive([envelope([ad_row()]), page_one, boom], depth="deep")
564 assert len(result["ads"]) == 1
565 assert result["page"]["name"] == "Brightpan"
566 assert result.get("partial") is True
567 assert "429" in result["error"]
568
569 def test_fatal_status_during_discovery_has_nothing_to_keep(self):
570 boom = meta_ads.http.HTTPError("rate limited", status_code=429)
571 result = self._drive([boom])
572 assert result["ads"] == []
573 assert result["page"] is None
574 assert "429" in result["error"]
575
576
577 class TestShortBrandNames:
578 """A brand whose every word is under the token floor must still resolve.
579
580 The floor exists to stop a shared short word ("AI") from matching every
581 advertiser that carries it. Applied without an exact-identity escape it
582 also makes an initialism brand unresolvable against its own page, which
583 fails the feature silently for a whole class of well-known names.
584 """
585
586 def test_initialism_brand_matches_its_own_page_exactly(self):
587 assert names_match("KLM", "KLM")
588 assert names_match("BMW", "bmw")
589
590 def test_initialism_brand_still_rejects_an_unrelated_page(self):
591 assert not names_match("KLM", "Kitchen Lighting Market")
592
593 def test_initialism_resolves_through_the_exact_tier(self):
594 rows = [ad_row(page_id="1", page_name="KLM") for _ in range(2)] + [
595 ad_row(page_id="2", page_name="Unrelated Deals Co") for _ in range(30)
596 ]
597 page, _runner_ups, _top, strength = resolve_page("KLM", rows)
598 assert page["name"] == "KLM"
599 assert strength == meta_ads.MATCH_EXACT
600
601
602 class TestResolutionStrength:
603 def test_shared_whole_token_outranks_mere_containment(self):
604 # "Brightpan Kitchen" shares the whole token; the containment-only
605 # page must not win on ad volume alone.
606 rows = [ad_row(page_id="1", page_name="Brightpan Kitchen") for _ in range(2)] + [
607 ad_row(page_id="2", page_name="Superbrightpanel Co") for _ in range(40)
608 ]
609 page, _runner_ups, _top, strength = resolve_page("Brightpan", rows)
610 assert page["name"] == "Brightpan Kitchen"
611 assert strength == meta_ads.MATCH_TOKEN
612
613 def test_containment_tier_is_reported_as_such(self):
614 rows = [ad_row(page_id="1", page_name="Brightpan Kitchen") for _ in range(3)]
615 _page, _runner_ups, _top, strength = resolve_page("BrightpanCo", rows)
616 assert strength == meta_ads.MATCH_CONTAINED
617
618 def test_transcript_cap_bounds_paid_requests_not_successes(self):
619 # Every transcript call costs a credit whether or not one comes back,
620 # so an upstream with no transcripts must not keep the lane calling.
621 videos = []
622 for i in range(10):
623 row = ad_row(ad_archive_id=str(i), collation_id=f"c{i}")
624 row["snapshot"]["videos"] = [{"video_hd_url": "https://cdn.example/v.mp4"}]
625 videos.append(row)
626 unavailable = {"transcript_available": False, "transcript": None}
627 responses = [envelope([ad_row()]), envelope(videos, key="results")] + [
628 unavailable
629 ] * 10
630 calls = []
631
632 def fake_get(url, **kwargs):
633 calls.append(url)
634 return responses.pop(0)
635
636 with mock.patch("lib.meta_ads.http.get", side_effect=fake_get):
637 result = search_meta_ads(
638 "Brightpan", FROM_DATE, TO_DATE, token=TOKEN, depth="default"
639 )
640 transcript_calls = [c for c in calls if c == meta_ads.AD_TRANSCRIPT_URL]
641 assert len(transcript_calls) == 3 # the default-depth cap, not 10
642 assert result["tally"]["transcribed"] == 0
643
644 def test_default_depth_run_stays_inside_its_documented_credit_ceiling(self):
645 videos = []
646 for i in range(10):
647 row = ad_row(ad_archive_id=str(i), collation_id=f"c{i}")
648 row["snapshot"]["videos"] = [{"video_hd_url": "https://cdn.example/v.mp4"}]
649 videos.append(row)
650 responses = [
651 envelope([ad_row(page_id="9", page_name="Unrelated Deals Co")]),
652 envelope([company_row("55", "Brightpan")], key="results"),
653 envelope(videos, key="results", cursor="more"),
654 envelope(videos, key="results", cursor="more"),
655 ] + [{"transcript_available": True, "transcript": "hello"}] * 5
656 calls = []
657
658 def fake_get(url, **kwargs):
659 calls.append(url)
660 return responses.pop(0)
661
662 with mock.patch("lib.meta_ads.http.get", side_effect=fake_get):
663 search_meta_ads("Brightpan", FROM_DATE, TO_DATE, token=TOKEN, depth="default")
664 # 1 discovery + 1 company fallback + 2 pages + 3 transcripts = 7
665 assert len(calls) <= 7
666
667
668 class TestGenericTokens:
669 """A word several advertisers share is a category, not an identity."""
670
671 def test_category_word_does_not_resolve_an_unrelated_advertiser(self):
672 rows = (
673 [ad_row(page_id="1", page_name="Acme Kitchen") for _ in range(3)]
674 + [ad_row(page_id="2", page_name="Kitchen World") for _ in range(40)]
675 + [ad_row(page_id="3", page_name="Kitchen Depot") for _ in range(20)]
676 )
677 page, _runner_ups, _top, _strength = resolve_page("Acme Kitchen", rows)
678 assert page["name"] == "Acme Kitchen"
679
680 def test_a_word_only_one_advertiser_uses_still_identifies(self):
681 rows = [ad_row(page_id="1", page_name="Brightpan Supply") for _ in range(2)] + [
682 ad_row(page_id="2", page_name="Unrelated Deals Co") for _ in range(40)
683 ]
684 page, _runner_ups, _top, strength = resolve_page("Brightpan Supply news", rows)
685 assert page["name"] == "Brightpan Supply"
686 assert strength == meta_ads.MATCH_TOKEN
687
688 def test_shared_category_alone_resolves_nothing(self):
689 rows = [ad_row(page_id="1", page_name="Kitchen World") for _ in range(30)] + [
690 ad_row(page_id="2", page_name="Kitchen Depot") for _ in range(20)
691 ]
692 page, _runner_ups, top, _strength = resolve_page("Acme Kitchen", rows)
693 assert page is None
694 assert top == "Kitchen World"
695
696
697 class TestHeadTokenIdentity:
698 """A multi-word topic carries its identity in the leading word.
699
700 Counting how many advertisers share a word cannot carry this: a result set
701 holding one lookalike makes its category word look perfectly distinctive.
702 """
703
704 def test_lone_category_lookalike_resolves_nothing(self):
705 rows = [ad_row(page_id="9", page_name="Kitchen World") for _ in range(30)]
706 page, _runner_ups, top, _strength = resolve_page("Acme Kitchen", rows)
707 assert page is None
708 assert top == "Kitchen World"
709
710 def test_brand_page_wins_over_a_busier_category_lookalike(self):
711 rows = [ad_row(page_id="1", page_name="Acme Kitchen") for _ in range(3)] + [
712 ad_row(page_id="9", page_name="Kitchen World") for _ in range(40)
713 ]
714 page, _runner_ups, _top, _strength = resolve_page("Acme Kitchen", rows)
715 assert page["name"] == "Acme Kitchen"
716
717 def test_a_page_sharing_only_the_brand_word_fails_closed(self):
718 # "Acme Supply" may or may not belong to the brand behind "Acme
719 # Kitchen"; the name alone cannot say. Resolving it would risk
720 # attributing another firm's ads, so the lane declines and names the
721 # candidate instead, which the page override recovers in one flag.
722 rows = [ad_row(page_id="1", page_name="Acme Supply") for _ in range(3)]
723 page, _runner_ups, top, _strength = resolve_page("Acme Kitchen", rows)
724 assert page is None
725 assert top == "Acme Supply"
726
727 def test_single_word_topic_keeps_its_umbrella_reach(self):
728 # Nothing to strip, so the whole name is the head and product-line
729 # pages still resolve.
730 rows = [ad_row(page_id="1", page_name="Brightpan Kitchen") for _ in range(14)]
731 page, _runner_ups, _top, _strength = resolve_page("BrightpanCo", rows)
732 assert page["name"] == "Brightpan Kitchen"
733
734
735 class TestDescriptorLeadingTopics:
736 """A research topic does not always open with the brand."""
737
738 def test_intent_modifier_before_the_brand_still_resolves_it(self):
739 rows = [ad_row(page_id="1", page_name="Acme Grills") for _ in range(5)]
740 page, _runner_ups, _top, _strength = resolve_page("best Acme grills", rows)
741 assert page["name"] == "Acme Grills"
742
743 @pytest.mark.parametrize(
744 "topic,page_name",
745 [
746 ("best Acme grills", "Acme Grills"),
747 ("latest Acme cookware", "Acme Cookware"),
748 ("top Acme deals", "Acme Deals"),
749 ],
750 )
751 def test_common_descriptors_do_not_become_the_identity(self, topic, page_name):
752 rows = [ad_row(page_id="1", page_name=page_name) for _ in range(5)]
753 page, _runner_ups, _top, _strength = resolve_page(topic, rows)
754 assert page["name"] == page_name
755
756 def test_a_category_lookalike_is_still_rejected_under_a_descriptor(self):
757 # "grill" appears inside "grills", but it is the category word, not
758 # the brand, so it must not carry the whole topic with it.
759 rows = [ad_row(page_id="9", page_name="Grill World") for _ in range(50)]
760 page, _runner_ups, _top, _strength = resolve_page("best Acme grills", rows)
761 assert page is None
762
763
764 class TestBrandPositionIndependence:
765 """The brand does not always lead the topic."""
766
767 @pytest.mark.parametrize(
768 "topic,page",
769 [
770 ("Kitchen by Crate and Barrel", "Crate and Barrel"),
771 ("smart home Acme", "Acme Home"),
772 ("cookware from Acme Supply", "Acme Supply"),
773 ],
774 )
775 def test_a_topic_that_spells_out_the_name_resolves_it(self, topic, page):
776 # The topic names the company outright, so where in the phrase that
777 # name sits cannot decide the match.
778 rows = [ad_row(page_id="1", page_name=page) for _ in range(6)]
779 resolved, _runner_ups, _top, _strength = resolve_page(topic, rows)
780 assert resolved["name"] == page
781
782 def test_a_lookalike_is_still_rejected_because_the_topic_omits_a_word(self):
783 # "Kitchen World" carries "world", which a topic about "Acme Kitchen"
784 # never mentions, so the topic is not naming this company.
785 rows = [ad_row(page_id="9", page_name="Kitchen World") for _ in range(30)]
786 page, _runner_ups, _top, _strength = resolve_page("Acme Kitchen", rows)
787 assert page is None
788
789
790 class TestPartialNamesCannotClaimATopic:
791 """A fragment of the topic is not an advertiser's whole name."""
792
793 def test_a_page_named_only_the_category_word_is_rejected(self):
794 # Trivially "covered" by the topic, but it accounts for one word of it
795 # and none of the brand.
796 rows = [ad_row(page_id="9", page_name="Kitchen") for _ in range(30)]
797 page, _runner_ups, _top, _strength = resolve_page("Acme Kitchen", rows)
798 assert page is None
799
800 def test_a_page_named_the_whole_single_word_topic_still_resolves(self):
801 rows = [ad_row(page_id="1", page_name="Acme Co") for _ in range(4)]
802 page, _runner_ups, _top, _strength = resolve_page("Acme", rows)
803 assert page["name"] == "Acme Co"
804
805 def test_coverage_needs_more_than_one_topic_word(self):
806 rows = [ad_row(page_id="9", page_name="Grills") for _ in range(30)]
807 page, _runner_ups, _top, _strength = resolve_page("best Acme grills", rows)
808 assert page is None
809
810
811 class TestCoverageBeatsVolume:
812 def test_the_page_matching_more_of_the_topic_wins(self):
813 # Two pages share the brand word; the one that also matches the rest
814 # of the topic is the better answer however much the other spends.
815 rows = [ad_row(page_id="1", page_name="Acme Kitchen Co") for _ in range(2)] + [
816 ad_row(page_id="9", page_name="Acme AI") for _ in range(80)
817 ]
818 page, _runner_ups, _top, _strength = resolve_page("Acme Kitchen", rows)
819 assert page["name"] == "Acme Kitchen Co"
820
821
822 class TestWholeNameRequired:
823 """Greptile round 7: a partial name must not stand in for a whole one."""
824
825 def test_a_page_with_a_word_the_topic_never_mentions_is_rejected(self):
826 # "Acme AI" and the brand behind "Acme Kitchen" share one word and
827 # nothing else; "ai" appears nowhere in the topic.
828 rows = [ad_row(page_id="9", page_name="Acme AI") for _ in range(80)]
829 page, _runner_ups, _top, _strength = resolve_page("Acme Kitchen", rows)
830 assert page is None
831
832 def test_short_words_of_the_page_name_are_counted(self):
833 # The page name's short word is part of its identity, so ignoring it
834 # would let the name look fully covered when it is not.
835 rows = [ad_row(page_id="9", page_name="Acme AI") for _ in range(5)]
836 page, _runner_ups, _top, _strength = resolve_page("Acme AI", rows)
837 assert page["name"] == "Acme AI"
838
839 def test_word_boundaries_are_respected_in_coverage(self):
840 # "Cart Wheel" is not named by "Acme Cartwheel" merely because its
841 # letters appear inside a longer word.
842 rows = [ad_row(page_id="9", page_name="Cart Wheel") for _ in range(30)]
843 page, _runner_ups, _top, _strength = resolve_page("Acme Cartwheel", rows)
844 assert page is None
845
845 lines PYTHON