strat-gameplay-webapp/backend/terminal_client/update_docs/phase_6.md
Cal Corum 1c32787195 CLAUDE: Refactor game models and modularize terminal client
This commit includes cleanup from model refactoring and terminal client
modularization for better code organization and maintainability.

## Game Models Refactor

**Removed RunnerState class:**
- Eliminated separate RunnerState model (was redundant)
- Replaced runners: List[RunnerState] with direct base references:
  - on_first: Optional[LineupPlayerState]
  - on_second: Optional[LineupPlayerState]
  - on_third: Optional[LineupPlayerState]
- Updated helper methods:
  - get_runner_at_base() now returns LineupPlayerState directly
  - get_all_runners() returns List[Tuple[int, LineupPlayerState]]
  - is_runner_on_X() simplified to direct None checks

**Benefits:**
- Matches database structure (plays table has on_first_id, etc.)
- Simpler state management (direct references vs list management)
- Better type safety (LineupPlayerState vs generic runner)
- Easier to work with in game engine logic

**Updated files:**
- app/models/game_models.py - Removed RunnerState, updated GameState
- app/core/play_resolver.py - Use get_all_runners() instead of state.runners
- app/core/validators.py - Updated runner access patterns
- tests/unit/models/test_game_models.py - Updated test assertions
- tests/unit/core/test_play_resolver.py - Updated test data
- tests/unit/core/test_validators.py - Updated test data

## Terminal Client Refactor

**Modularization (DRY principle):**
Created separate modules for better code organization:

1. **terminal_client/commands.py** (10,243 bytes)
   - Shared command functions for game operations
   - Used by both CLI (main.py) and REPL (repl.py)
   - Functions: submit_defensive_decision, submit_offensive_decision,
     resolve_play, quick_play_sequence
   - Single source of truth for command logic

2. **terminal_client/arg_parser.py** (7,280 bytes)
   - Centralized argument parsing and validation
   - Handles defensive/offensive decision arguments
   - Validates formats (alignment, depths, hold runners, steal attempts)

3. **terminal_client/completions.py** (10,357 bytes)
   - TAB completion support for REPL mode
   - Command completions, option completions, dynamic completions
   - Game ID completions, defensive/offensive option suggestions

4. **terminal_client/help_text.py** (10,839 bytes)
   - Centralized help text and command documentation
   - Detailed command descriptions
   - Usage examples for all commands

**Updated main modules:**
- terminal_client/main.py - Simplified by using shared commands module
- terminal_client/repl.py - Cleaner with shared functions and completions

**Benefits:**
- DRY: Behavior consistent between CLI and REPL modes
- Maintainability: Changes in one place affect both interfaces
- Testability: Can test commands module independently
- Organization: Clear separation of concerns

## Documentation

**New files:**
- app/models/visual_model_relationships.md
  - Visual documentation of model relationships
  - Helps understand data flow between models
- terminal_client/update_docs/ (6 phase documentation files)
  - Phased documentation for terminal client evolution
  - Historical context for implementation decisions

## Tests

**New test files:**
- tests/unit/terminal_client/__init__.py
- tests/unit/terminal_client/test_arg_parser.py
- tests/unit/terminal_client/test_commands.py
- tests/unit/terminal_client/test_completions.py
- tests/unit/terminal_client/test_help_text.py

**Updated tests:**
- Integration tests updated for new runner model
- Unit tests updated for model changes
- All tests passing with new structure

## Summary

-  Simplified game state model (removed RunnerState)
-  Better alignment with database structure
-  Modularized terminal client (DRY principle)
-  Shared command logic between CLI and REPL
-  Comprehensive test coverage
-  Improved documentation

Total changes: 26 files modified/created

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 14:16:38 -05:00

23 KiB

● Terminal Client Improvement Plan - Part 6: Testing & Migration Guide

Overview

Comprehensive testing strategy and step-by-step migration guide to implement all terminal client improvements safely with full test coverage.


A. Comprehensive Testing Strategy

  1. Create Test Directory Structure

