| """Tests for the What-If tab logic in app.py.""" |
|
|
| import math |
| import types |
| import unittest |
| from unittest.mock import MagicMock, patch |
|
|
| import pandas as pd |
| import sys |
| import os |
|
|
| |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
|
|
| |
| from app import ( |
| _build_timing_table, |
| _build_strategy_prompt, |
| _run_what_if, |
| _PIVOT_LAP_MIN, |
| _PIVOT_LAP_MAX, |
| ) |
|
|
|
|
| def _make_window(n_rows=3) -> pd.DataFrame: |
| return pd.DataFrame( |
| { |
| "lap_number": [30, 30, 31], |
| "driver_code": ["VER", "HAM", "VER"], |
| "position": [1, 2, 1], |
| "gap_to_leader_s": [float("nan"), 3.412, float("nan")], |
| "compound": ["MEDIUM", "HARD", "MEDIUM"], |
| "tyre_life": [10, 14, 11], |
| "lap_time_s": [88.1, 88.9, 88.0], |
| "sc_active": [False, False, False], |
| } |
| ) |
|
|
|
|
| class TestBuildTimingTable(unittest.TestCase): |
| def test_header_row_present(self): |
| table = _build_timing_table(_make_window()) |
| assert "LAP" in table |
| assert "DRV" in table |
| assert "CMPD" in table |
|
|
| def test_leader_label(self): |
| table = _build_timing_table(_make_window()) |
| assert "LEADER" in table |
|
|
| def test_gap_formatted(self): |
| table = _build_timing_table(_make_window()) |
| assert "3.412" in table |
|
|
| def test_empty_dataframe(self): |
| result = _build_timing_table(pd.DataFrame()) |
| assert "No data" in result |
|
|
|
|
| class TestBuildStrategyPrompt(unittest.TestCase): |
| def _race(self): |
| return { |
| "name": "British Grand Prix", |
| "season": 2023, |
| "circuit": "Silverstone", |
| "top_finishers": "Verstappen, Hamilton, Alonso", |
| } |
|
|
| def test_fields_injected(self): |
| table = "LAP DRV POS GAP(s) CMPD AGE" |
| prompt = _build_strategy_prompt(self._race(), 35, "What if HAM pitted earlier?", table) |
| assert "British Grand Prix" in prompt |
| assert "2023" in prompt |
| assert "Silverstone" in prompt |
| assert "35" in prompt |
| assert "What if HAM pitted earlier?" in prompt |
| assert table in prompt |
|
|
|
|
| class TestRunWhatIf(unittest.TestCase): |
| """Test the generator function _run_what_if without hitting Modal.""" |
|
|
| def _collect(self, gen): |
| return list(gen) |
|
|
| def test_empty_whatif_returns_prompt(self): |
| race = {"season": 2023, "round": 5, "name": "X", "circuit": "Y"} |
| results = self._collect(_run_what_if(race, 30, " ")) |
| left, right = results[0] |
| assert "Enter" in left |
|
|
| def test_pivot_below_min_shows_warning(self): |
| race = {"season": 2023, "round": 5, "name": "X", "circuit": "Y"} |
| results = self._collect(_run_what_if(race, _PIVOT_LAP_MIN - 1, "some scenario")) |
| left, right = results[0] |
| assert "outside the recommended range" in left |
| assert right == "" |
|
|
| def test_pivot_above_max_shows_warning(self): |
| race = {"season": 2023, "round": 5, "name": "X", "circuit": "Y"} |
| results = self._collect(_run_what_if(race, _PIVOT_LAP_MAX + 1, "some scenario")) |
| left, right = results[0] |
| assert "outside the recommended range" in left |
| assert right == "" |
|
|
| def test_pivot_at_boundary_passes_validation(self): |
| race = {"season": 2023, "round": 5, "name": "X", "circuit": "Y"} |
| window_df = _make_window() |
| with patch("app.get_race_window", return_value=window_df), \ |
| patch("app.call_reason_strategy", return_value={"reasoning_chain": "Step 1. Step 2."}): |
| results = self._collect(_run_what_if(race, _PIVOT_LAP_MIN, "HAM earlier pit?")) |
| |
| final_left, final_right = results[-1] |
| assert "Step 1" in final_right |
|
|
| def test_file_not_found_surfaces_error(self): |
| race = {"season": 2023, "round": 5, "name": "X", "circuit": "Y"} |
| with patch("app.get_race_window", side_effect=FileNotFoundError("No cache")): |
| results = self._collect(_run_what_if(race, 30, "some scenario")) |
| |
| left, right = results[-1] |
| assert "No cache" in left |
|
|
| def test_timing_table_in_left_panel_on_success(self): |
| race = {"season": 2023, "round": 5, "name": "X", "circuit": "Y"} |
| window_df = _make_window() |
| with patch("app.get_race_window", return_value=window_df), \ |
| patch("app.call_reason_strategy", return_value={"reasoning_chain": "Analysis done."}): |
| results = self._collect(_run_what_if(race, 30, "What if VER pitted?")) |
| final_left, final_right = results[-1] |
| assert "LAP" in final_left |
| assert "Analysis done." in final_right |
|
|
| def test_no_audio_component(self): |
| """Right panel is plain text — no audio output from _run_what_if.""" |
| race = {"season": 2023, "round": 5, "name": "X", "circuit": "Y"} |
| window_df = _make_window() |
| with patch("app.get_race_window", return_value=window_df), \ |
| patch("app.call_reason_strategy", return_value={"reasoning_chain": "Reasoning."}): |
| results = self._collect(_run_what_if(race, 30, "What if?")) |
| for left, right in results: |
| |
| assert isinstance(right, str) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main() |
|
|