Files
interactive-story/backend/tests/test_turn_flow_integration.py
T
Parth e7d75c3b05 Rewrite Python comments in Google developer documentation style (#12)
* Rewrite comments in Google developer documentation style

Rewrite the comments and docstrings across the backend core modules so they
read plainly. The previous prose was accurate but dense and figurative, which
made it slow to skim.

Applies the Google developer documentation style guide: short sentences, active
voice, present tense, American spelling, and no metaphors, idioms, or
rhetorical asides. Replaces em-dash chains with separate sentences.
2026-08-26 15:37:25 +05:30

140 lines
4.4 KiB
Python

"""End-to-end HTTP tests for undo and retry state revert. These tests drive
real turns through the actual routes and scripting engine, with only the LLM
provider mocked.
A script's output hook adds 10 gold each turn. These tests confirm that the
adventure's stored gold total stays correct across play, undo, and retry.
python -m pytest tests/test_turn_flow_integration.py -v
"""
import os
import tempfile
_tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
_tmp.close()
os.environ["AIDND_DB_PATH"] = _tmp.name
os.environ.pop("AIDND_DATABASE_URL", None)
os.environ.pop("DATABASE_URL", None)
import pytest
from fastapi import Depends
from fastapi.testclient import TestClient
from app import auth, limits, models
from app.database import Base, SessionLocal, engine, get_db
from app.main import app
from app.providers import PromptParts
from app.routers import adventures
GOLD_SCRIPT = """
const modifier = (text) => {
state.gold = (state.gold || 0) + 10;
return { text };
};
modifier(text);
"""
class FakeProvider:
"""Stand-in for OpenAICompatibleProvider: streams one fixed line, no network."""
last_usage = None
def __init__(self, *a, **k):
pass
async def generate(self, parts: PromptParts, *, temperature, max_tokens):
yield ("text", "The torch flickers as you press onward.")
@pytest.fixture()
def client(monkeypatch):
Base.metadata.create_all(bind=engine)
setup = SessionLocal()
user = models.User(is_guest=False, email="tester@example.com")
setup.add(user)
setup.flush()
setup.add(models.Settings(user_id=user.id, api_key="enc:dummy", model="test-model"))
adv = models.Adventure(user_id=user.id, title="Cave", script_state={})
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.AdventureScript(
adventure_id=adv.id, position=0, enabled=True, name="Gold",
output_js=GOLD_SCRIPT,
))
setup.commit()
adv_id, user_id = adv.id, user.id
setup.close()
# Force a real, non-demo turn that uses the fake provider.
monkeypatch.setattr(adventures, "OpenAICompatibleProvider", FakeProvider)
monkeypatch.setattr(auth, "resolve_provider_config", lambda s: auth.ProviderConfig(
"http://fake", "k", "test-model", False))
monkeypatch.setattr(limits, "rate_limit", lambda *a, **k: None)
monkeypatch.setattr(limits, "check_row_cap", lambda *a, **k: None)
def _current_user(db=Depends(get_db)):
return db.get(models.User, user_id)
app.dependency_overrides[auth.get_current_user] = _current_user
c = TestClient(app)
c.adv_id = adv_id
try:
yield c
finally:
app.dependency_overrides.clear()
adventures._active_turns.clear()
Base.metadata.drop_all(bind=engine)
def _state(adv_id):
db = SessionLocal()
try:
return db.get(models.Adventure, adv_id).script_state
finally:
db.close()
def _play(client, type_="do", text="look around"):
r = client.post(f"/api/adventures/{client.adv_id}/actions", json={"type": type_, "text": text})
assert r.status_code == 200, r.text
return r
def test_play_then_undo_reverts_gold(client):
assert _state(client.adv_id) == {}
_play(client)
assert _state(client.adv_id) == {"gold": 10}
r = client.post(f"/api/adventures/{client.adv_id}/undo")
assert r.status_code == 200, r.text
assert _state(client.adv_id) == {} # gold reverted to zero
def test_two_turns_then_undo_reverts_only_last(client):
_play(client)
_play(client)
assert _state(client.adv_id) == {"gold": 20}
client.post(f"/api/adventures/{client.adv_id}/undo")
assert _state(client.adv_id) == {"gold": 10} # back to after turn 1, not 0
def test_retry_does_not_double_apply_gold(client):
_play(client)
assert _state(client.adv_id) == {"gold": 10}
# Before the fix, this produced 20 because the output hook ran twice.
# Now it stays 10.
r = client.post(f"/api/adventures/{client.adv_id}/retry")
assert r.status_code == 200, r.text
assert _state(client.adv_id) == {"gold": 10}
def test_retry_then_undo_still_clean(client):
_play(client)
client.post(f"/api/adventures/{client.adv_id}/retry")
assert _state(client.adv_id) == {"gold": 10}
client.post(f"/api/adventures/{client.adv_id}/undo")
assert _state(client.adv_id) == {}