Remove three kinds of duplication in the backend
Stage 2, items 1, 4, and 5 of `plan/17-refactor.md`. **One path resolver in `worldstate`.** `apply_delta` and `apply_override` routed `flags.<name>`, `milestones.<id>`, `world.<stat>`, `player.<stat>`, and `npc.<id>.<stat>` with parallel code, about 100 lines each. `_resolve` now says what a path points at and returns either a target or the rejection to report. Each function keeps its own write rule, because the rules genuinely differ: an override sets a number rather than adding to it, ignores `cooldown`, `max_delta_per_turn`, and the rule that a counter only counts up, and can un-set a milestone. A differential check ran both implementations over 3960 payloads: twenty paths, fourteen values, three starting states, plus every three-path combination. The results are identical except that 674 rejections from `apply_override` now carry a `fix` string. `apply_delta` already worded those, and the world-state editor renders them, so an override that names an unknown flag now explains itself the way a delta does. **`sse`, `SSE_HEADERS`, and `turn_error` move to `app/sse.py`.** Two routers stream, and `chat.py` had to import from `routers.adventures` to reach them. **`get_adventure_or_404` becomes the `current_adventure` dependency.** All 32 handlers repeated the call as their first statement. The ownership check now reads in the signature and runs before the body. FastAPI caches a dependency for one request, so the handler's `db` is the session the adventure came from. The generated OpenAPI document is byte-identical except on `rename_branch`, where `branch_id` is now listed before `adventure_id`, because that handler no longer names `adventure_id` itself. Parameter order in the document is cosmetic. Six tests in `test_state_revert.py` call `undo_turn` and `retry_action` directly rather than over HTTP. They pass the adventure they already hold instead of an id. 549 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Dix4oGV3njgWRdu7P9t6r
This commit is contained in:
co-authored by
Claude Opus 5
parent
0623dd8b78
commit
2c57b1ceab
@@ -81,7 +81,7 @@ def test_undo_reverts_state_to_before_the_turn(db):
|
||||
_add(db, adv, 2, "ai", state_after={"gold": 10})
|
||||
db.commit()
|
||||
|
||||
adventures.undo_turn(adv.id, db=db, user=user)
|
||||
adventures.undo_turn(adv.id, db=db, adventure=adv)
|
||||
|
||||
assert adv.script_state == {"gold": 0}
|
||||
assert [a.type for a in adv.actions] == ["start"]
|
||||
@@ -95,7 +95,7 @@ def test_undo_of_bare_continue_uses_the_node_in_front(db):
|
||||
_add(db, adv, 1, "ai", state_after={"gold": 5})
|
||||
db.commit()
|
||||
|
||||
adventures.undo_turn(adv.id, db=db, user=user)
|
||||
adventures.undo_turn(adv.id, db=db, adventure=adv)
|
||||
|
||||
assert adv.script_state == {"gold": 0}
|
||||
assert [a.type for a in adv.actions] == ["start"]
|
||||
@@ -110,7 +110,7 @@ def test_undo_leaves_state_untouched_when_snapshot_missing(db):
|
||||
_add(db, adv, 2, "ai")
|
||||
_forget_snapshots(db, adv)
|
||||
|
||||
adventures.undo_turn(adv.id, db=db, user=user)
|
||||
adventures.undo_turn(adv.id, db=db, adventure=adv)
|
||||
|
||||
assert adv.script_state == {"gold": 10}
|
||||
|
||||
@@ -120,7 +120,7 @@ def test_undo_raises_when_nothing_to_undo(db):
|
||||
_add(db, adv, 0, "start")
|
||||
db.commit()
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
adventures.undo_turn(adv.id, db=db, user=user)
|
||||
adventures.undo_turn(adv.id, db=db, adventure=adv)
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ def test_undo_blocked_by_active_turn_lock(db):
|
||||
adventures.turns.acquire_turn_lock(adv.id) # a turn is "generating"
|
||||
try:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
adventures.undo_turn(adv.id, db=db, user=user)
|
||||
adventures.undo_turn(adv.id, db=db, adventure=adv)
|
||||
assert exc.value.status_code == 409
|
||||
# The failed undo must not have released someone else's lock.
|
||||
assert adv.id in adventures.turns._active_turns
|
||||
@@ -151,7 +151,7 @@ def test_undo_prunes_memory_covering_removed_actions(db):
|
||||
db.add_all([covering, keep])
|
||||
db.commit()
|
||||
|
||||
adventures.undo_turn(adv.id, db=db, user=user) # removes indexes 2 & 3
|
||||
adventures.undo_turn(adv.id, db=db, adventure=adv) # removes indexes 2 & 3
|
||||
|
||||
texts = {m.text for m in adv.memories}
|
||||
assert texts == {"k"}
|
||||
@@ -228,7 +228,7 @@ def test_retry_restores_the_state_the_turn_started_from(db, monkeypatch):
|
||||
yield # make it an async generator
|
||||
monkeypatch.setattr(adventures.turns, "generate_turn", _noop)
|
||||
|
||||
adventures.retry_action(adv.id, request=None, db=db, user=user)
|
||||
adventures.retry_action(adv.id, request=None, db=db, user=user, adventure=adv)
|
||||
|
||||
assert adv.script_state == {"gold": 10}
|
||||
# Nothing is written until a replacement actually arrives: the attempt on
|
||||
|
||||
Reference in New Issue
Block a user