Files
interactive-story/backend/tests/test_seed_sweep.py
T
parththakkar106andClaude Opus 5 9398c13da5 Delete seeded scenarios no seed file claims any more
`previous_titles` stops a rename stranding the row it left behind, but the rows
already stranded still had to be deleted by hand on every deployment. The
seeder now removes them on the next boot, which retires the stale "Road to the
Champion" demo without a database console.

Only rows with a NULL owner and `is_public` are considered, and a player's own
scenario is neither, so nothing anybody created is reachable. An adventure
started from a deleted demo survives: `adventures.scenario_id` is `ON DELETE
SET NULL`, and the adventure holds its own copies of the cards and scripts, so
it loses only the inherited cover art.

Two cases skip the sweep, because neither is an instruction to remove live
content: a seed file that fails to parse claims no title, and an empty seed
directory reads as a packaging failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PacdRuPXSkQQy4ZYdH32hF
2026-08-28 19:19:23 +05:30

163 lines
5.4 KiB
Python

"""Seeded demo scenarios: renames land in place, and orphans are removed.
A seed file is matched to its scenario by title, so renaming one used to insert
a second scenario and leave the first public forever. Both halves of the fix are
here: `previous_titles` moves the rename onto the existing row, and the sweep
deletes seeded rows no file claims any more.
python -m pytest tests/test_seed_sweep.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 app import models, seed
from app.database import Base, SessionLocal, engine
@pytest.fixture()
def db():
Base.metadata.create_all(bind=engine)
session = SessionLocal()
try:
yield session
finally:
session.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture()
def seed_dir(tmp_path, monkeypatch):
monkeypatch.setattr(seed, "SEED_DIR", tmp_path)
return tmp_path
def write_seed(seed_dir, name: str, **fields) -> None:
data = {"title": fields.pop("title"), "description": "d", "prompt": "p"}
data.update(fields)
(seed_dir / name).write_text(json.dumps(data), encoding="utf-8")
def titles(db) -> set[str]:
return {s.title for s in db.query(models.Scenario).all()}
def test_a_seed_is_inserted_once(db, seed_dir):
write_seed(seed_dir, "a.json", title="Alpha")
seed.seed_public_scenarios(engine)
seed.seed_public_scenarios(engine)
assert titles(db) == {"Alpha"}
def test_a_rename_moves_the_existing_row(db, seed_dir):
"""The whole point: one row, one id, and no orphan left behind."""
write_seed(seed_dir, "a.json", title="Alpha")
seed.seed_public_scenarios(engine)
original = db.query(models.Scenario).one().id
write_seed(seed_dir, "a.json", title="Alpha Prime", previous_titles=["Alpha"])
seed.seed_public_scenarios(engine)
db.expire_all()
row = db.query(models.Scenario).one()
assert (row.id, row.title) == (original, "Alpha Prime")
def test_a_seeded_row_no_file_claims_is_deleted(db, seed_dir):
"""The stale demo this sweep exists for, reproduced."""
write_seed(seed_dir, "a.json", title="Alpha")
write_seed(seed_dir, "b.json", title="Beta")
seed.seed_public_scenarios(engine)
assert titles(db) == {"Alpha", "Beta"}
(seed_dir / "b.json").unlink()
seed.seed_public_scenarios(engine)
db.expire_all()
assert titles(db) == {"Alpha"}
def test_a_previous_title_still_counts_as_claimed(db, seed_dir):
"""A rename runs the sweep in the same pass, and must not eat its own row."""
write_seed(seed_dir, "a.json", title="Alpha")
seed.seed_public_scenarios(engine)
write_seed(seed_dir, "a.json", title="Alpha Prime", previous_titles=["Alpha"])
seed.seed_public_scenarios(engine)
db.expire_all()
assert titles(db) == {"Alpha Prime"}
def test_a_players_own_scenario_is_never_touched(db, seed_dir):
"""Only a NULL owner and `is_public` make a row seeded. A player's is
neither, so nothing anybody created is reachable from the sweep."""
user = models.User(is_guest=True)
db.add(user)
db.flush()
db.add(models.Scenario(user_id=user.id, is_public=True, title="Mine"))
db.add(models.Scenario(user_id=user.id, is_public=False, title="Also mine"))
db.commit()
write_seed(seed_dir, "a.json", title="Alpha")
seed.seed_public_scenarios(engine)
db.expire_all()
assert titles(db) == {"Alpha", "Mine", "Also mine"}
def test_an_unreadable_seed_file_stops_the_sweep(db, seed_dir):
"""A syntax error is not an instruction to delete live demo content.
A file that will not parse claims no title, so sweeping on that pass would
remove the scenario it describes, and the next deploy would put it back.
"""
write_seed(seed_dir, "a.json", title="Alpha")
write_seed(seed_dir, "b.json", title="Beta")
seed.seed_public_scenarios(engine)
(seed_dir / "b.json").write_text("{ not json", encoding="utf-8")
seed.seed_public_scenarios(engine)
db.expire_all()
assert titles(db) == {"Alpha", "Beta"}
def test_an_adventure_outlives_the_demo_it_started_from(db, seed_dir):
"""`adventures.scenario_id` is ON DELETE SET NULL, so the story survives and
loses only the cover art it inherited."""
write_seed(seed_dir, "a.json", title="Alpha")
write_seed(seed_dir, "keep.json", title="Kept")
seed.seed_public_scenarios(engine)
scenario = db.query(models.Scenario).filter_by(title="Alpha").one()
user = models.User(is_guest=True)
db.add(user)
db.flush()
adventure = models.Adventure(user_id=user.id, scenario_id=scenario.id, title="Mine")
db.add(adventure)
db.commit()
adventure_id = adventure.id
(seed_dir / "a.json").unlink()
seed.seed_public_scenarios(engine)
db.expire_all()
assert titles(db) == {"Kept"}
survivor = db.get(models.Adventure, adventure_id)
assert survivor is not None
assert survivor.scenario_id is None
def test_an_empty_seed_directory_deletes_nothing(db, seed_dir):
"""No files at all reads as a packaging failure, not as a request to remove
every demo. The seeder returns before the sweep."""
write_seed(seed_dir, "a.json", title="Alpha")
seed.seed_public_scenarios(engine)
(seed_dir / "a.json").unlink()
seed.seed_public_scenarios(engine)
db.expire_all()
assert titles(db) == {"Alpha"}