"""The pre-played adventure every new guest is given: app/starter.py. The starter is a shipped export bundle, so the two things that can break it are the file and the import path. A bundle that no longer plans, or a story trimmed to a state a player cannot continue from, both leave the guest worse off than an empty account would. python -m pytest tests/test_starter_adventure.py -v """ import json 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 sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from app import bundle, models, starter from app.database import Base from app.migrations import bootstrap @pytest.fixture() def db(tmp_path): engine = create_engine(f"sqlite:///{tmp_path/'t.db'}", connect_args={"check_same_thread": False}) @event.listens_for(engine, "connect") def _fk(dbapi_connection, _record): cur = dbapi_connection.cursor() cur.execute("PRAGMA foreign_keys=ON") cur.close() bootstrap(engine) Base.metadata.create_all(bind=engine) session = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)() yield session session.close() @pytest.fixture() def guest(db): user = models.User(is_guest=True) db.add(user) db.commit() return user def payload() -> dict: return json.loads(starter.STARTER_FILE.read_text(encoding="utf-8")) def test_the_shipped_file_is_a_bundle_this_build_can_import(): """The file is written by an export, so a format change can strand it.""" data = payload() version = bundle.check_format(data) assert version == bundle.FORMAT story = bundle.plan(data, version) assert story["nodes"] def test_a_guest_gets_the_adventure_and_owns_it(db, guest): adventure = starter.give(db, guest) db.commit() assert adventure is not None assert adventure.user_id == guest.id assert "Pokemon" in adventure.title assert len(adventure.actions) == len(payload()["actions"]) def test_the_story_cards_come_with_it(db, guest): """A copy without the cards would drop out of character on the next turn.""" adventure = starter.give(db, guest) db.commit() assert {c.name for c in adventure.story_cards} == { c["name"] for c in payload()["storyCards"] } def test_the_turns_carry_what_the_engine_recorded(db, guest): """The point of shipping a played story: the summaries have chips in them. An applied change, a refused one, and a milestone all appear in the first two exchanges, which is what a visitor sees before spending a demo turn. """ adventure = starter.give(db, guest) db.commit() chips = [chip for action in adventure.actions for chip in action.world_changes] kinds = {chip["kind"] for chip in chips} assert "stat" in kinds assert "rejected" in kinds assert "milestone" in kinds def test_the_state_left_behind_can_be_played_from(db, guest): """The opponent has to be on the field with HP, or every hit is refused. A Pokemon sent in at 0 HP sits at the floor of `active_hp`, so the engine refuses each later change and the story stops moving. That state reached a playtest once, and it must not be what a guest inherits. """ adventure = starter.give(db, guest) db.commit() milo = adventure.world_state["npc"]["milo"] assert milo["active_hp"] > 0 assert milo["active_pokemon"] assert adventure.actions[-1].world_state_after["npc"]["milo"] == milo def test_a_missing_file_costs_the_guest_nothing(db, guest, monkeypatch): """A packaging mistake must not stop an account from being created.""" monkeypatch.setattr(starter, "STARTER_FILE", starter.STARTER_FILE.with_name("gone.json")) assert starter.give(db, guest) is None def test_a_broken_bundle_leaves_no_half_written_adventure(db, guest, monkeypatch): """The savepoint: a failure partway through discards the rows it wrote.""" monkeypatch.setattr(bundle, "materialize", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom"))) assert starter.give(db, guest) is None db.commit() assert db.query(models.Adventure).count() == 0