| """Unit tests for the Prompt Builder module (Issue #7).""" |
|
|
| import sys |
| from pathlib import Path |
|
|
| import pandas as pd |
| import pytest |
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent.parent)) |
|
|
| from prompts.builder import ( |
| build_commentary_prompt, |
| build_persona_prompt, |
| build_strategy_prompt, |
| _ANTI_HALLUCINATION, |
| _MINICPM_TOKEN_LIMIT, |
| _NEMOTRON_TOKEN_LIMIT, |
| _CHARS_PER_TOKEN, |
| ) |
| from data.race_data import WINDOW_COLUMNS |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture() |
| def lap_df() -> pd.DataFrame: |
| rows = [] |
| for lap in range(41, 51): |
| for driver, pos, gap in [("VER", 1, 0.0), ("HAM", 2, 6.2)]: |
| rows.append({ |
| "lap_number": lap, |
| "driver_code": driver, |
| "position": pos, |
| "gap_to_leader_s": gap, |
| "compound": "MEDIUM", |
| "tyre_life": lap - 30, |
| "lap_time_s": 90.1 + (lap * 0.01), |
| "sc_active": False, |
| }) |
| return pd.DataFrame(rows, columns=WINDOW_COLUMNS) |
|
|
|
|
| |
| |
| |
|
|
| class TestBroadcastMode: |
| def test_returns_string(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "broadcast") |
| assert isinstance(result, str) |
|
|
| def test_team_name_interpolated(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "broadcast") |
| assert "Oracle Red Bull Racing" in result |
|
|
| def test_lap_data_interpolated(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "broadcast") |
| assert "VER" in result |
| assert "HAM" in result |
|
|
| def test_broadcast_tone_marker_present(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "broadcast") |
| |
| assert "broadcast" in result.lower() |
|
|
| def test_within_minicpm_context_window(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "broadcast") |
| assert len(result) <= _MINICPM_TOKEN_LIMIT * _CHARS_PER_TOKEN |
|
|
|
|
| |
| |
| |
|
|
| class TestRadioMode: |
| def test_returns_string(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Mercedes-AMG Petronas", "radio") |
| assert isinstance(result, str) |
|
|
| def test_team_name_interpolated(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Mercedes-AMG Petronas", "radio") |
| assert "Mercedes-AMG Petronas" in result |
|
|
| def test_radio_tone_marker_present(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Mercedes-AMG Petronas", "radio") |
| |
| assert "radio" in result.lower() or "race engineer" in result.lower() |
|
|
| def test_within_minicpm_context_window(self, lap_df): |
| result = build_commentary_prompt(lap_df, "Mercedes-AMG Petronas", "radio") |
| assert len(result) <= _MINICPM_TOKEN_LIMIT * _CHARS_PER_TOKEN |
|
|
|
|
| |
| |
| |
|
|
| class TestBroadcastVsRadioStructure: |
| def test_prompts_are_different(self, lap_df): |
| broadcast = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "broadcast") |
| radio = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "radio") |
| assert broadcast != radio |
|
|
| def test_different_instruction_sections(self, lap_df): |
| broadcast = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "broadcast") |
| radio = build_commentary_prompt(lap_df, "Oracle Red Bull Racing", "radio") |
| |
| assert "television" in broadcast.lower() or "global audience" in broadcast.lower() |
| assert "terse" in radio.lower() or "clipped" in radio.lower() or "race engineer" in radio.lower() |
|
|
| def test_invalid_mode_raises(self, lap_df): |
| with pytest.raises(ValueError, match="Unknown commentary mode"): |
| build_commentary_prompt(lap_df, "Red Bull", "freestyle") |
|
|
|
|
| |
| |
| |
|
|
| class TestStrategyMode: |
| WHAT_IF = "What if Hamilton had pitted 5 laps earlier on lap 30 for fresh mediums?" |
|
|
| def test_returns_string(self, lap_df): |
| result = build_strategy_prompt(lap_df, self.WHAT_IF) |
| assert isinstance(result, str) |
|
|
| def test_lap_data_interpolated(self, lap_df): |
| result = build_strategy_prompt(lap_df, self.WHAT_IF) |
| assert "VER" in result |
| assert "HAM" in result |
|
|
| def test_what_if_variable_interpolated(self, lap_df): |
| result = build_strategy_prompt(lap_df, self.WHAT_IF) |
| assert self.WHAT_IF.strip() in result |
|
|
| def test_anti_hallucination_instruction_present(self, lap_df): |
| result = build_strategy_prompt(lap_df, self.WHAT_IF) |
| assert _ANTI_HALLUCINATION in result |
|
|
| def test_anti_hallucination_references_invented_values(self, lap_df): |
| result = build_strategy_prompt(lap_df, self.WHAT_IF) |
| assert "invent" in result.lower() or "fabricat" in result.lower() |
|
|
| def test_within_nemotron_context_window(self, lap_df): |
| result = build_strategy_prompt(lap_df, self.WHAT_IF) |
| assert len(result) <= _NEMOTRON_TOKEN_LIMIT * _CHARS_PER_TOKEN |
|
|
|
|
| |
| |
| |
|
|
| class TestPersonaMode: |
| def test_returns_string_for_active_driver(self): |
| result = build_persona_prompt("verstappen") |
| assert isinstance(result, str) |
| assert len(result) > 100 |
|
|
| def test_cutoff_block_appended_for_active_driver(self): |
| for driver in ["verstappen", "hamilton", "norris"]: |
| result = build_persona_prompt(driver) |
| assert "Knowledge Cutoff" in result, f"{driver}: missing cutoff block" |
| assert "end of 2023" in result, f"{driver}: wrong cutoff year" |
|
|
| def test_cutoff_block_appended_for_historical_drivers(self): |
| result_senna = build_persona_prompt("senna") |
| assert "Knowledge Cutoff" in result_senna |
| assert "1994" in result_senna |
|
|
| result_schumi = build_persona_prompt("schumacher") |
| assert "Knowledge Cutoff" in result_schumi |
| assert "2012" in result_schumi |
|
|
| def test_race_context_injected_for_active_driver(self): |
| ctx = "2023 Dutch GP, Lap 42. Verstappen leads by 4.8s." |
| result = build_persona_prompt("verstappen", race_context=ctx) |
| assert ctx in result |
| assert "Current Race Context" in result |
|
|
| def test_race_context_absent_for_historical_driver(self): |
| ctx = "2023 Dutch GP, Lap 42." |
| result = build_persona_prompt("senna", race_context=ctx) |
| assert ctx not in result |
| assert "Current Race Context" not in result |
|
|
| result_s = build_persona_prompt("schumacher", race_context=ctx) |
| assert ctx not in result_s |
|
|
| def test_cutoff_block_comes_after_persona_text(self): |
| result = build_persona_prompt("verstappen") |
| persona_end = result.index("Knowledge Cutoff") |
| |
| assert persona_end > 200 |
|
|
| def test_missing_driver_raises_file_not_found(self): |
| with pytest.raises(FileNotFoundError): |
| build_persona_prompt("alonso") |
|
|
| def test_within_minicpm_context_window(self): |
| result = build_persona_prompt("verstappen") |
| assert len(result) <= _MINICPM_TOKEN_LIMIT * _CHARS_PER_TOKEN |
|
|
|
|
| |
| |
| |
|
|
| class TestPromptLengthValidation: |
| def test_oversized_strategy_prompt_raises(self): |
| |
| rows = [] |
| for lap in range(1, 5001): |
| rows.append({ |
| "lap_number": lap, |
| "driver_code": "VER", |
| "position": 1, |
| "gap_to_leader_s": 0.0, |
| "compound": "HARD", |
| "tyre_life": lap, |
| "lap_time_s": 90.0, |
| "sc_active": False, |
| }) |
| huge_df = pd.DataFrame(rows, columns=WINDOW_COLUMNS) |
| with pytest.raises(ValueError, match="context window"): |
| build_strategy_prompt(huge_df, "any change") |
|
|
| def test_max_10lap_2driver_strategy_within_bounds(self, lap_df): |
| |
| result = build_strategy_prompt(lap_df, "Hamilton pits 5 laps earlier.") |
| assert len(result) <= _NEMOTRON_TOKEN_LIMIT * _CHARS_PER_TOKEN |
|
|