more branch scrape exploration.
This commit is contained in:
1 parent
c637c393b8
commit
a2de38f187
17 files changed
+452
-3
No files matched your search
@@ -1,206 +0,0 @@
|
||||
"""Test helpers for the Twitter full-conversation fetcher.
|
||||
|
||||
Owns the canonical test-truth constants that production
|
||||
``twitter.fetch_thread`` mirrors (the equality is asserted at
|
||||
import time once production exists). Also owns the offline fakes
|
||||
used by every test that exercises the fetcher without touching
|
||||
the network: ``FakeTransport``, ``RecordingSleeper``, and
|
||||
``FakeTransaction``.
|
||||
|
||||
This module deliberately stays a *test-only* module: it must not
|
||||
import ``twitter.fetch_thread`` (production does not exist yet
|
||||
when the helpers are first created) and must not read cookies,
|
||||
open sockets, or sleep for real.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
# --- canonical test-truth constants (production mirrors these) ---
|
||||
|
||||
|
||||
KNOWN_ROOT_ID: str = "1650678968255913985"
|
||||
KNOWN_TIP_ID: str = "1651282559287042048"
|
||||
|
||||
EXPECTED_16: frozenset[str] = frozenset({
|
||||
"1650678968255913985", # kenpex OP
|
||||
"1650685805776732160", # abductee_org -> OP
|
||||
"1650915682136240129", # streleav -> OP
|
||||
"1651056827839180800", # wadetb -> OP
|
||||
"1651086935467917312", # ChristerEricson -> OP
|
||||
"1651168030557077504", # SebAaltonen -> OP
|
||||
"1651253961524142081", # kenpex -> wadetb
|
||||
"1651254727160795137", # kenpex -> ChristerEricson
|
||||
"1651268028795961344", # NOTimothyLottes -> kenpex
|
||||
"1651282559287042048", # wvo (the input tip)
|
||||
"1651295755293036544", # ErikHaliewicz -> OP
|
||||
"1651508233243095043", # npatsiouras -> kenpex Christer branch
|
||||
"1651510424649744385", # ChristerEricson -> npatsiouras
|
||||
"1651511316010663936", # ChristerEricson -> self
|
||||
"1651579576089300992", # JaceCear -> OP
|
||||
"1651636988267888641", # kenpex -> JaceCear
|
||||
})
|
||||
|
||||
# --- fixture loader ---
|
||||
|
||||
|
||||
FIXTURES_DIR: Path = (
|
||||
Path(__file__).resolve().parent / "fixtures" / "fetch_thread"
|
||||
)
|
||||
|
||||
|
||||
def load_fixture(name: str) -> str:
|
||||
"""Return the raw UTF-8 text of a sanitized fixture by basename.
|
||||
|
||||
The caller may pass ``"tip_tweet_detail"`` or
|
||||
``"tip_tweet_detail.json"``; either form resolves to the same
|
||||
file under :data:`FIXTURES_DIR`.
|
||||
"""
|
||||
filename = name if name.endswith(".json") else f"{name}.json"
|
||||
return (FIXTURES_DIR / filename).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def load_json_fixture(name: str) -> object:
|
||||
"""Parse the JSON fixture with ``name`` (with or without the
|
||||
``.json`` suffix) and return the deserialised value.
|
||||
|
||||
Return type is ``object`` because the wire shape varies: pages,
|
||||
cursor lists, error envelopes, and homepage HTML fragments all
|
||||
round-trip through here.
|
||||
"""
|
||||
return json.loads(load_fixture(name))
|
||||
|
||||
|
||||
# --- fakes ---
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecordingSleeper:
|
||||
"""Records each requested sleep duration without blocking.
|
||||
|
||||
The fetcher's :class:`RequestGovernor` calls ``sleep(min_delay)``
|
||||
after the first request of every role; tests assert the exact
|
||||
sequence and value via ``self.durations``.
|
||||
"""
|
||||
|
||||
durations: list[float] = field(default_factory=list)
|
||||
|
||||
def sleep(self, seconds: float) -> None:
|
||||
"""Append the requested duration; return immediately."""
|
||||
self.durations.append(seconds)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeTransport:
|
||||
"""In-memory transport. Issues no HTTP requests.
|
||||
|
||||
The fetcher's production transport implements
|
||||
``fetch(role, url) -> (status, body)``. The fake mirrors that
|
||||
signature so production and tests agree on the call surface.
|
||||
|
||||
Attributes:
|
||||
responses: Role-keyed canned responses. Each value is a
|
||||
``(status_code, body_text)`` pair. Missing keys raise
|
||||
``AssertionError`` so tests fail loudly when a new role
|
||||
is introduced without a corresponding fixture.
|
||||
calls: Per-``fetch`` call record. Each entry is a dict with
|
||||
``"role"`` and ``"url"`` keys, in call order.
|
||||
"""
|
||||
|
||||
responses: dict[str, tuple[int, str]] = field(default_factory=dict)
|
||||
calls: list[dict[str, str]] = field(default_factory=list)
|
||||
_transaction: object | None = None
|
||||
|
||||
def set_transaction(self, transaction: object) -> None:
|
||||
"""Store a fake transaction for later inspection.
|
||||
|
||||
The real ``RequestsSessionTransport`` calls this once the
|
||||
two bootstrap requests succeed. Tests pass a
|
||||
:class:`FakeTransaction` (or any object) and assert the
|
||||
order by reading ``self._transaction`` later.
|
||||
"""
|
||||
self._transaction = transaction
|
||||
|
||||
def fetch(self, role: str, url: str) -> tuple[int, str]:
|
||||
"""Return the canned response for ``role`` and record the call.
|
||||
|
||||
Raises ``AssertionError`` if ``role`` is not keyed in
|
||||
:attr:`responses`; this is the offline equivalent of a 5xx
|
||||
when the live provider returns nothing for an unknown role.
|
||||
"""
|
||||
if role not in self.responses:
|
||||
raise AssertionError(
|
||||
f"FakeTransport: no response registered for role {role!r}"
|
||||
)
|
||||
self.calls.append({"role": role, "url": url})
|
||||
return self.responses[role]
|
||||
|
||||
|
||||
class FakeTransaction:
|
||||
"""Fake MIT ``XClientTransaction``-shaped object for tests.
|
||||
|
||||
The real constructor signature (verified offline) is
|
||||
``ClientTransaction(home_page_response, ondemand_file_response,
|
||||
random_keyword=None, random_number=None)``. Tests capture both
|
||||
arguments verbatim so they can assert the fetcher passed the
|
||||
parsed ``BeautifulSoup`` and raw JS body to the factory in the
|
||||
right order. The ``generate_transaction_id`` method is directly
|
||||
defined on this class (delegating to the module-level
|
||||
:func:`generate_transaction_id` helper) so the fake exactly
|
||||
matches the real call surface.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home_soup: object,
|
||||
js_body: object,
|
||||
**kwargs: object,
|
||||
) -> None:
|
||||
self.home_soup = home_soup
|
||||
self.js_body = js_body
|
||||
self.kwargs = kwargs
|
||||
|
||||
def generate_transaction_id(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
**kwargs: object,
|
||||
) -> str:
|
||||
"""Return a fixed test transaction id."""
|
||||
return generate_transaction_id(method, path, **kwargs)
|
||||
|
||||
|
||||
_FIXED_TRANSACTION_ID: str = "fixed-test-transaction-id"
|
||||
|
||||
|
||||
def generate_transaction_id(
|
||||
method: str,
|
||||
path: str,
|
||||
**kwargs: object,
|
||||
) -> str:
|
||||
"""Return a fixed transaction id for offline tests.
|
||||
|
||||
The id value is irrelevant to behaviour; tests assert that the
|
||||
production code calls ``generate_transaction_id(method, path,
|
||||
**kwargs)`` and that the result reaches the request header.
|
||||
"""
|
||||
return _FIXED_TRANSACTION_ID
|
||||
|
||||
|
||||
def make_fake_transaction(
|
||||
home_soup: object,
|
||||
js_body: object,
|
||||
**kwargs: object,
|
||||
) -> FakeTransaction:
|
||||
"""Construct a :class:`FakeTransaction` with the production
|
||||
call signature so a test's ``transaction_factory`` matches the
|
||||
real ``ClientTransaction(home_page_response, ondemand_file_response)``
|
||||
two-positional-arg constructor.
|
||||
|
||||
Exposed as a free function (rather than baking the signature
|
||||
into ``FakeTransaction``) so tests can pass it as a
|
||||
``transaction_factory`` callable to the fetcher.
|
||||
"""
|
||||
return FakeTransaction(home_soup, js_body, **kwargs)
|
||||
Whitespace-only changes.
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "depth-1-tweet",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1700000000000000001",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "8100000001",
|
||||
"core": {
|
||||
"name": "fixture-depth-1-author",
|
||||
"screen_name": "fixture-depth-1-handle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1700000000000000001",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:01:00 +0000 2026",
|
||||
"full_text": "fixture-depth-1-text",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "depth-1-cursor-forward",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineCursor",
|
||||
"cursorType": "ShowMore",
|
||||
"value": "fixture-depth-2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "depth-2-tweet",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1700000000000000002",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "8100000002",
|
||||
"core": {
|
||||
"name": "fixture-depth-2-author",
|
||||
"screen_name": "fixture-depth-2-handle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1700000000000000002",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:02:00 +0000 2026",
|
||||
"full_text": "fixture-depth-2-text",
|
||||
"in_reply_to_status_id_str": "1700000000000000001",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "depth-2-cursor-forward",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineCursor",
|
||||
"cursorType": "Bottom",
|
||||
"value": "fixture-depth-3"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "depth-3-tweet",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1700000000000000003",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "8100000003",
|
||||
"core": {
|
||||
"name": "fixture-depth-3-author",
|
||||
"screen_name": "fixture-depth-3-handle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1700000000000000003",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:03:00 +0000 2026",
|
||||
"full_text": "fixture-depth-3-text",
|
||||
"in_reply_to_status_id_str": "1700000000000000002",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "depth-3-cursor-echo-current",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineCursor",
|
||||
"cursorType": "Bottom",
|
||||
"value": "fixture-depth-3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "depth-3-cursor-forward",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineCursor",
|
||||
"cursorType": "ShowMoreThreads",
|
||||
"value": "fixture-depth-4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "depth-4-tweet",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1700000000000000004",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "8100000004",
|
||||
"core": {
|
||||
"name": "fixture-depth-4-author",
|
||||
"screen_name": "fixture-depth-4-handle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1700000000000000004",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:04:00 +0000 2026",
|
||||
"full_text": "fixture-depth-4-text",
|
||||
"in_reply_to_status_id_str": "1700000000000000003",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "depth-4-cursor-forward",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineCursor",
|
||||
"cursorType": "ShowMore",
|
||||
"value": "fixture-depth-5"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "depth-5-tweet",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1700000000000000005",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "8100000005",
|
||||
"core": {
|
||||
"name": "fixture-depth-5-author",
|
||||
"screen_name": "fixture-depth-5-handle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1700000000000000005",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:05:00 +0000 2026",
|
||||
"full_text": "fixture-depth-5-text",
|
||||
"in_reply_to_status_id_str": "1700000000000000004",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "depth-5-cursor-forward",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineCursor",
|
||||
"cursorType": "Bottom",
|
||||
"value": "fixture-depth-6"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "depth-6-tweet",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1700000000000000006",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "8100000006",
|
||||
"core": {
|
||||
"name": "fixture-depth-6-author",
|
||||
"screen_name": "fixture-depth-6-handle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1700000000000000006",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:06:00 +0000 2026",
|
||||
"full_text": "fixture-depth-6-text",
|
||||
"in_reply_to_status_id_str": "1700000000000000005",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "depth-6-cursor-forward",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineCursor",
|
||||
"cursorType": "ShowMoreThreads",
|
||||
"value": "fixture-depth-7"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "depth-7-tweet",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1700000000000000007",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "8100000007",
|
||||
"core": {
|
||||
"name": "fixture-depth-7-author",
|
||||
"screen_name": "fixture-depth-7-handle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1700000000000000007",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:07:00 +0000 2026",
|
||||
"full_text": "fixture-depth-7-text",
|
||||
"in_reply_to_status_id_str": "1700000000000000006",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "continuation-tweet-lottes",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651268028795961344",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "444444444",
|
||||
"core": {
|
||||
"name": "fixture-author-lottes-continuation",
|
||||
"screen_name": "fixture-handle-lottes-continuation"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651268028795961344",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:03:00 +0000 2026",
|
||||
"full_text": "fixture-text-lottes-continuation",
|
||||
"in_reply_to_status_id_str": "1651253961524142081",
|
||||
"in_reply_to_user_id_str": "333333333",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "continuation-tweet-tip",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651282559287042048",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "555555555",
|
||||
"core": {
|
||||
"name": "fixture-author-tip-continuation",
|
||||
"screen_name": "fixture-handle-tip-continuation"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651282559287042048",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:04:00 +0000 2026",
|
||||
"full_text": "fixture-text-tip-continuation",
|
||||
"in_reply_to_status_id_str": "1651268028795961344",
|
||||
"in_reply_to_user_id_str": "444444444",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "tweet-root",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1650678968255913985",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "955913985",
|
||||
"core": {
|
||||
"name": "fixture-author-root-page",
|
||||
"screen_name": "fixture-handle-root-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1650678968255913985",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-root-page",
|
||||
"in_reply_to_status_id_str": null,
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 5,
|
||||
"retweet_count": 6,
|
||||
"favorite_count": 7
|
||||
},
|
||||
"views": {
|
||||
"count": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "TimelineAddToModule",
|
||||
"moduleItems": [
|
||||
{
|
||||
"entryId": "mod-01-abductee_org-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1650685805776732160",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "976732160",
|
||||
"core": {
|
||||
"name": "fixture-author-abductee_org-page",
|
||||
"screen_name": "fixture-handle-abductee-org-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1650685805776732160",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-abductee-org-page",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-02-streleav-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1650915682136240129",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "936240129",
|
||||
"core": {
|
||||
"name": "fixture-author-streleav-page",
|
||||
"screen_name": "fixture-handle-streleav-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1650915682136240129",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-streleav-page",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-03-wadetb-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651056827839180800",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "939180800",
|
||||
"core": {
|
||||
"name": "fixture-author-wadetb-page",
|
||||
"screen_name": "fixture-handle-wadetb-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651056827839180800",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-wadetb-page",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-04-Christer-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651086935467917312",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "967917312",
|
||||
"core": {
|
||||
"name": "fixture-author-Christer-page",
|
||||
"screen_name": "fixture-handle-Christer-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651086935467917312",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-Christer-page",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-05-Seb-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651168030557077504",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "957077504",
|
||||
"core": {
|
||||
"name": "fixture-author-Seb-page",
|
||||
"screen_name": "fixture-handle-Seb-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651168030557077504",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-Seb-page",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-06-kenpex-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651253961524142081",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "924142081",
|
||||
"core": {
|
||||
"name": "fixture-author-kenpex-page",
|
||||
"screen_name": "fixture-handle-kenpex-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651253961524142081",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-kenpex-to-wadetb-page",
|
||||
"in_reply_to_status_id_str": "1651056827839180800",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-07-kenpex-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651254727160795137",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "960795137",
|
||||
"core": {
|
||||
"name": "fixture-author-kenpex-page",
|
||||
"screen_name": "fixture-handle-kenpex-page-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651254727160795137",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-kenpex-to-Christer-page",
|
||||
"in_reply_to_status_id_str": "1651086935467917312",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-08-Erik-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651295755293036544",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "993036544",
|
||||
"core": {
|
||||
"name": "fixture-author-Erik-page",
|
||||
"screen_name": "fixture-handle-Erik-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651295755293036544",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-Erik-page",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-09-npatsiouras-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651508233243095043",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "943095043",
|
||||
"core": {
|
||||
"name": "fixture-author-npatsiouras-page",
|
||||
"screen_name": "fixture-handle-npatsiouras-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651508233243095043",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-npatsiouras-page",
|
||||
"in_reply_to_status_id_str": "1651254727160795137",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-10-Christer-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651510424649744385",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "949744385",
|
||||
"core": {
|
||||
"name": "fixture-author-Christer-page",
|
||||
"screen_name": "fixture-handle-Christer-page-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651510424649744385",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-Christer-to-npatsiouras-page",
|
||||
"in_reply_to_status_id_str": "1651508233243095043",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-11-Christer-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651511316010663936",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "910663936",
|
||||
"core": {
|
||||
"name": "fixture-author-Christer-page",
|
||||
"screen_name": "fixture-handle-Christer-page-3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651511316010663936",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-Christer-self-page",
|
||||
"in_reply_to_status_id_str": "1651510424649744385",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-12-Jace-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651579576089300992",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "989300992",
|
||||
"core": {
|
||||
"name": "fixture-author-Jace-page",
|
||||
"screen_name": "fixture-handle-Jace-page"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651579576089300992",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-Jace-page",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-13-kenpex-page",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651636988267888641",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "967888641",
|
||||
"core": {
|
||||
"name": "fixture-author-kenpex-page",
|
||||
"screen_name": "fixture-handle-kenpex-page-3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651636988267888641",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-kenpex-to-Jace-page",
|
||||
"in_reply_to_status_id_str": "1651579576089300992",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"threaded_conversation_with_injections_v2": {
|
||||
"instructions": [
|
||||
{
|
||||
"type": "TimelineAddEntries",
|
||||
"entries": [
|
||||
{
|
||||
"entryId": "tweet-root",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineItem",
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1650678968255913985",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "111111111",
|
||||
"core": {
|
||||
"name": "fixture-author-root",
|
||||
"screen_name": "fixture-handle-root"
|
||||
},
|
||||
"legacy": {
|
||||
"name": "fixture-author-root-legacy-should-be-ignored",
|
||||
"screen_name": "fixture-handle-root-legacy-should-be-ignored"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1650678968255913985",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:00:00 +0000 2026",
|
||||
"full_text": "fixture-text-root-legacy",
|
||||
"in_reply_to_status_id_str": null,
|
||||
"in_reply_to_user_id_str": null,
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 1,
|
||||
"retweet_count": 2,
|
||||
"favorite_count": 3
|
||||
},
|
||||
"views": {"count": 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "conversationthread-1",
|
||||
"content": {
|
||||
"entryType": "TimelineTimelineModule",
|
||||
"items": [
|
||||
{
|
||||
"entryId": "mod-tweet-wadetb",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651056827839180800",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "222222222",
|
||||
"core": {
|
||||
"name": "fixture-author-wadetb",
|
||||
"screen_name": "fixture-handle-wadetb"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651056827839180800",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:01:00 +0000 2026",
|
||||
"full_text": "fixture-text-wadetb-full",
|
||||
"in_reply_to_status_id_str": "1650678968255913985",
|
||||
"in_reply_to_user_id_str": "111111111",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0,
|
||||
"extended_entities": {
|
||||
"media": [
|
||||
{
|
||||
"media_url_https": "https://pbs.twimg.com/media/fixture-photo-wadetb.jpg",
|
||||
"type": "photo"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-tweet-kenpex",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651253961524142081",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "333333333",
|
||||
"core": {
|
||||
"name": "fixture-author-kenpex",
|
||||
"screen_name": "fixture-handle-kenpex"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"note_tweet": {
|
||||
"note_tweet_results": {
|
||||
"result": {
|
||||
"text": "fixture-note-tweet-text-kenpex-override"
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651253961524142081",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:02:00 +0000 2026",
|
||||
"full_text": "fixture-text-kenpex-legacy-should-be-ignored",
|
||||
"in_reply_to_status_id_str": "1651056827839180800",
|
||||
"in_reply_to_user_id_str": "222222222",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-tweet-lottes",
|
||||
"itemType": "TimelineTimelineItem",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "TweetWithVisibilityResults",
|
||||
"tweet": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651268028795961344",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "444444444",
|
||||
"core": {
|
||||
"name": "fixture-author-lottes",
|
||||
"screen_name": "fixture-handle-lottes"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651268028795961344",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:03:00 +0000 2026",
|
||||
"full_text": "fixture-text-lottes",
|
||||
"in_reply_to_status_id_str": "1651253961524142081",
|
||||
"in_reply_to_user_id_str": "333333333",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0,
|
||||
"extended_entities": {
|
||||
"media": [
|
||||
{
|
||||
"media_url_https": "https://pbs.twimg.com/media/fixture-photo-lottes-poster.jpg",
|
||||
"type": "video",
|
||||
"video_info": {
|
||||
"variants": [
|
||||
{
|
||||
"content_type": "video/mp4",
|
||||
"url": "https://video.twimg.com/fixture-video-low.mp4",
|
||||
"bitrate": 320000
|
||||
},
|
||||
{
|
||||
"content_type": "video/mp4",
|
||||
"url": "https://video.twimg.com/fixture-video-med.mp4",
|
||||
"bitrate": 832000
|
||||
},
|
||||
{
|
||||
"content_type": "video/mp4",
|
||||
"url": "https://video.twimg.com/fixture-video-high.mp4",
|
||||
"bitrate": 1280000
|
||||
},
|
||||
{
|
||||
"content_type": "application/x-mpegURL",
|
||||
"url": "https://video.twimg.com/fixture-video.m3u8",
|
||||
"bitrate": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-tweet-tip",
|
||||
"itemType": "TimelineTweet",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"tweet_results": {
|
||||
"result": {
|
||||
"__typename": "Tweet",
|
||||
"rest_id": "1651282559287042048",
|
||||
"core": {
|
||||
"user_results": {
|
||||
"result": {
|
||||
"__typename": "User",
|
||||
"id_str": "555555555",
|
||||
"core": {
|
||||
"name": "fixture-author-tip",
|
||||
"screen_name": "fixture-handle-tip"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"id_str": "1651282559287042048",
|
||||
"conversation_id_str": "1650678968255913985",
|
||||
"created_at": "Mon Apr 26 12:04:00 +0000 2026",
|
||||
"full_text": "fixture-text-tip",
|
||||
"in_reply_to_status_id_str": "1651268028795961344",
|
||||
"in_reply_to_user_id_str": "444444444",
|
||||
"quoted_status_id_str": null,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0
|
||||
},
|
||||
"views": {"count": "1234"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"entryId": "mod-cursor-top",
|
||||
"itemType": "TimelineTimelineCursor",
|
||||
"item": {
|
||||
"itemContent": {
|
||||
"cursorType": "Top",
|
||||
"value": "fixture-top-cursor-token-should-be-ignored"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,18 +2,51 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
from contextlib import redirect_stdout
|
||||
from dataclasses import asdict, replace
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
from twitter import tw as tw_module
|
||||
from twitter.models import PostData, PostMetrics, ThreadData, load_thread
|
||||
from twitter.tw import (
|
||||
_capture_ids,
|
||||
_copy_selected_media,
|
||||
_gallery_base_args,
|
||||
_merge_branch_posts,
|
||||
_merge_gallery_files,
|
||||
_select_branch_capture,
|
||||
_validate_capture_ids,
|
||||
build_parser,
|
||||
cmd_add_branch,
|
||||
)
|
||||
|
||||
|
||||
def make_post(post_id: str, reply_to_id: str | None) -> PostData:
|
||||
return PostData(
|
||||
post_id=post_id,
|
||||
author=post_id,
|
||||
handle=post_id,
|
||||
text=post_id,
|
||||
timestamp=f"2026-01-01 00:00:{int(post_id) % 60:02d}",
|
||||
media_urls=(),
|
||||
reply_to_id=reply_to_id,
|
||||
quote_of_id=None,
|
||||
metrics=PostMetrics(0, 0, 0, None),
|
||||
)
|
||||
|
||||
|
||||
def write_test_thread(path: Path, thread: ThreadData) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps(asdict(thread), indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class TwMediaCommandTests(unittest.TestCase):
|
||||
def test_refetch_accepts_repeatable_branch(self) -> None:
|
||||
args = build_parser().parse_args([
|
||||
@@ -82,6 +115,224 @@ class TwMediaCommandTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(SystemExit, "300"):
|
||||
_validate_capture_ids(path, ("100", "300"))
|
||||
|
||||
def test_select_branch_capture_keeps_entire_visible_subtree(self) -> None:
|
||||
existing = ThreadData(
|
||||
root_post_id="100",
|
||||
posts=(make_post("100", None),),
|
||||
source_url="https://x.com/i/status/200",
|
||||
)
|
||||
captured = ThreadData(
|
||||
root_post_id="300",
|
||||
posts=(
|
||||
make_post("100", None),
|
||||
make_post("300", "100"),
|
||||
make_post("400", "300"),
|
||||
make_post("500", "300"),
|
||||
make_post("600", "100"),
|
||||
),
|
||||
source_url="https://x.com/i/status/300",
|
||||
)
|
||||
selected, leaves = _select_branch_capture(existing, captured, "300")
|
||||
self.assertEqual(
|
||||
tuple(post.post_id for post in selected),
|
||||
("300", "400", "500"),
|
||||
)
|
||||
self.assertEqual(leaves, ("400", "500"))
|
||||
|
||||
def test_select_branch_capture_keeps_missing_attachment_path(self) -> None:
|
||||
existing = ThreadData(
|
||||
root_post_id="100",
|
||||
posts=(make_post("100", None),),
|
||||
source_url="https://x.com/i/status/200",
|
||||
)
|
||||
captured = ThreadData(
|
||||
root_post_id="300",
|
||||
posts=(
|
||||
make_post("100", None),
|
||||
make_post("250", "100"),
|
||||
make_post("300", "250"),
|
||||
make_post("400", "300"),
|
||||
),
|
||||
source_url="https://x.com/i/status/300",
|
||||
)
|
||||
selected, leaves = _select_branch_capture(existing, captured, "300")
|
||||
self.assertEqual(
|
||||
tuple(post.post_id for post in selected),
|
||||
("250", "300", "400"),
|
||||
)
|
||||
self.assertEqual(leaves, ("400",))
|
||||
|
||||
def test_select_branch_capture_rejects_unattached_capture(self) -> None:
|
||||
existing = ThreadData(
|
||||
root_post_id="100",
|
||||
posts=(make_post("100", None),),
|
||||
source_url="https://x.com/i/status/200",
|
||||
)
|
||||
captured = ThreadData(
|
||||
root_post_id="999",
|
||||
posts=(make_post("999", None), make_post("300", "999")),
|
||||
source_url="https://x.com/i/status/300",
|
||||
)
|
||||
with self.assertRaisesRegex(SystemExit, "does not attach"):
|
||||
_select_branch_capture(existing, captured, "300")
|
||||
|
||||
def test_merge_branch_posts_preserves_existing_and_first_new_post(self) -> None:
|
||||
existing_100 = make_post("100", None)
|
||||
existing_200 = make_post("200", "100")
|
||||
existing = ThreadData(
|
||||
root_post_id="100",
|
||||
posts=(existing_100, existing_200),
|
||||
source_url="https://x.com/i/status/200",
|
||||
)
|
||||
first_300 = make_post("300", "100")
|
||||
second_300 = replace(first_300, text="replacement must not win")
|
||||
merged, added = _merge_branch_posts(
|
||||
existing,
|
||||
[
|
||||
(replace(existing_200, text="fresh duplicate"), first_300),
|
||||
(second_300, make_post("400", "300")),
|
||||
],
|
||||
)
|
||||
self.assertEqual(merged.posts, (
|
||||
existing_100,
|
||||
existing_200,
|
||||
first_300,
|
||||
make_post("400", "300"),
|
||||
))
|
||||
self.assertEqual(merged.root_post_id, existing.root_post_id)
|
||||
self.assertEqual(merged.source_url, existing.source_url)
|
||||
self.assertEqual(added, ("300", "400"))
|
||||
|
||||
def test_copy_selected_media_copies_only_selected_post_files(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
source = root / "source"
|
||||
destination = root / "destination"
|
||||
(source / "media").mkdir(parents=True)
|
||||
(destination / "media").mkdir(parents=True)
|
||||
(source / "media" / "300_1.jpg").write_bytes(b"300")
|
||||
(source / "media" / "400_1.png").write_bytes(b"400")
|
||||
(source / "media" / "600_1.jpg").write_bytes(b"600")
|
||||
(destination / "media" / "existing.jpg").write_bytes(b"old")
|
||||
copied = _copy_selected_media(
|
||||
source,
|
||||
destination,
|
||||
{"300", "400"},
|
||||
)
|
||||
self.assertEqual(
|
||||
tuple(path.name for path in copied),
|
||||
("300_1.jpg", "400_1.png"),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(path.name for path in (destination / "media").iterdir()),
|
||||
["300_1.jpg", "400_1.png", "existing.jpg"],
|
||||
)
|
||||
|
||||
def test_add_branch_accepts_repeatable_from_nodes(self) -> None:
|
||||
args = build_parser().parse_args([
|
||||
"add-branch",
|
||||
"--id", "200",
|
||||
"--from", "300",
|
||||
"--from", "400",
|
||||
])
|
||||
self.assertEqual(args.id, "200")
|
||||
self.assertEqual(args.from_ids, ["300", "400"])
|
||||
|
||||
def test_add_branch_stages_selected_subtree_and_keeps_spine_tip(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
assets = root / "assets"
|
||||
notes = root / "notes"
|
||||
notes.mkdir()
|
||||
existing = ThreadData(
|
||||
root_post_id="200",
|
||||
posts=(make_post("100", None), make_post("200", "100")),
|
||||
source_url="https://x.com/i/status/200",
|
||||
)
|
||||
existing_path = assets / "thread_data.json"
|
||||
write_test_thread(existing_path, existing)
|
||||
existing_bytes = existing_path.read_bytes()
|
||||
|
||||
branch_scratch = root / "refetch_300"
|
||||
captured = ThreadData(
|
||||
root_post_id="300",
|
||||
posts=(
|
||||
make_post("100", None),
|
||||
make_post("300", "100"),
|
||||
make_post("400", "300"),
|
||||
make_post("500", "300"),
|
||||
make_post("600", "100"),
|
||||
),
|
||||
source_url="https://x.com/i/status/300",
|
||||
)
|
||||
write_test_thread(branch_scratch / "thread_data.json", captured)
|
||||
(branch_scratch / "media").mkdir()
|
||||
for post_id in ("300", "400", "500", "600"):
|
||||
(branch_scratch / "media" / f"{post_id}_1.jpg").write_bytes(
|
||||
post_id.encode("ascii"),
|
||||
)
|
||||
|
||||
def fake_scratch(post_id: str) -> Path:
|
||||
return root / f"refetch_{post_id}"
|
||||
|
||||
with (
|
||||
patch.object(tw_module, "locate", return_value=(assets, notes)),
|
||||
patch.object(tw_module, "scratch_dir", side_effect=fake_scratch),
|
||||
patch.object(tw_module, "check_frozen"),
|
||||
patch.object(tw_module, "cmd_refetch") as refetch,
|
||||
patch.object(tw_module, "cmd_emit") as emit,
|
||||
):
|
||||
with redirect_stdout(StringIO()):
|
||||
result = cmd_add_branch(
|
||||
Namespace(id="200", from_ids=["300"]),
|
||||
)
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
refetch_args = refetch.call_args.args[0]
|
||||
self.assertEqual(refetch_args.id, "300")
|
||||
self.assertEqual(refetch_args.branch, [])
|
||||
emit_args = emit.call_args.args[0]
|
||||
self.assertEqual(emit_args.id, "200")
|
||||
self.assertTrue(emit_args.tip)
|
||||
self.assertIsNone(emit_args.slug)
|
||||
|
||||
staged = load_thread(root / "refetch_200" / "thread_data.json")
|
||||
self.assertEqual(
|
||||
tuple(post.post_id for post in staged.posts),
|
||||
("100", "200", "300", "400", "500"),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(path.name for path in (root / "refetch_200" / "media").iterdir()),
|
||||
["300_1.jpg", "400_1.jpg", "500_1.jpg"],
|
||||
)
|
||||
self.assertEqual(existing_path.read_bytes(), existing_bytes)
|
||||
|
||||
def test_add_branch_rejects_id_that_is_not_stored_spine_tip(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
assets = root / "assets"
|
||||
notes = root / "notes"
|
||||
notes.mkdir()
|
||||
write_test_thread(
|
||||
assets / "thread_data.json",
|
||||
ThreadData(
|
||||
root_post_id="100",
|
||||
posts=(make_post("100", None), make_post("200", "100")),
|
||||
source_url="https://x.com/i/status/100",
|
||||
),
|
||||
)
|
||||
with (
|
||||
patch.object(tw_module, "locate", return_value=(assets, notes)),
|
||||
patch.object(tw_module, "check_frozen"),
|
||||
patch.object(
|
||||
tw_module,
|
||||
"cmd_refetch",
|
||||
side_effect=AssertionError("must reject before fetch"),
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "stored spine tip"):
|
||||
cmd_add_branch(Namespace(id="200", from_ids=["300"]))
|
||||
|
||||
def test_migrate_defaults_to_dry_run(self) -> None:
|
||||
args = build_parser().parse_args(["migrate-media", "--id", "100"])
|
||||
self.assertEqual(args.cmd, "migrate-media")
|
||||
|
||||
Reference in new issue
Block a user