backend/tests/unit/terminal_client/ ├── init.py ├── test_commands.py # From Part 1 ├── test_arg_parser.py # From Part 2 ├── test_completions.py # From Part 3 ├── test_help_text.py # From Part 4 ├── test_player_cache.py # From Part 5 └── test_integration.py # New - full integration tests

  1. Create Integration Tests

backend/tests/unit/terminal_client/test_integration.py

""" Integration tests for terminal client improvements.

Tests the complete flow of all improvements working together. """ import pytest import asyncio from uuid import uuid4 from unittest.mock import AsyncMock, MagicMock, patch, call

from terminal_client.commands import GameCommands from terminal_client.arg_parser import ( parse_new_game_args, parse_defensive_args, parse_offensive_args ) from terminal_client.completions import GameREPLCompletions from terminal_client.help_text import show_help from terminal_client.player_cache import player_cache

class TestEndToEndWorkflow: """Test complete workflow using all improvements."""

  @pytest.fixture
  def game_commands(self):
      """Create GameCommands with mocked dependencies."""
      commands = GameCommands()
      commands.db_ops = AsyncMock()
      return commands

  @pytest.fixture
  def repl_completions(self):
      """Create GameREPLCompletions instance."""
      return GameREPLCompletions()

  @pytest.mark.asyncio
  async def test_complete_game_workflow(self, game_commands):
      """
      Test complete workflow: create game, make decisions, resolve.
      
      This tests that all components work together:
      - Shared commands
      - Argument parsing
      - Game engine integration
      """
      game_id = uuid4()

      with patch('terminal_client.commands.state_manager') as mock_sm:
          with patch('terminal_client.commands.game_engine') as mock_ge:
              with patch('terminal_client.commands.uuid4', return_value=game_id):
                  # Mock game state
                  from app.models.game_models import GameState
                  mock_state = GameState(
                      game_id=game_id,
                      league_id='sba',
                      home_team_id=1,
                      away_team_id=2,
                      inning=1,
                      half='top',
                      status='active'
                  )

                  mock_sm.create_game = AsyncMock(return_value=mock_state)
                  mock_ge.start_game = AsyncMock(return_value=mock_state)
                  mock_ge.submit_defensive_decision = AsyncMock(return_value=mock_state)
                  mock_ge.submit_offensive_decision = AsyncMock(return_value=mock_state)

                  # Step 1: Create game
                  gid, success = await game_commands.create_new_game(
                      league='sba',
                      home_team=1,
                      away_team=2
                  )

                  assert success is True
                  assert gid == game_id
                  mock_ge.start_game.assert_called_once_with(game_id)

                  # Step 2: Submit defensive decision
                  success = await game_commands.submit_defensive_decision(
                      game_id=game_id,
                      alignment='shifted_left',
                      hold_runners=[1, 3]
                  )

                  assert success is True
                  mock_ge.submit_defensive_decision.assert_called_once()

                  # Step 3: Submit offensive decision
                  success = await game_commands.submit_offensive_decision(
                      game_id=game_id,
                      approach='power',
                      steal_attempts=[2]
                  )

                  assert success is True
                  mock_ge.submit_offensive_decision.assert_called_once()

  def test_argument_parsing_with_completions(self, repl_completions):
      """
      Test that parsed arguments work with tab completions.
      
      Ensures argument parser and completion system are compatible.
      """
      # Parse arguments
      args = parse_defensive_args('--alignment shifted_left --hold 1,3')

      assert args['alignment'] == 'shifted_left'
      assert args['hold'] == [1, 3]

      # Verify completion suggests valid values
      completions = repl_completions.complete_defensive(
          'shift', 'defensive --alignment shift', 10, 30
      )

      assert 'shifted_left' in completions

  def test_help_system_covers_all_commands(self):
      """
      Test that help system has documentation for all commands.
      
      Ensures every command has proper help text.
      """
      from terminal_client.help_text import HELP_DATA

      required_commands = [
          'new_game', 'defensive', 'offensive', 'resolve',
          'quick_play', 'status', 'list_games', 'use_game'
      ]

      for cmd in required_commands:
          assert cmd in HELP_DATA, f"Missing help for {cmd}"

          help_data = HELP_DATA[cmd]
          assert 'summary' in help_data
          assert 'usage' in help_data
          assert 'examples' in help_data

  @pytest.mark.asyncio
  async def test_player_cache_integration(self):
      """
      Test player cache integration with game workflow.
      
      Verifies caching works correctly during gameplay.
      """
      game_id = uuid4()

      # Add players to cache
      player_cache.add_player({
          'card_id': 101,
          'name': 'Mike Trout',
          'position': 'CF',
          'team_id': 1,
          'league': 'sba'
      }, lineup_id=1)

      player_cache.add_player({
          'card_id': 201,
          'name': 'Clayton Kershaw',
          'position': 'P',
          'team_id': 2,
          'league': 'sba'
      }, lineup_id=10)

      # Verify retrieval
      batter = player_cache.get_by_lineup_id(1)
      assert batter is not None
      assert batter.name == 'Mike Trout'

      pitcher = player_cache.get_by_lineup_id(10)
      assert pitcher is not None
      assert pitcher.name == 'Clayton Kershaw'

      # Get stats
      stats = player_cache.get_stats()
      assert stats['card_cache_size'] >= 2

