sync: update from private repo (ddadfd71)
CI / build-and-test (push) Waiting to run

This commit is contained in:
oss-sync
2026-07-08 23:35:00 +00:00
parent b1292e34b2
commit 77ee3bc426
187 changed files with 19918 additions and 10938 deletions
+98
View File
@@ -29,6 +29,7 @@ def ns(**kw):
check("ref: bare id", xa.parse_tweet_ref("20") == "20")
check("ref: x.com url", xa.parse_tweet_ref("https://x.com/jack/status/20") == "20")
check("ref: twitter.com url", xa.parse_tweet_ref("https://twitter.com/jack/statuses/20?s=1") == "20")
check("ref: article url", xa.parse_tweet_ref("https://x.com/jack/article/2072471529242407210") == "2072471529242407210")
check("ref: junk -> None", xa.parse_tweet_ref("not a tweet") is None)
check("ref: empty -> None", xa.parse_tweet_ref("") is None)
@@ -162,6 +163,103 @@ check("home: who-to-follow user skipped", xa.parse_home_timeline(
{"entryId": "cursor-top", "content": {"cursorType": "Top"}},
]}]}}}}, limit=10) == [])
# ── find_tweet_result_in_detail / extract_article (X 長文記事) ──
# TweetDetail レスポンスの実構造を模した fixture (2026-07 実データの縮約)
article_result = {
"rest_id": "500",
"legacy": {"full_text": "https://t.co/xxxx", "created_at": "Wed Oct 10 20:19:24 +0000 2018",
"favorite_count": 1, "retweet_count": 0, "reply_count": 0},
"article": {"article_results": {"result": {
"rest_id": "499",
"title": "記事タイトル",
"preview_text": "プレビュー文",
"plain_text": "本文です。" * 10,
"metadata": {"first_published_at_secs": 1782950691},
"cover_media": {"media_info": {"original_img_url": "https://pbs.twimg.com/media/COVER.jpg"}},
}}},
}
detail_payload = {"data": {"threaded_conversation_with_injections_v2": {"instructions": [
{"type": "TimelineAddEntries", "entries": [
{"entryId": "tweet-1", "content": {"itemContent": {"tweet_results": {"result": {"rest_id": "1", "legacy": {}}}}}},
{"entryId": "tweet-500", "content": {"itemContent": {"tweet_results": {"result": article_result}}}},
]},
]}}}
found = xa.find_tweet_result_in_detail(detail_payload, "500")
check("detail: focal tweet found", found is not None and found.get("rest_id") == "500")
check("detail: missing id -> None", xa.find_tweet_result_in_detail(detail_payload, "999") is None)
check("detail: empty payload -> None", xa.find_tweet_result_in_detail({}, "500") is None)
# visibility wrapper 越しでも見つかる
wrapped_payload = {"data": {"entries": [{"tweet_results": {"result": {
"__typename": "TweetWithVisibilityResults", "tweet": article_result}}}]}
}
found_w = xa.find_tweet_result_in_detail(wrapped_payload, "500")
check("detail: visibility wrapper unwrapped", found_w is not None and found_w.get("rest_id") == "500")
art = xa.extract_article(article_result)
check("article: title", art["title"] == "記事タイトル")
check("article: previewText", art["previewText"] == "プレビュー文")
check("article: plainText", art["plainText"] == "本文です。" * 10)
check("article: publishedAtISO", art["publishedAtISO"] == "2026-07-02T00:04:51+00:00")
check("article: coverImageUrl", art["coverImageUrl"] == "https://pbs.twimg.com/media/COVER.jpg")
check("article: not truncated", "plainTextTruncated" not in art)
# cap: full_text=False では ARTICLE_TEXT_CAP で切る
long_art = {"article": {"article_results": {"result": {
"title": "t", "plain_text": "" * (xa.ARTICLE_TEXT_CAP + 100)}}}}
capped = xa.extract_article(long_art)
check("article: capped length", len(capped["plainText"]) == xa.ARTICLE_TEXT_CAP)
check("article: truncated flag", capped.get("plainTextTruncated") is True)
full = xa.extract_article(long_art, full_text=True)
check("article: full_text lifts cap", len(full["plainText"]) == xa.ARTICLE_TEXT_CAP + 100)
check("article: full not truncated", "plainTextTruncated" not in full)
# 記事なしツイート → None / 壊れた形 → None
check("article: non-article -> None", xa.extract_article(result_legacy) is None)
check("article: junk -> None", xa.extract_article({"article": {"article_results": {"result": "?"}}}) is None)
check("article: non-dict -> None", xa.extract_article(None) is None)
# plain_text 無し (fieldToggles 未対応時) でも title/preview は返す
no_body = {"article": {"article_results": {"result": {"title": "t2", "preview_text": "p2"}}}}
nb = xa.extract_article(no_body)
check("article: no body still returns meta", nb["title"] == "t2" and "plainText" not in nb)
# ── extract_article_media (記事内の埋め込み画像・動画) ──
img_entity = {"media_id": "111", "media_info": {
"__typename": "ApiImage",
"original_img_url": "https://pbs.twimg.com/media/IMG1.jpg",
"original_img_width": 1983, "original_img_height": 793}}
vid_entity = {"media_id": "222", "media_info": {
"__typename": "ApiVideo",
"duration_millis": 7658,
"preview_image": {"original_img_url": "https://pbs.twimg.com/amplify_video_thumb/222/img/P.jpg"},
"variants": [
{"bit_rate": 2176000, "content_type": "video/mp4", "url": "https://video.twimg.com/a/720.mp4"},
{"content_type": "application/x-mpegURL", "url": "https://video.twimg.com/a/pl.m3u8"},
]}}
gif_entity = {"media_id": "333", "media_info": {
"__typename": "ApiGif",
"preview_image": {"original_img_url": "https://pbs.twimg.com/tweet_video_thumb/G.jpg"},
"variants": [{"content_type": "video/mp4", "url": "https://video.twimg.com/tweet_video/g.mp4"}]}}
unknown_entity = {"media_id": "444", "media_info": {"__typename": "ApiAudioSpace"}}
art_with_media = {"article": {"article_results": {"result": {
"title": "t", "media_entities": [img_entity, vid_entity, gif_entity, unknown_entity]}}}}
am = xa.extract_article_media(art_with_media)
check("amedia: count (unknown skipped)", len(am) == 3)
check("amedia: photo", am[0] == {"type": "photo", "url": "https://pbs.twimg.com/media/IMG1.jpg"})
check("amedia: video poster", am[1]["type"] == "video" and am[1]["url"].endswith("P.jpg"))
check("amedia: video variant mapped", am[1]["variants"][0] ==
{"url": "https://video.twimg.com/a/720.mp4", "bitrate": 2176000, "contentType": "video/mp4"})
check("amedia: m3u8 variant kept", am[1]["variants"][1]["contentType"] == "application/x-mpegURL")
check("amedia: gif -> animated_gif", am[2]["type"] == "animated_gif" and am[2]["variants"][0]["url"].endswith("g.mp4"))
check("amedia: non-article -> []", xa.extract_article_media(result_legacy) == [])
check("amedia: no media_entities -> []", xa.extract_article_media(no_body) == [])
check("amedia: non-dict -> []", xa.extract_article_media(None) == [])
# 件数 cap: ARTICLE_MEDIA_CAP を超えたら切る
many = {"article": {"article_results": {"result": {"media_entities": [img_entity] * (xa.ARTICLE_MEDIA_CAP + 5)}}}}
check("amedia: capped", len(xa.extract_article_media(many)) == xa.ARTICLE_MEDIA_CAP)
# ── emit (optional, needs PyYAML) ──
try:
import io