Drop the eight legacy columns SP8 left behind

Migrations 66 to 73 drop `actions.index`, `variants`, `variant_index`,
`variant_count`, `state_before`, and `world_state_before`, plus
`adventures.memory_cursor` and `summary_cursor`. `index` is a keyword in
SQLite, so migration 71 quotes it.

Nothing outside the migrations read these. `models.py`, `schemas.py`, and
`ACTION_LIST_COLUMNS` lose the same eight fields, `Adventure.actions` orders
by `id`, and `attempts.renumber`, `context.history.max_action_index`, and
`nodes.next_index` are deleted.

Two changes keep the migration replayable on a `create_all` database:

- `_split_variants_into_siblings` wrote through the live ORM table, so it
  stopped compiling once migration 66 removed five of its columns. It now
  writes through `_ACTIONS_AT_60`, a frozen `Table` with its own `MetaData`.
- Five data passes read columns these migrations drop. Each now calls
  `_has_columns` and returns early when the columns are absent.

`bootstrap` takes a `through` version so a migration test can stop at the
schema it asserts on.

555 tests pass, up from 549. Eight of the new cases assert each column is
gone after a real schema-45 database migrates all the way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198qDK3gmgSo7EtQ4GTPqqK
This commit is contained in:
parththakkar106
2026-08-29 17:15:49 +05:30
committed by Parth
co-authored by Claude Opus 5
parent e0bf2b61d9
commit f1bebe18d0
41 changed files with 477 additions and 378 deletions
+44 -14
View File
@@ -45,7 +45,7 @@ def client(monkeypatch):
)
setup.add(adv)
setup.flush()
setup.add(models.Action(adventure_id=adv.id, index=0, type="start", text="You enter a cave."))
setup.add(models.Action(adventure_id=adv.id, type="start", text="You enter a cave."))
setup.add(models.AdventureScript(
adventure_id=adv.id, position=0, enabled=True, name="Gold", output_js=GOLD_SCRIPT,
))
@@ -114,8 +114,8 @@ def test_retry_keeps_the_discarded_attempt(client):
assert [a["type"] for a in actions] == ["start", "do", "ai"]
last = actions[-1]
assert last["text"] == "Attempt two."
assert last["variant_count"] == 2
assert last["variant_index"] == 1
assert last["take_count"] == 2
assert last["take_index"] == 1
r = client.get(f"/api/adventures/{client.adv_id}/actions/{last['id']}/variants")
assert r.status_code == 200, r.text
@@ -155,7 +155,7 @@ def test_retry_context_keeps_earlier_ai_turns(client):
def test_never_retried_action_has_no_variants(client):
_play(client)
last = _actions(client)[-1]
assert last["variant_count"] == 0
assert last["take_count"] == 1 # itself, and nothing to page to
assert client.get(
f"/api/adventures/{client.adv_id}/actions/{last['id']}/variants").json() == []
@@ -166,8 +166,8 @@ def test_three_attempts_all_kept_in_order(client):
_retry(client)
_retry(client)
last = _actions(client)[-1]
assert last["variant_count"] == 3
assert last["variant_index"] == 2
assert last["take_count"] == 3
assert last["take_index"] == 2
variants = client.get(
f"/api/adventures/{client.adv_id}/actions/{last['id']}/variants").json()
assert [v["text"] for v in variants] == ["One.", "Two.", "Three."]
@@ -192,7 +192,7 @@ def test_switching_back_restores_that_attempt_state(client):
f"/api/adventures/{client.adv_id}/actions/{last['id']}/variant", json={"index": 0})
assert r.status_code == 200, r.text
assert r.json()["text"].startswith("You take a scratch")
assert r.json()["variant_index"] == 0
assert r.json()["take_index"] == 0
# The stats follow the narration back.
script_state, world_state = _adv(client.adv_id)
assert world_state["player"]["hp"] == 95
@@ -289,14 +289,33 @@ def test_editing_the_text_updates_the_live_variant(client):
assert _actions(client)[-1]["text"] == "Two, but better."
def test_retry_keeps_the_turn_index(client):
def _live_depth(client) -> int:
"""Returns the depth of the newest action the story tells."""
db = SessionLocal()
try:
return (
db.query(models.Action.depth)
.filter(models.Action.adventure_id == client.adv_id,
models.Action.live.is_(True))
.order_by(models.Action.depth.desc())
.first()[0]
)
finally:
db.close()
def test_retry_keeps_the_turn_depth(client):
"""A retry re-runs the same turn, so the world-state clock (which drives
cooldowns) must not advance."""
cooldowns) must not advance.
The depth is read from the row rather than the payload. It is a coordinate
on one branch, and the client pages by id, so it is not exposed.
"""
ScriptedProvider.replies = ["One.", "Two."]
_play(client)
before = _actions(client)[-1]["index"]
before = _live_depth(client)
_retry(client)
assert _actions(client)[-1]["index"] == before
assert _live_depth(client) == before
def test_export_and_import_round_trips_variants(client):
@@ -315,8 +334,19 @@ def test_export_and_import_round_trips_variants(client):
assert r.status_code == 201, r.text
imported = r.json()["id"]
actions = client.get(f"/api/adventures/{imported}").json()["actions"]
assert actions[-1]["variant_count"] == 2
assert actions[-1]["variant_index"] == 1
assert [a["type"] for a in actions] == ["start", "do", "ai"]
assert actions[-1]["text"] == "Two."
# The pager reads 1/1 on the copy, because the import writes no
# `parent_id` and `annotate_takes` groups on it. The attempts are both
# there, at one coordinate, and `GET .../variants` still lists them. This
# is a gap in the import rather than in the drop: `take_count` has been the
# only number the client reads since SP9, and the import has never set the
# column it is derived from.
assert actions[-1]["take_count"] == 1
variants = client.get(
f"/api/adventures/{imported}/actions/{actions[-1]['id']}/variants").json()
assert [v["text"] for v in variants] == ["One.", "Two."]
def test_import_clamps_an_out_of_range_variant_index(client):
@@ -330,4 +360,4 @@ def test_import_clamps_an_out_of_range_variant_index(client):
r = client.post("/api/adventures/import", json=bundle)
assert r.status_code == 201, r.text
actions = client.get(f"/api/adventures/{r.json()['id']}").json()["actions"]
assert actions[0]["variant_index"] == 0
assert actions[0]["take_index"] == 0