class TestErrorHandling: """Test error handling across all improvements."""

  @pytest.fixture
  def game_commands(self):
      commands = GameCommands()
      commands.db_ops = AsyncMock()
      return commands

  @pytest.mark.asyncio
  async def test_command_handles_database_error(self, game_commands):
      """Test that commands handle database errors gracefully."""
      game_id = uuid4()

      with patch('terminal_client.commands.game_engine') as mock_ge:
          mock_ge.submit_defensive_decision = AsyncMock(
              side_effect=Exception("Database error")
          )

          success = await game_commands.submit_defensive_decision(
              game_id=game_id
          )

          assert success is False

  def test_argument_parser_handles_invalid_input(self):
      """Test that argument parser handles invalid input gracefully."""
      from terminal_client.arg_parser import (
          parse_defensive_args,
          ArgumentParseError
      )

      # Invalid option
      with pytest.raises(ArgumentParseError, match="Unknown option"):
          parse_defensive_args('--invalid-option value')

      # Invalid type
      with pytest.raises(ArgumentParseError, match="expected int"):
          parse_defensive_args('--hold abc')

  def test_completion_handles_empty_state(self):
      """Test that completions work even with empty state."""
      from terminal_client.completions import GameREPLCompletions

      repl = GameREPLCompletions()

      # Should not crash on empty game list
      with patch('terminal_client.completions.state_manager') as mock_sm:
          mock_sm.list_games.return_value = []

          completions = repl.complete_use_game('', 'use_game ', 9, 9)

          assert completions == []

class TestPerformance: """Test performance of improvements."""

  def test_player_cache_performance(self):
      """Test that player cache provides fast lookups."""
      import time

      # Add 100 players
      for i in range(100):
          player_cache.add_player({
              'card_id': i,
              'name': f'Player {i}',
              'position': 'CF',
              'team_id': 1,
              'league': 'sba'
          }, lineup_id=i)

      # Time lookups
      start = time.time()
      for i in range(100):
          player = player_cache.get_by_lineup_id(i)
          assert player is not None
      end = time.time()

      # Should be very fast (< 10ms for 100 lookups)
      elapsed_ms = (end - start) * 1000
      assert elapsed_ms < 10, f"Cache lookups too slow: {elapsed_ms}ms"

  def test_argument_parsing_performance(self):
      """Test that argument parsing is fast."""
      import time
      from terminal_client.arg_parser import parse_defensive_args

      start = time.time()
      for _ in range(1000):
          args = parse_defensive_args(
              '--alignment shifted_left --infield double_play --hold 1,3'
          )
      end = time.time()

      elapsed_ms = (end - start) * 1000
      # Should parse 1000 times in < 100ms
      assert elapsed_ms < 100, f"Parsing too slow: {elapsed_ms}ms"

