feat: update test mocks for google-genai SDK (refs #235)

Replace engine._model/engine._generation_config mocks with
engine._client/engine._model_name. Update sys.modules patches
from vertexai to google.genai. Remove dead if-False branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-28 11:21:10 -06:00
parent 9f51e62b94
commit 1464a0e1af

View File

@@ -2,11 +2,11 @@
Covers: GeminiEngine initialization, PDF size validation, Covers: GeminiEngine initialization, PDF size validation,
successful extraction, empty results, and error handling. successful extraction, empty results, and error handling.
All Vertex AI SDK calls are mocked. All google-genai SDK calls are mocked.
""" """
import json import json
from unittest.mock import MagicMock, patch, PropertyMock from unittest.mock import MagicMock, patch
import pytest import pytest
@@ -156,22 +156,16 @@ class TestExtractMaintenance:
}, },
] ]
mock_model = MagicMock() mock_client = MagicMock()
mock_model.generate_content.return_value = _make_gemini_response(schedule) mock_client.models.generate_content.return_value = _make_gemini_response(schedule)
with ( with patch.dict("sys.modules", {
patch( "google.genai": MagicMock(),
"app.engines.gemini_engine.importlib_vertex_ai" "google.genai.types": MagicMock(),
) if False else patch.dict("sys.modules", { }):
"google.cloud": MagicMock(),
"google.cloud.aiplatform": MagicMock(),
"vertexai": MagicMock(),
"vertexai.generative_models": MagicMock(),
}),
):
engine = GeminiEngine() engine = GeminiEngine()
engine._model = mock_model engine._client = mock_client
engine._generation_config = MagicMock() engine._model_name = "gemini-2.5-flash"
result = engine.extract_maintenance(_make_pdf_bytes()) result = engine.extract_maintenance(_make_pdf_bytes())
@@ -200,12 +194,12 @@ class TestExtractMaintenance:
mock_settings.vertex_ai_location = "us-central1" mock_settings.vertex_ai_location = "us-central1"
mock_settings.gemini_model = "gemini-2.5-flash" mock_settings.gemini_model = "gemini-2.5-flash"
mock_model = MagicMock() mock_client = MagicMock()
mock_model.generate_content.return_value = _make_gemini_response([]) mock_client.models.generate_content.return_value = _make_gemini_response([])
engine = GeminiEngine() engine = GeminiEngine()
engine._model = mock_model engine._client = mock_client
engine._generation_config = MagicMock() engine._model_name = "gemini-2.5-flash"
result = engine.extract_maintenance(_make_pdf_bytes()) result = engine.extract_maintenance(_make_pdf_bytes())
@@ -223,12 +217,12 @@ class TestExtractMaintenance:
schedule = [{"serviceName": "Brake Fluid Replacement"}] schedule = [{"serviceName": "Brake Fluid Replacement"}]
mock_model = MagicMock() mock_client = MagicMock()
mock_model.generate_content.return_value = _make_gemini_response(schedule) mock_client.models.generate_content.return_value = _make_gemini_response(schedule)
engine = GeminiEngine() engine = GeminiEngine()
engine._model = mock_model engine._client = mock_client
engine._generation_config = MagicMock() engine._model_name = "gemini-2.5-flash"
result = engine.extract_maintenance(_make_pdf_bytes()) result = engine.extract_maintenance(_make_pdf_bytes())
@@ -264,7 +258,8 @@ class TestErrorHandling:
with ( with (
patch("app.engines.gemini_engine.settings") as mock_settings, patch("app.engines.gemini_engine.settings") as mock_settings,
patch.dict("sys.modules", { patch.dict("sys.modules", {
"google.cloud.aiplatform": None, "google": None,
"google.genai": None,
}), }),
): ):
mock_settings.google_vision_key_path = "/fake/creds.json" mock_settings.google_vision_key_path = "/fake/creds.json"
@@ -283,12 +278,12 @@ class TestErrorHandling:
mock_settings.vertex_ai_location = "us-central1" mock_settings.vertex_ai_location = "us-central1"
mock_settings.gemini_model = "gemini-2.5-flash" mock_settings.gemini_model = "gemini-2.5-flash"
mock_model = MagicMock() mock_client = MagicMock()
mock_model.generate_content.side_effect = RuntimeError("API quota exceeded") mock_client.models.generate_content.side_effect = RuntimeError("API quota exceeded")
engine = GeminiEngine() engine = GeminiEngine()
engine._model = mock_model engine._client = mock_client
engine._generation_config = MagicMock() engine._model_name = "gemini-2.5-flash"
with pytest.raises(GeminiProcessingError, match="maintenance extraction failed"): with pytest.raises(GeminiProcessingError, match="maintenance extraction failed"):
engine.extract_maintenance(_make_pdf_bytes()) engine.extract_maintenance(_make_pdf_bytes())
@@ -307,12 +302,12 @@ class TestErrorHandling:
mock_response = MagicMock() mock_response = MagicMock()
mock_response.text = "not valid json {{" mock_response.text = "not valid json {{"
mock_model = MagicMock() mock_client = MagicMock()
mock_model.generate_content.return_value = mock_response mock_client.models.generate_content.return_value = mock_response
engine = GeminiEngine() engine = GeminiEngine()
engine._model = mock_model engine._client = mock_client
engine._generation_config = MagicMock() engine._model_name = "gemini-2.5-flash"
with pytest.raises(GeminiProcessingError, match="invalid JSON"): with pytest.raises(GeminiProcessingError, match="invalid JSON"):
engine.extract_maintenance(_make_pdf_bytes()) engine.extract_maintenance(_make_pdf_bytes())
@@ -322,32 +317,32 @@ class TestErrorHandling:
class TestLazyInitialization: class TestLazyInitialization:
"""Verify the model is not created until first use.""" """Verify the client is not created until first use."""
def test_model_is_none_after_construction(self): def test_client_is_none_after_construction(self):
"""GeminiEngine should not initialize the model in __init__.""" """GeminiEngine should not initialize the client in __init__."""
engine = GeminiEngine() engine = GeminiEngine()
assert engine._model is None assert engine._client is None
@patch("app.engines.gemini_engine.settings") @patch("app.engines.gemini_engine.settings")
@patch("app.engines.gemini_engine.os.path.isfile", return_value=True) @patch("app.engines.gemini_engine.os.path.isfile", return_value=True)
def test_model_reused_on_second_call(self, mock_isfile, mock_settings): def test_client_reused_on_second_call(self, mock_isfile, mock_settings):
"""Once initialized, the same model instance is reused.""" """Once initialized, the same client instance is reused."""
mock_settings.google_vision_key_path = "/fake/creds.json" mock_settings.google_vision_key_path = "/fake/creds.json"
mock_settings.vertex_ai_project = "test-project" mock_settings.vertex_ai_project = "test-project"
mock_settings.vertex_ai_location = "us-central1" mock_settings.vertex_ai_location = "us-central1"
mock_settings.gemini_model = "gemini-2.5-flash" mock_settings.gemini_model = "gemini-2.5-flash"
schedule = [{"serviceName": "Oil Change", "intervalMiles": 5000}] schedule = [{"serviceName": "Oil Change", "intervalMiles": 5000}]
mock_model = MagicMock() mock_client = MagicMock()
mock_model.generate_content.return_value = _make_gemini_response(schedule) mock_client.models.generate_content.return_value = _make_gemini_response(schedule)
engine = GeminiEngine() engine = GeminiEngine()
engine._model = mock_model engine._client = mock_client
engine._generation_config = MagicMock() engine._model_name = "gemini-2.5-flash"
engine.extract_maintenance(_make_pdf_bytes()) engine.extract_maintenance(_make_pdf_bytes())
engine.extract_maintenance(_make_pdf_bytes()) engine.extract_maintenance(_make_pdf_bytes())
# Model's generate_content should have been called twice # Client's generate_content should have been called twice
assert mock_model.generate_content.call_count == 2 assert mock_client.models.generate_content.call_count == 2