|
1 | 1 | import json |
2 | 2 | import os |
3 | 3 | import random |
| 4 | +import re |
4 | 5 | from pathlib import Path |
| 6 | +from unittest.mock import MagicMock, patch |
5 | 7 |
|
6 | 8 | import pytest |
7 | 9 |
|
@@ -98,3 +100,55 @@ def mocked_lms(recorder_dir): |
98 | 100 | @pytest.fixture |
99 | 101 | def rng(): |
100 | 102 | return random.Random(42) |
| 103 | + |
| 104 | + |
| 105 | +def _make_litellm_mock_response(content: str): |
| 106 | + """Create a mock litellm.completion response.""" |
| 107 | + resp = MagicMock() |
| 108 | + resp.choices = [MagicMock()] |
| 109 | + resp.choices[0].message.content = content |
| 110 | + resp.choices[0].finish_reason = "stop" |
| 111 | + return resp |
| 112 | + |
| 113 | + |
| 114 | +@pytest.fixture(autouse=True) |
| 115 | +def _mock_litellm_for_llm_call_tests(request): |
| 116 | + """Auto-mock litellm.completion for tests marked with @pytest.mark.llm_call. |
| 117 | +
|
| 118 | + Returns a JSON dict with randomized values that satisfies both the reflection |
| 119 | + proposer (expects fenced code block) and the refiner (expects JSON dict). |
| 120 | + """ |
| 121 | + marker = request.node.get_closest_marker("llm_call") |
| 122 | + if marker is None: |
| 123 | + yield |
| 124 | + return |
| 125 | + |
| 126 | + call_rng = random.Random(42) |
| 127 | + |
| 128 | + def fake_completion(*_args, **kwargs): |
| 129 | + messages = kwargs.get("messages", []) |
| 130 | + last_content = messages[-1]["content"] if messages else "" |
| 131 | + |
| 132 | + # Check for multi-param candidates |
| 133 | + param_a_match = re.search(r'"param_a"\s*:\s*"(\d+)"', last_content) |
| 134 | + param_b_match = re.search(r'"param_b"\s*:\s*"(\d+)"', last_content) |
| 135 | + if param_a_match and param_b_match: |
| 136 | + a = int(param_a_match.group(1)) + call_rng.randint(-5, 5) |
| 137 | + b = 100 - a + call_rng.randint(-3, 3) |
| 138 | + response_text = json.dumps({"param_a": str(a), "param_b": str(b)}) |
| 139 | + else: |
| 140 | + # Each call returns a unique candidate so the cache can't stall budget |
| 141 | + number_match = re.search(r'"number"\s*:\s*"(\d+)"', last_content) |
| 142 | + if number_match: |
| 143 | + old = int(number_match.group(1)) |
| 144 | + new_number = old + call_rng.randint(-10, 10) |
| 145 | + else: |
| 146 | + new_number = 42 + call_rng.randint(-10, 10) |
| 147 | + response_text = json.dumps({"number": str(new_number)}) |
| 148 | + |
| 149 | + # Wrap in fenced block for reflection proposer compatibility |
| 150 | + content = f"```\n{response_text}\n```" |
| 151 | + return _make_litellm_mock_response(content) |
| 152 | + |
| 153 | + with patch("litellm.completion", side_effect=fake_completion): |
| 154 | + yield |
0 commit comments