class TestBackwardCompatibility: """Test that improvements don't break existing functionality."""

  @pytest.mark.asyncio
  async def test_old_command_interface_still_works(self):
      """Test that old-style command usage still works."""
      from terminal_client.commands import game_commands

      game_id = uuid4()

      with patch('terminal_client.commands.game_engine') as mock_ge:
          from app.models.game_models import GameState
          mock_state = GameState(
              game_id=game_id,
              league_id='sba',
              home_team_id=1,
              away_team_id=2
          )
          mock_ge.get_game_state = AsyncMock(return_value=mock_state)

          # Old-style status check should still work
          success = await game_commands.show_game_status(game_id)

          assert success is True
          mock_ge.get_game_state.assert_called_once_with(game_id)
  1. End-to-End REPL Tests

backend/tests/e2e/test_terminal_client.py

""" End-to-end tests for terminal client REPL.

These tests simulate actual user interaction with the REPL. """ import pytest import asyncio from io import StringIO from unittest.mock import patch, MagicMock

from terminal_client.repl import GameREPL

class TestREPLInteraction: """Test REPL user interaction flows."""

  @pytest.fixture
  def repl(self):
      """Create REPL instance for testing."""
      with patch('terminal_client.repl.state_manager'):
          with patch('terminal_client.repl.game_engine'):
              repl = GameREPL()
              return repl

  def test_help_command(self, repl):
      """Test that help command displays correctly."""
      with patch('terminal_client.repl.HelpFormatter.show_command_list') as mock_help:
          repl.do_help('')
          mock_help.assert_called_once()

  def test_help_specific_command(self, repl):
      """Test help for specific command."""
      with patch('terminal_client.repl.show_help') as mock_help:
          repl.do_help('new_game')
          mock_help.assert_called_once_with('new_game')

  def test_tab_completion_defensive(self, repl):
      """Test tab completion for defensive command."""
      completions = repl.complete_defensive(
          '--align',
          'defensive --align',
          10,
          17
      )

      assert '--alignment' in completions

  def test_cache_stats_command(self, repl):
      """Test cache stats command."""
      with patch('terminal_client.repl.show_cache_stats') as mock_stats:
          repl.do_cache('stats')
          # Need to run the async function
          repl.loop.run_until_complete(repl._run_async(repl.do_cache('stats')))

class TestREPLWorkflow: """Test complete REPL workflows."""

  def test_new_game_workflow(self):
      """
      Test complete new game workflow:
      1. new_game
      2. defensive
      3. offensive
      4. resolve
      5. status
      """
      # This would be a full integration test
      # Implementation left as exercise
      pass

B. Migration Guide

Step-by-Step Implementation Order

Phase 1: Preparation (Week 1 Day 1-2)

  1. Create new files without breaking existing code

Create new modules

touch backend/terminal_client/commands.py touch backend/terminal_client/arg_parser.py touch backend/terminal_client/completions.py touch backend/terminal_client/help_text.py touch backend/terminal_client/player_cache.py touch backend/terminal_client/enhanced_display.py

Create test directory

mkdir -p backend/tests/unit/terminal_client touch backend/tests/unit/terminal_client/init.py

  1. Implement shared commands module
  • Copy code from Part 1 into commands.py
  • Add all imports
  • Don't modify repl.py or main.py yet
  • Run: python -c "from terminal_client.commands import GameCommands; print('✓ Import successful')"
  1. Run tests

Should still pass (no changes to existing code yet)

pytest backend/tests/unit/terminal_client/ -v

Phase 2: Argument Parser (Week 1 Day 3)

  1. Implement argument parser
  • Copy code from Part 2 into arg_parser.py
  • Add unit tests from Part 2
  • Test independently:

pytest backend/tests/unit/terminal_client/test_arg_parser.py -v

  1. Verify no regressions

