CLAUDE: Fix game recovery for new GameState structure
Fixed state_manager._rebuild_state_from_data to provide required current_batter field when recovering games from database. The GameState model now requires current_batter as a LineupPlayerState object, but recovery was not populating this field, causing validation errors. Changes: - state_manager.py: Create placeholder current_batter during recovery - Build LineupPlayerState from first active batter (batting_order=1) - Fallback to first available lineup if no #1 batter found - Raise error if no lineups exist (invalid game state) - _prepare_next_play() will correct the batter after recovery - Moved get_lineup_player helper to top of method (removed duplicate) - tests/unit/core/test_state_manager.py: Update test to use new structure - test_update_state_nonexistent_raises_error: Create LineupPlayerState instead of using old current_batter_lineup_id field All 26 state_manager unit tests passing. Game recovery now works correctly in terminal client - fixes "current_batter Field required" validation error when running status command on recovered games. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
440adf2c26
commit
e6bd66ee39
@ -254,6 +254,46 @@ class StateManager:
|
||||
Reconstructed GameState
|
||||
"""
|
||||
game = game_data['game']
|
||||
lineups = game_data.get('lineups', [])
|
||||
|
||||
# Build lineup lookup dict for quick access
|
||||
lineup_dict = {l['id']: l for l in lineups}
|
||||
|
||||
# Helper function to create LineupPlayerState from lineup_id
|
||||
def get_lineup_player(lineup_id: int) -> Optional[LineupPlayerState]:
|
||||
if not lineup_id or lineup_id not in lineup_dict:
|
||||
return None
|
||||
lineup = lineup_dict[lineup_id]
|
||||
return LineupPlayerState(
|
||||
lineup_id=lineup['id'],
|
||||
card_id=lineup['card_id'] or 0, # Handle nullable
|
||||
position=lineup['position'],
|
||||
batting_order=lineup.get('batting_order'),
|
||||
is_active=lineup.get('is_active', True)
|
||||
)
|
||||
|
||||
# Get placeholder current_batter (required field)
|
||||
# _prepare_next_play() will set the correct batter after recovery
|
||||
current_batter_placeholder = None
|
||||
for lineup in lineups:
|
||||
if lineup.get('batting_order') == 1 and lineup.get('is_active'):
|
||||
current_batter_placeholder = get_lineup_player(lineup['id'])
|
||||
break
|
||||
|
||||
# If no batter found, use first available lineup
|
||||
if not current_batter_placeholder and lineups:
|
||||
first_lineup = lineups[0]
|
||||
current_batter_placeholder = LineupPlayerState(
|
||||
lineup_id=first_lineup['id'],
|
||||
card_id=first_lineup.get('card_id') or 0,
|
||||
position=first_lineup['position'],
|
||||
batting_order=first_lineup.get('batting_order'),
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# If still no batter (no lineups at all), raise error - game is in invalid state
|
||||
if not current_batter_placeholder:
|
||||
raise ValueError(f"Cannot recover game {game['id']}: No lineups found")
|
||||
|
||||
state = GameState(
|
||||
game_id=game['id'],
|
||||
@ -267,7 +307,8 @@ class StateManager:
|
||||
half=game.get('current_half', 'top'),
|
||||
home_score=game.get('home_score', 0),
|
||||
away_score=game.get('away_score', 0),
|
||||
play_count=len(game_data.get('plays', []))
|
||||
play_count=len(game_data.get('plays', [])),
|
||||
current_batter=current_batter_placeholder # Placeholder - corrected by _prepare_next_play()
|
||||
)
|
||||
|
||||
# Get last completed play to recover runner state and batter indices
|
||||
@ -278,23 +319,6 @@ class StateManager:
|
||||
if completed_plays:
|
||||
last_play = max(completed_plays, key=lambda p: p['play_number'])
|
||||
|
||||
# Build lineup lookup dict for quick access
|
||||
lineups = game_data.get('lineups', [])
|
||||
lineup_dict = {l['id']: l for l in lineups}
|
||||
|
||||
# Helper function to create LineupPlayerState from lineup_id
|
||||
def get_lineup_player(lineup_id: int) -> Optional[LineupPlayerState]:
|
||||
if not lineup_id or lineup_id not in lineup_dict:
|
||||
return None
|
||||
lineup = lineup_dict[lineup_id]
|
||||
return LineupPlayerState(
|
||||
lineup_id=lineup['id'],
|
||||
card_id=lineup['card_id'] or 0, # Handle nullable
|
||||
position=lineup['position'],
|
||||
batting_order=lineup.get('batting_order'),
|
||||
is_active=lineup.get('is_active', True)
|
||||
)
|
||||
|
||||
# Recover runners from *_final fields (where they ended up after last play)
|
||||
# Check each base - if a runner ended on that base, place them there
|
||||
runner_count = 0
|
||||
|
||||
@ -196,13 +196,21 @@ class TestStateManagerGetUpdate:
|
||||
|
||||
def test_update_state_nonexistent_raises_error(self, state_manager):
|
||||
"""Test updating nonexistent game raises error"""
|
||||
from app.models.game_models import LineupPlayerState
|
||||
|
||||
fake_id = uuid4()
|
||||
fake_batter = LineupPlayerState(
|
||||
lineup_id=1,
|
||||
card_id=100,
|
||||
position='CF',
|
||||
batting_order=1
|
||||
)
|
||||
fake_state = GameState(
|
||||
game_id=fake_id,
|
||||
league_id="sba",
|
||||
home_team_id=1,
|
||||
away_team_id=2,
|
||||
current_batter_lineup_id=1
|
||||
current_batter=fake_batter
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
|
||||
142
backend/uv.lock
generated
142
backend/uv.lock
generated
@ -626,77 +626,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paper-dynasty-backend"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "click" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "httpx" },
|
||||
{ name = "passlib", extra = ["bcrypt"] },
|
||||
{ name = "pendulum" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "python-jose", extra = ["cryptography"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "python-socketio" },
|
||||
{ name = "redis" },
|
||||
{ name = "rich" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
{ name = "flake8" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofiles", specifier = "==24.1.0" },
|
||||
{ name = "alembic", specifier = "==1.14.0" },
|
||||
{ name = "asyncpg", specifier = "==0.30.0" },
|
||||
{ name = "click", specifier = "==8.1.8" },
|
||||
{ name = "fastapi", specifier = "==0.115.6" },
|
||||
{ name = "greenlet", specifier = "==3.2.4" },
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "passlib", extras = ["bcrypt"], specifier = "==1.7.4" },
|
||||
{ name = "pendulum", specifier = "==3.0.0" },
|
||||
{ name = "psycopg2-binary", specifier = "==2.9.10" },
|
||||
{ name = "pydantic", specifier = "==2.10.6" },
|
||||
{ name = "pydantic-settings", specifier = "==2.7.1" },
|
||||
{ name = "python-dotenv", specifier = "==1.0.1" },
|
||||
{ name = "python-jose", extras = ["cryptography"], specifier = "==3.3.0" },
|
||||
{ name = "python-multipart", specifier = "==0.0.20" },
|
||||
{ name = "python-socketio", specifier = "==5.11.4" },
|
||||
{ name = "redis", specifier = "==5.2.1" },
|
||||
{ name = "rich", specifier = "==13.9.4" },
|
||||
{ name = "sqlalchemy", specifier = "==2.0.36" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = "==24.10.0" },
|
||||
{ name = "flake8", specifier = "==7.1.1" },
|
||||
{ name = "mypy", specifier = "==1.14.1" },
|
||||
{ name = "pytest", specifier = "==8.3.4" },
|
||||
{ name = "pytest-asyncio", specifier = "==0.25.2" },
|
||||
{ name = "pytest-cov", specifier = "==6.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "passlib"
|
||||
version = "1.7.4"
|
||||
@ -1111,6 +1040,77 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225, upload-time = "2024-11-18T19:45:02.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strat-gameplay-backend"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "click" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "httpx" },
|
||||
{ name = "passlib", extra = ["bcrypt"] },
|
||||
{ name = "pendulum" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "python-jose", extra = ["cryptography"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "python-socketio" },
|
||||
{ name = "redis" },
|
||||
{ name = "rich" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
{ name = "flake8" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofiles", specifier = "==24.1.0" },
|
||||
{ name = "alembic", specifier = "==1.14.0" },
|
||||
{ name = "asyncpg", specifier = "==0.30.0" },
|
||||
{ name = "click", specifier = "==8.1.8" },
|
||||
{ name = "fastapi", specifier = "==0.115.6" },
|
||||
{ name = "greenlet", specifier = "==3.2.4" },
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "passlib", extras = ["bcrypt"], specifier = "==1.7.4" },
|
||||
{ name = "pendulum", specifier = "==3.0.0" },
|
||||
{ name = "psycopg2-binary", specifier = "==2.9.10" },
|
||||
{ name = "pydantic", specifier = "==2.10.6" },
|
||||
{ name = "pydantic-settings", specifier = "==2.7.1" },
|
||||
{ name = "python-dotenv", specifier = "==1.0.1" },
|
||||
{ name = "python-jose", extras = ["cryptography"], specifier = "==3.3.0" },
|
||||
{ name = "python-multipart", specifier = "==0.0.20" },
|
||||
{ name = "python-socketio", specifier = "==5.11.4" },
|
||||
{ name = "redis", specifier = "==5.2.1" },
|
||||
{ name = "rich", specifier = "==13.9.4" },
|
||||
{ name = "sqlalchemy", specifier = "==2.0.36" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = "==24.10.0" },
|
||||
{ name = "flake8", specifier = "==7.1.1" },
|
||||
{ name = "mypy", specifier = "==1.14.1" },
|
||||
{ name = "pytest", specifier = "==8.3.4" },
|
||||
{ name = "pytest-asyncio", specifier = "==0.25.2" },
|
||||
{ name = "pytest-cov", specifier = "==6.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-machine"
|
||||
version = "2.19.0"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user