"""Tests for markitect.llm.gemini — retry behavior + happy path.""" from unittest import mock import pytest from markitect.llm.gemini import GeminiAdapter from markitect.llm.exceptions import LLMAPIError, LLMRateLimitError from markitect.prompts.execution.models import RunConfig, LLMResponse def _api_response(text="hello", model="gemini-2.5-flash"): return { "candidates": [ { "content": {"parts": [{"text": text}], "role": "model"}, "finishReason": "STOP", } ], "modelVersion": model, "usageMetadata": { "promptTokenCount": 3, "candidatesTokenCount": 2, "totalTokenCount": 5, }, } class TestGeminiAdapter: def _adapter(self, **kwargs): defaults = {"api_key": "AIza-test"} defaults.update(kwargs) return GeminiAdapter(**defaults) @mock.patch("markitect.llm.gemini.post_json") def test_success(self, mock_post): mock_post.return_value = _api_response("generated") adapter = self._adapter() resp = adapter.execute_prompt("hi", RunConfig()) assert isinstance(resp, LLMResponse) assert resp.content == "generated" assert resp.metadata["provider"] == "gemini" @mock.patch("markitect.llm.gemini.post_json") @mock.patch("markitect.llm.gemini.time.sleep") def test_retry_on_429(self, mock_sleep, mock_post): mock_post.side_effect = [ LLMRateLimitError("rate limited", status_code=429), _api_response("recovered"), ] adapter = self._adapter(max_retries=2) resp = adapter.execute_prompt("hi", RunConfig()) assert resp.content == "recovered" assert mock_sleep.call_count == 1 @mock.patch("markitect.llm.gemini.post_json") @mock.patch("markitect.llm.gemini.time.sleep") def test_retry_on_503(self, mock_sleep, mock_post): mock_post.side_effect = [ LLMAPIError("unavailable", status_code=503), _api_response("back"), ] adapter = self._adapter(max_retries=2) resp = adapter.execute_prompt("hi", RunConfig()) assert resp.content == "back" @mock.patch("markitect.llm.gemini.post_json") def test_no_retry_on_400(self, mock_post): mock_post.side_effect = LLMAPIError("bad request", status_code=400) adapter = self._adapter(max_retries=2) with pytest.raises(LLMAPIError) as exc_info: adapter.execute_prompt("hi", RunConfig()) assert exc_info.value.status_code == 400 @mock.patch("markitect.llm.gemini.post_json") @mock.patch("markitect.llm.gemini.time.sleep") def test_exhausted_retries_raises(self, mock_sleep, mock_post): mock_post.side_effect = LLMRateLimitError("rate limited", status_code=429) adapter = self._adapter(max_retries=1) with pytest.raises(LLMRateLimitError): adapter.execute_prompt("hi", RunConfig()) assert mock_sleep.call_count == 1 # 1 retry before giving up