Existing tests should still pass

pytest backend/tests/ -v

Phase 3: Update REPL (Week 1 Day 4-5)

  1. Create backup

cp backend/terminal_client/repl.py backend/terminal_client/repl.py.backup cp backend/terminal_client/main.py backend/terminal_client/main.py.backup

  1. Update repl.py incrementally

Update one command at a time:

Start with new_game

def do_new_game(self, arg): # New implementation using shared commands pass

Test it

python -m terminal_client > new_game --league sba

  1. Update all commands
  • Replace each do_* method with new implementation
  • Test each command individually
  • Verify existing functionality works
  1. Run full test suite

pytest backend/tests/unit/terminal_client/ -v

Phase 4: Tab Completion (Week 2 Day 1-2)

  1. Implement completions
  • Copy code from Part 3 into completions.py
  • Add tests
  1. Update REPL class

Add mixin

class GameREPL(GameREPLCompletions, cmd.Cmd): pass

  1. Test completions interactively

python -m terminal_client > def > defensive --

Phase 5: Help System (Week 2 Day 3)

  1. Implement help text
  • Copy code from Part 4 into help_text.py
  • Verify all commands have help data
  1. Update help methods in REPL
  • Replace do_help method
  • Add help_* methods
  1. Test help system

python -m terminal_client > help > help defensive

Phase 6: Player Cache (Week 2 Day 4-5) [OPTIONAL - Requires Week 6 Models]

  1. Implement cache (inactive by default)
  • Copy code from Part 5
  • Set feature flags to False
  1. Add tests
  • Test cache in isolation
  • Don't activate yet
  1. Document activation process
  • Update CLAUDE.md with activation instructions

Verification Checklist

After each phase, verify:

  • All existing tests pass
  • New tests pass
  • REPL starts without errors
  • Can create a new game
  • Can submit decisions
  • Can resolve plays
  • Can see game status
  • No import errors
  • No runtime errors in logs

Rollback Plan

If something goes wrong:

Quick Rollback:

Restore backups

cp backend/terminal_client/repl.py.backup backend/terminal_client/repl.py cp backend/terminal_client/main.py.backup backend/terminal_client/main.py

Remove new modules

rm backend/terminal_client/commands.py rm backend/terminal_client/arg_parser.py rm backend/terminal_client/completions.py rm backend/terminal_client/help_text.py

Verify system works

python -m terminal_client > new_game > quit

Incremental Rollback:

If only one component is problematic:

  1. Keep working components
  2. Revert only the problematic module
  3. Remove integration code for that module
  4. Continue with working improvements

C. Documentation Updates

  1. Update backend/terminal_client/CLAUDE.md

Add sections:

Recent Improvements (2025-10-27)

Shared Command Logic

All command implementations now use terminal_client/commands.py for:

  • Reduced code duplication (-500 lines)
  • Consistent behavior between REPL and CLI
  • Easier testing and maintenance

Usage: Commands automatically use shared logic

Robust Argument Parsing

Arguments now use shlex for proper parsing:

  • Handles quoted strings with spaces
  • Better error messages
  • Type validation

Example:

⚾ > defensive --alignment "shifted left"  # Now works!

Tab Completion

All commands support tab completion:
- Command names: new<TAB> → new_game
- Options: defensive --<TAB> → shows all options
- Values: --alignment <TAB> → shows valid alignments

Usage: Press TAB at any point for suggestions

Enhanced Help System

Detailed help with examples:
- help - List all commands
- help <command> - Detailed help with examples

Player Name Display (Future)

Infrastructure ready for Week 6 player models:
- Player cache system implemented
- Enhanced display functions ready
- Feature flags control activation

Activation: Set ENABLE_PLAYER_NAMES = True when ready

### 2. Create Migration Document

#### `backend/.claude/terminal_client_improvements.md`

