* 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.
140 lines
4.4 KiB
Python
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) == {}
|