Every test module carried the same eight-line prologue redirecting the database to a temp file. Only the first one to be imported ever took effect: `app.database` reads `AIDND_DB_PATH` at import and builds `engine` from it once, so by the time the second module ran the engine already existed. The other 34 copies created a temp file that nothing opened and nothing deleted, and leaked one per module per run. `conftest.py` now does it once, which is early enough because pytest imports conftest before any test module. It also deletes the file when the run ends. The tests still share one database, exactly as they already did: each `client` fixture calls `create_all` on setup and `drop_all` on teardown, so no test sees another test's rows. `tests/fakes.py` holds the one `ScriptedProvider`. Nine modules each had a copy, and the copies had drifted into four feature sets, so a test that needed to raise a provider error had to be written in one of the files whose copy supported that. The shared one is the superset. The two `FakeProvider` copies were the same class with a fixed reply, so they use it too. `test_chat.py` keeps its own, which implements `chat` rather than `generate` and records what it was constructed with. An autouse fixture resets the fake's class state between tests, so a stale reply list can no longer reach the next test. 435 lines out of the suite. 549 tests pass. Verified live by sabotage: breaking the shared fake fails 13 tests across four modules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Dix4oGV3njgWRdu7P9t6r
156 lines
5.2 KiB
Python
156 lines
5.2 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 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"}
|