```markdown
# Terminal Client Improvements - Implementation Log

## Summary

Six major improvements to terminal client:
1. Shared command logic (-500 lines duplication)
2. Robust argument parsing with shlex
3. Tab completion for all commands
4. Enhanced help system
5. Player cache infrastructure (inactive)
6. Comprehensive test suite

## Files Created

- `terminal_client/commands.py` (450 lines)
- `terminal_client/arg_parser.py` (250 lines)
- `terminal_client/completions.py` (350 lines)
- `terminal_client/help_text.py` (400 lines)
- `terminal_client/player_cache.py` (300 lines)
- `terminal_client/enhanced_display.py` (150 lines)

Total: ~1900 lines of new, tested code

## Files Modified

- `terminal_client/repl.py` (simplified, -200 lines)
- `terminal_client/main.py` (simplified, -150 lines)

Total: -350 lines of duplicated code

## Tests Added

- `test_commands.py` (200 lines)
- `test_arg_parser.py` (250 lines)
- `test_completions.py` (200 lines)
- `test_help_text.py` (150 lines)
- `test_player_cache.py` (200 lines)
- `test_integration.py` (300 lines)

Total: ~1300 lines of test code

## Test Coverage

- Unit tests: 80+ tests
- Integration tests: 15+ tests
- Coverage: ~95% of new code

## Performance Impact

- Argument parsing: < 0.1ms per command
- Tab completion: < 5ms per completion
- Player cache: < 0.01ms per lookup
- Help display: < 10ms

**No measurable impact on REPL responsiveness**

## Migration Status

- [x] Phase 1: Preparation
- [x] Phase 2: Argument Parser
- [x] Phase 3: Update REPL
- [x] Phase 4: Tab Completion
- [x] Phase 5: Help System
- [ ] Phase 6: Player Cache (waiting on Week 6)

## Known Issues

None

## Future Enhancements

1. Activate player cache when Week 6 models ready
2. Add command history persistence
3. Add command aliases (e.g., 'd' for defensive)
4. Add macro recording/playback for testing

---
D. Final Verification

Manual Testing Checklist

Run through this checklist before considering migration complete:

# 1. Start REPL
python -m terminal_client

# 2. Test help system
⚾ > help
⚾ > help new_game
⚾ > help defensive

# 3. Test tab completion
⚾ > new<TAB>           # Should complete to new_game
⚾ > defensive --<TAB>  # Should show all options
⚾ > defensive --alignment <TAB>  # Should show valid values

# 4. Test argument parsing
⚾ > new_game --league pd --home-team 5
⚾ > defensive --alignment shifted_left --hold 1,3
⚾ > offensive --approach power --hit-run

# 5. Test gameplay
⚾ > defensive
⚾ > offensive
⚾ > resolve
⚾ > status

# 6. Test quick play
⚾ > quick_play 10

# 7. Test error handling
⚾ > defensive --invalid value   # Should show clear error
⚾ > resolve                       # Should fail appropriately (no decisions)

# 8. Test cache commands (if activated)
⚾ > cache stats
⚾ > cache clear

# 9. Exit cleanly
⚾ > quit

Automated Test Run

# Run all tests
pytest backend/tests/unit/terminal_client/ -v --cov=terminal_client --cov-report=html

# Should see:
# - 80+ passing tests
# - ~95% coverage
# - 0 failures

---
E. Success Criteria

Migration is successful when:

- All 80+ tests pass
- Test coverage ≥ 95%
- REPL starts without errors
- All commands work as before
- Tab completion works
- Help system displays correctly
- Argument parsing handles edge cases
- No performance degradation
- Manual testing checklist passes
- Documentation updated
- No regressions in existing functionality

---
Summary

Total New Code: ~1900 lines
Code Removed: ~350 lines (duplicates)
Test Code: ~1300 lines
Net Result: Better, more maintainable, well-tested terminal client

Estimated Implementation Time: 2 weeks (1 developer)

Benefits:
- Reduced duplication
- Better UX (tab completion, help)
- Easier to maintain
- Ready for future enhancements
- Comprehensive test coverage

Risks: Low (incremental migration, full rollback plan)