feat(tiktok): emit graceful ErrorItem for blocked/empty listings

Profile and search feeds are trust-gated: an anonymous headless session
gets an empty item_list (profile) or no results XHR (search), while
hashtag feeds load. A zero-item listing now yields one honest ErrorItem
(errorCode="no_items") instead of vanishing silently, and ErrorItems are
excluded from billing so a blocked target is surfaced but never charged.
This commit is contained in:
CREDO23 2026-07-08 23:14:50 +02:00
parent f731d371ab
commit ba375ea3ee
5 changed files with 96 additions and 14 deletions

View file

@ -9,6 +9,7 @@ from app.capabilities.tiktok.scrape.schemas import (
MAX_TIKTOK_ITEMS,
MAX_TIKTOK_SOURCES,
ScrapeInput,
ScrapeOutput,
)
pytestmark = pytest.mark.unit
@ -43,3 +44,15 @@ def test_rejects_more_sources_than_the_cap():
too_many = [f"tag{i}" for i in range(MAX_TIKTOK_SOURCES + 1)]
with pytest.raises(ValidationError):
ScrapeInput(hashtags=too_many)
def test_error_items_are_not_billed():
# Real videos count; ErrorItems (blocked/empty targets) are surfaced free.
out = ScrapeOutput(
items=[
{"id": "1", "webVideoUrl": "https://tiktok.com/@a/video/1"},
{"errorCode": "no_items", "input": "nasa", "error": "blocked"},
]
)
assert len(out.items) == 2
assert out.billable_units == 1

View file

@ -95,3 +95,18 @@ async def test_listing_dedupes_then_caps_per_target():
TikTokScrapeInput(hashtags=["x"], resultsPerPage=2), fetch_listing=fake_listing
)
assert [i["id"] for i in items] == ["1", "2"]
async def test_empty_listing_emits_error_item():
# A trust-gated/empty feed (0 videos) must surface one honest ErrorItem,
# tagged with errorCode, rather than vanishing silently.
async def fake_listing(_url: str, _count: int) -> list[dict]:
return []
items = await scrape_tiktok(
TikTokScrapeInput(profiles=["nasa"], resultsPerPage=5),
fetch_listing=fake_listing,
)
assert len(items) == 1
assert items[0]["errorCode"] == "no_items"
assert items[0]["input"] == "nasa"