Forkei Claude Sonnet 4.6 commited on
Commit
b92e9ca
·
1 Parent(s): 8f74598

tests: fix test failures from Kenji-routing and API guard changes

Browse files

leaderboard.py / queries.py: replace hardcoded preset_key filter with
optional character_id parameter. Routes look up Kenji at request time
and pass the ID — keeps the library functions generic.

test_phase_3a_migration: update expected head version to
0014_mature_default_rating; update backfill assertion (family→mature).

test_phase_3a_ownership: add autouse fixture to set ALLOW_CHARACTER_API=true
+ clear lru_cache so patch/clone tests see the enabled flag.

Remaining failure (test_phase_4_1_rooms::test_detail_page_includes_viktor_theme_vars)
is pre-existing from commit 8d8c009 (demo: hide ambient toggle).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

app/discovery/leaderboard.py CHANGED
@@ -125,10 +125,18 @@ def character_leaderboard(
125
  *,
126
  viewer: Player,
127
  window: LeaderboardWindow = "all",
 
128
  ) -> list[CharacterLeaderboardRow]:
129
  char_wins, char_losses, draws = _char_win_expr()
130
  total = func.count(Match.id)
131
 
 
 
 
 
 
 
 
132
  stmt = (
133
  select(
134
  Character.id,
@@ -141,11 +149,7 @@ def character_leaderboard(
141
  draws.label("draws"),
142
  )
143
  .join(Match, Match.character_id == Character.id)
144
- .where(
145
- *_window_clauses(window),
146
- *visible_character_filter(viewer),
147
- Character.preset_key == "kenji_sato",
148
- )
149
  .group_by(Character.id, Character.name, Character.avatar_emoji, Character.current_elo)
150
  .having(total >= MIN_MATCHES_FOR_LEADERBOARD)
151
  )
@@ -186,11 +190,13 @@ def player_leaderboard(
186
  *,
187
  viewer: Player,
188
  window: LeaderboardWindow = "all",
 
189
  ) -> list[PlayerLeaderboardRow]:
190
  """Rank players by win rate vs. characters.
191
 
192
  A match's outcome is inverted from `_char_win_expr`: the player wins when
193
  the character loses, etc. Abandoned matches are player losses.
 
194
  """
195
  total = func.count(Match.id)
196
 
@@ -245,7 +251,7 @@ def player_leaderboard(
245
  *_window_clauses(window),
246
  *visible_character_filter(viewer),
247
  Player.username != "legacy_system", # never include the system fallback
248
- Character.preset_key == "kenji_sato",
249
  )
250
  .group_by(Player.id, Player.username, Player.display_name, Player.elo)
251
  .having(total >= MIN_MATCHES_FOR_LEADERBOARD)
 
125
  *,
126
  viewer: Player,
127
  window: LeaderboardWindow = "all",
128
+ character_id: str | None = None,
129
  ) -> list[CharacterLeaderboardRow]:
130
  char_wins, char_losses, draws = _char_win_expr()
131
  total = func.count(Match.id)
132
 
133
+ where_clauses = [
134
+ *_window_clauses(window),
135
+ *visible_character_filter(viewer),
136
+ ]
137
+ if character_id is not None:
138
+ where_clauses.append(Character.id == character_id)
139
+
140
  stmt = (
141
  select(
142
  Character.id,
 
149
  draws.label("draws"),
150
  )
151
  .join(Match, Match.character_id == Character.id)
152
+ .where(*where_clauses)
 
 
 
 
153
  .group_by(Character.id, Character.name, Character.avatar_emoji, Character.current_elo)
154
  .having(total >= MIN_MATCHES_FOR_LEADERBOARD)
155
  )
 
190
  *,
191
  viewer: Player,
192
  window: LeaderboardWindow = "all",
193
+ character_id: str | None = None,
194
  ) -> list[PlayerLeaderboardRow]:
195
  """Rank players by win rate vs. characters.
196
 
197
  A match's outcome is inverted from `_char_win_expr`: the player wins when
198
  the character loses, etc. Abandoned matches are player losses.
199
+ If character_id is given, restrict to matches against that character.
200
  """
201
  total = func.count(Match.id)
202
 
 
251
  *_window_clauses(window),
252
  *visible_character_filter(viewer),
253
  Player.username != "legacy_system", # never include the system fallback
254
+ *([] if character_id is None else [Character.id == character_id]),
255
  )
256
  .group_by(Player.id, Player.username, Player.display_name, Player.elo)
257
  .having(total >= MIN_MATCHES_FOR_LEADERBOARD)
app/discovery/queries.py CHANGED
@@ -81,21 +81,25 @@ def list_live_matches(
81
  viewer: Player,
82
  limit: int = 20,
83
  offset: int = 0,
 
84
  ) -> list[MatchSummary]:
