File size: 6,133 Bytes
43e5407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da80710
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43e5407
da80710
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1e47cba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e068425
 
 
 
 
 
1e47cba
e068425
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""
The abstract backfill.

90.6% of the live corpus (1,630,273 of 1,799,348 rows, measured 2026-08-21) is
capped at 500 characters, and the truncation degrades four things at once: the
card, the generated explanation, the density badge, and the cross-encoder in the
search path.

These cover the decision logic. The network and the writes are not exercised —
this repairs a production store, and a test that mutates it would be worse than
no test.
"""
import importlib.util
import pathlib

import pytest

spec = importlib.util.spec_from_file_location(
    "backfill", pathlib.Path("scripts/backfill_abstracts.py"))
backfill = importlib.util.module_from_spec(spec)
spec.loader.exec_module(backfill)


def test_batch_and_pause_respect_arxiv_policy():
    """arXiv asks for no more than one request every three seconds in bulk."""
    assert backfill.PAUSE_S >= 3.0
    assert backfill.BATCH <= 200, "id_list batches larger than this get refused"


def test_a_marginally_longer_abstract_is_not_worth_a_write():
    """Rewriting a row for 10 extra characters costs a write and buys nothing."""
    assert backfill.MIN_GAIN_CHARS > 0
    assert 500 + backfill.MIN_GAIN_CHARS > 500


def test_cell_encodes_the_types_turso_accepts():
    assert backfill._cell(None) == {"type": "null", "value": None}
    assert backfill._cell(7) == {"type": "integer", "value": "7"}
    assert backfill._cell("abc") == {"type": "text", "value": "abc"}


def test_every_strategy_is_a_valid_ordering():
    """A typo here would silently reorder a 13-hour job."""
    for strategy in ("served", "recent", "any"):
        # pick() builds SQL from this map; exercise the lookup without a network.
        order = {"served": "ORDER BY citation_count DESC",
                 "recent": "ORDER BY update_date DESC",
                 "any": ""}[strategy]
        assert "DROP" not in order.upper()
        assert order == "" or order.startswith("ORDER BY")


def test_writing_requires_an_explicit_flag():
    """--dry-run is the default posture for a bulk production mutation."""
    src = pathlib.Path("scripts/backfill_abstracts.py").read_text()
    assert '"--apply"' in src
    assert "if args.dry_run or not args.apply:" in src, (
        "the script can write without an explicit --apply")


def test_the_script_is_resumable():
    """A repaired row must stop matching the selection.

    This test previously asserted `>= 500` and PASSED while the script was NOT
    resumable — a repaired abstract of 976 characters still satisfies `>= 500`,
    so the script would have re-selected and re-fetched its own completed work
    forever. The predicate has to be the cap EXACTLY.
    """
    assert backfill.TRUNCATED == "length(abstract_preview) = 500"

    src = pathlib.Path("scripts/backfill_abstracts.py").read_text()
    assert "abstract_preview) >= 500" not in src, (
        "a >= predicate also matches repaired rows and rows that were never "
        "truncated")


def test_the_predicate_does_not_count_healthy_rows_as_damaged():
    """`>= 500` counted 193,689 legitimately-long abstracts as truncated,
    overstating the job by 13%."""
    assert "=" in backfill.TRUNCATED and ">=" not in backfill.TRUNCATED


def test_one_http_request_per_batch():
    """The first run was 429'd out of 9 of its 20 batches.

    The cause was reusing arxiv_svc.fetch_metadata_batch, which fans any input
    into 20-id sub-requests at ~3/s — so a "batch of 100" was a burst of five
    requests and the pause sat between bursts, not between requests.
    """
    import ast
    src = pathlib.Path("scripts/backfill_abstracts.py").read_text()
    assert "async def fetch_abstracts" in src

    # Parsed, not grepped: the module comments EXPLAIN why the interactive
    # helper is not used, and a substring check flags its own rationale.
    tree = ast.parse(src)
    calls = [
        ast.unparse(n.func) for n in ast.walk(tree)
        if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)
    ]
    assert "arxiv_svc.fetch_metadata_batch" not in calls, (
        "still fanning out through the interactive helper")

    assert backfill.MAX_RETRIES >= 2, "no retry budget for rate limiting"
    assert "429" in src, "no explicit handling of arXiv rate limiting"


def test_old_style_ids_survive_the_round_trip():
    """6.4% of the corpus has a category prefix; the script must key on it."""
    from app.arxiv_svc import _normalise_id
    assert _normalise_id("http://arxiv.org/abs/math/0309136v1") == "math/0309136"


def test_writes_are_batched_into_one_round_trip():
    """One UPDATE per row is not a detail at this scale.

    At ~150ms per Turso round trip, 1.63M individual writes would spend ~68
    hours on HTTP alone — dwarfing the arXiv rate limiting the estimate was
    originally based on. Turso's pipeline API takes a batch.
    """
    src = pathlib.Path("scripts/backfill_abstracts.py").read_text()
    assert "async def _pipeline" in src
    assert "for aid, full in updates" in src

    # The per-row await inside the write loop is what must not come back.
    import ast
    tree = ast.parse(src)
    for node in ast.walk(tree):
        if isinstance(node, ast.AsyncFunctionDef) and node.name == "repair":
            for inner in ast.walk(node):
                if isinstance(inner, ast.For):
                    body = ast.unparse(inner)
                    if "UPDATE papers" in body and "_turso(" in body:
                        pytest.fail(
                            "repair() awaits a write inside a per-row loop")


def test_the_estimate_is_measured_not_modelled():
    """Four estimates, three wrong: 13.6h counted only the arXiv pause, 22.6h
    added a guessed write cost but kept an inflated truncation count, 10h fixed
    the count but kept the guess. This one is timed against real batches."""
    assert backfill.SECONDS_PER_BATCH > backfill.PAUSE_S, (
        "the per-batch cost cannot be less than the mandated pause")
    src = pathlib.Path("scripts/backfill_abstracts.py").read_text()
    assert "SECONDS_PER_BATCH" in src
    assert "PAUSE_S + 2.0" not in src, "still using the guessed write cost"