Put the pager's numbers on the page

`2/4` needs the shape of a turn's take group for every message on screen.
Asking `attempts.group` per row would put a query behind each one -- the exact
cost `variant_count` was cached to avoid, and the reason SP8 could not just
drop that column and be done. So one query per page, keyed on the parents the
page mentions, and the count and ordinal land on the rows before they are
serialised.

`take_count` / `take_index` rather than reusing the old pair, because they do
not mean the same thing: the old ones cache a coordinate's siblings and say 0
for a turn nobody retook, these count the *turn's* takes across whatever
branches they ended up on and say 1/1. SP8 still drops `variant_count` and
`variant_index`.

Two traps, one of them mine. `parent_id` was not in ACTION_LIST_COLUMNS, so
reading it off a windowed row would have been a lazy load per action -- an
N+1 hidden behind the thing `load_only` exists to prevent. And the adventure
GET does not build `ActionOut` at all: it hands the window to the relationship
with `set_committed_value` and lets Pydantic walk it, so patching the three
places that do build ActionOut missed the one path every page load takes.
The test caught it by reading the wire instead of the helper.

424 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Dvvqn9ZDR4ixeFPHNbww7
This commit is contained in:
parththakkar106
2026-08-18 21:34:12 +05:30
committed by Parth
co-authored by Claude Opus 5
parent 2726840c60
commit c3c9b310eb
3 changed files with 117 additions and 3 deletions
+39
View File
@@ -246,6 +246,45 @@ def test_takes_under_one_parent_do_not_count_takes_under_its_sibling(client):
assert _group_size(under_c1[0].id) == 2, "C1's line counts only its own two"
def _page(client) -> list[dict]:
return client.get(f"/api/adventures/{client.adv_id}").json()["actions"]
def test_the_page_carries_the_pager_numbers(client):
"""`2/4` arrives with the page, not from a query per message."""
_play(client)
_retry(client)
_retry(client)
ai = [a for a in _page(client) if a["type"] == "ai"]
assert len(ai) == 1, "one take is on the path; the others are behind it"
assert ai[0]["take_count"] == 3
assert ai[0]["take_index"] == 2, "the newest take is the one being read"
def test_a_turn_nobody_retook_reads_one_of_one(client):
_play(client)
for action in _page(client):
assert action["take_count"] == 1
assert action["take_index"] == 0
def test_the_pager_still_counts_a_take_that_was_forked_away(client):
"""The 1/3 case, seen from the wire rather than from `attempts.group`."""
_play(client)
_retry(client)
_retry(client)
_play(client, "press on")
first_take = _ai_rows(client.adv_id)[0]
_play(client, "a different way", after_id=first_take.id)
ai = [a for a in _page(client) if a["id"] == first_take.id]
assert ai, "the forked take is what this branch now tells"
assert ai[0]["take_count"] == 3
assert ai[0]["take_index"] == 0
def _take(client, action_id, text):
return client.post(
f"/api/adventures/{client.adv_id}/actions/{action_id}/takes",