85
  """In-progress matches the viewer can see, most-recently started first.
86
 
87
  Excludes the viewer's own matches — they're already in them.
 
88
  """
 
 
 
 
 
 
 
89
  stmt = (
90
  select(Match, Character, Player)
91
  .join(Character, Match.character_id == Character.id)
92
  .join(Player, Match.player_id == Player.id)
93
- .where(
94
- Match.status == MatchStatus.IN_PROGRESS,
95
- Match.player_id != viewer.id,
96
- *visible_character_filter(viewer),
97
- Character.preset_key == "kenji_sato",
98
- )
99
  .order_by(Match.started_at.desc())
100
  .offset(offset)
101
  .limit(limit)
@@ -109,21 +113,25 @@ def list_recent_matches(
109
  viewer: Player,
110
  limit: int = 20,
111
  offset: int = 0,
 
112
  ) -> list[MatchSummary]:
113
  """Completed or abandoned matches the viewer can see, most-recently ended first.
114
 
115
  Includes the viewer's own matches so they can navigate back to a summary.
 
116
  """
 
 
 
 
 
 
 
117
  stmt = (
118
  select(Match, Character, Player)
119
  .join(Character, Match.character_id == Character.id)
120
  .join(Player, Match.player_id == Player.id)
121
- .where(
122
- Match.status.in_([MatchStatus.COMPLETED, MatchStatus.RESIGNED, MatchStatus.ABANDONED]),
123
- Match.ended_at.is_not(None),
124
- *visible_character_filter(viewer),
125
- Character.preset_key == "kenji_sato",
126
- )
127
  .order_by(Match.ended_at.desc())
128
  .offset(offset)
129
  .limit(limit)
 
81
  viewer: Player,
82
  limit: int = 20,
83
  offset: int = 0,
84
+ character_id: str | None = None,
85
  ) -> list[MatchSummary]:
86
  """In-progress matches the viewer can see, most-recently started first.
87
 
88
  Excludes the viewer's own matches — they're already in them.
89
+ If character_id is given, restrict to matches against that character.
90
  """
91
+ clauses = [
92
+ Match.status == MatchStatus.IN_PROGRESS,
93
+ Match.player_id != viewer.id,
94
+ *visible_character_filter(viewer),
95
+ ]
96
+ if character_id is not None:
97
+ clauses.append(Match.character_id == character_id)
98
  stmt = (
99
  select(Match, Character, Player)
100
  .join(Character, Match.character_id == Character.id)
101
  .join(Player, Match.player_id == Player.id)
102
+ .where(*clauses)
 
 
 
 
 
103
  .order_by(Match.started_at.desc())
104
  .offset(offset)
105
  .limit(limit)
 
113
  viewer: Player,
114
  limit: int = 20,
115
  offset: int = 0,
116
+ character_id: str | None = None,
117
  ) -> list[MatchSummary]:
118
  """Completed or abandoned matches the viewer can see, most-recently ended first.
119
 
120
  Includes the viewer's own matches so they can navigate back to a summary.
121
+ If character_id is given, restrict to matches against that character.
122
  """
123
+ clauses = [
124
+ Match.status.in_([MatchStatus.COMPLETED, MatchStatus.RESIGNED, MatchStatus.ABANDONED]),
125
+ Match.ended_at.is_not(None),
126
+ *visible_character_filter(viewer),
127
+ ]
128
+ if character_id is not None:
129
+ clauses.append(Match.character_id == character_id)
130
  stmt = (
131
  select(Match, Character, Player)
132
  .join(Character, Match.character_id == Character.id)
133
  .join(Player, Match.player_id == Player.id)
134
+ .where(*clauses)
 
 
 
 
 
135
  .order_by(Match.ended_at.desc())
136
  .offset(offset)
137
  .limit(limit)
app/web/routes.py CHANGED
@@ -157,6 +157,14 @@ def index(
157
  # ------------------------------ Discovery ------------------------------
158
 
159
 
 
 
 
 
 
 
 
 
160
  @router.get("/leaderboard/characters", response_class=HTMLResponse)
161
  def leaderboard_characters_page(
162
  request: Request,
@@ -166,7 +174,9 @@ def leaderboard_characters_page(
166
  ) -> HTMLResponse:
167
  if window not in ("all", "30d", "7d"):
168
  window = "all"
169
- rows = character_leaderboard(session, viewer=player, window=window)
 
 
170
  return templates.TemplateResponse(
171
  request,
172
  "leaderboard_characters.html",
@@ -183,7 +193,9 @@ def leaderboard_players_page(
183
  ) -> HTMLResponse:
184
  if window not in ("all", "30d", "7d"):
185
  window = "all"
186
- rows = player_leaderboard(session, viewer=player, window=window)
 
 
187
  return templates.TemplateResponse(
188
  request,
189
  "leaderboard_players.html",
@@ -249,8 +261,9 @@ def discovery(
249
  player: Player = Depends(require_player),
250
  session: Session = Depends(get_session),
251
  ) -> HTMLResponse:
252
- live = list_live_matches(session, viewer=player, limit=20)
253
- recent = list_recent_matches(session, viewer=player, limit=20)
 
254
 
255
  return templates.TemplateResponse(
256
  request,
 
157
  # ------------------------------ Discovery ------------------------------
158
 
159
 
160
+ def _kenji_character_id(session) -> str | None:
161
+ """Return Kenji's character UUID, or None if not seeded yet."""
162
+ kenji = session.execute(
163
+ select(Character).where(Character.preset_key == "kenji_sato")
164
+ ).scalar_one_or_none()
165
+ return kenji.id if kenji is not None else None
166
+
167
+
168
  @router.get("/leaderboard/characters", response_class=HTMLResponse)
169
  def leaderboard_characters_page(
170
  request: Request,
 
174
  ) -> HTMLResponse:
175
  if window not in ("all", "30d", "7d"):
176
  window = "all"
177
+ rows = character_leaderboard(
178
+ session, viewer=player, window=window, character_id=_kenji_character_id(session)
179
+ )
180
  return templates.TemplateResponse(
181
  request,
182
  "leaderboard_characters.html",
 
193
  ) -> HTMLResponse:
194
  if window not in ("all", "30d", "7d"):
195
  window = "all"
196
+ rows = player_leaderboard(
197
+ session, viewer=player, window=window, character_id=_kenji_character_id(session)
198
+ )
199
  return templates.TemplateResponse(
200
  request,
201
  "leaderboard_players.html",
 
261
  player: Player = Depends(require_player),
262
  session: Session = Depends(get_session),
263
  ) -> HTMLResponse:
264
+ kenji_id = _kenji_character_id(session)
265
+ live = list_live_matches(session, viewer=player, limit=20, character_id=kenji_id)
266
+ recent = list_recent_matches(session, viewer=player, limit=20, character_id=kenji_id)
267
 
268
  return templates.TemplateResponse(
269
  request,
tests/test_phase_3a_migration.py CHANGED
@@ -56,7 +56,7 @@ def test_alembic_upgrade_head_on_fresh_db_is_idempotent(tmp_path, monkeypatch):
56
  try:
57
  cur = con.execute("SELECT version_num FROM alembic_version")
58
  version = cur.fetchone()[0]
59
- assert version == "0012_character_evolution_state"
60
  finally:
61
  con.close()
62
 
@@ -155,11 +155,11 @@ def test_alembic_upgrade_backfills_pre_3a_db(tmp_path, monkeypatch):
155
  ).fetchone()
156
  assert margot[0] == "family"
157
 
158
- # max_content_rating present and defaulted.
159
  mcr = con.execute(
160
  "SELECT max_content_rating FROM players WHERE id = 'p1'"
161
  ).fetchone()
162
- assert mcr[0] == "family"
163
 
164
  # Visibility defaulted to public.
165
  vis = con.execute(
 
56
  try:
57
  cur = con.execute("SELECT version_num FROM alembic_version")
58
  version = cur.fetchone()[0]
59
+ assert version == "0014_mature_default_rating"
60
  finally:
61
  con.close()
62
 
 
155
  ).fetchone()
156
  assert margot[0] == "family"
157
 
158
+ # max_content_rating present and upgraded to mature (migration 0014 backfill).
159
  mcr = con.execute(
160
  "SELECT max_content_rating FROM players WHERE id = 'p1'"
161
  ).fetchone()
162
+ assert mcr[0] == "mature"
163
 
164
  # Visibility defaulted to public.
165
  vis = con.execute(
tests/test_phase_3a_ownership.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  from fastapi.testclient import TestClient
6
 
7
  from app.db import SessionLocal
@@ -11,6 +12,16 @@ from app.models.match import Player
11
  from tests.conftest import signup_and_login
12
 
13
 
 
 
 
 
 
 
 
 
 
 
14
  def _client() -> TestClient:
15
  return TestClient(create_app(), follow_redirects=False)
16
 
 
2
 
3
  from __future__ import annotations
4
 
5
+ import pytest
6
  from fastapi.testclient import TestClient
7
 
8
  from app.db import SessionLocal
 
12
  from tests.conftest import signup_and_login
13
 
14
 
15
+ @pytest.fixture(autouse=True)
16
+ def _enable_character_api(monkeypatch):
17
+ """Enable character create/edit/clone API endpoints for ownership tests."""
18
+ monkeypatch.setenv("ALLOW_CHARACTER_API", "true")
19
+ from app.config import get_settings
20
+ get_settings.cache_clear()
21
+ yield
22
+ get_settings.cache_clear()
23
+
24
+
25
  def _client() -> TestClient:
26
  return TestClient(create_app(), follow_redirects=False)
27