3838from google .genai import types
3939import pytest
4040
41+ BigQueryLoggerConfig = bigquery_logging_plugin .BigQueryLoggerConfig
42+
4143
4244class PluginTestBase :
4345 """Base class for plugin tests with common context setup."""
@@ -109,14 +111,20 @@ def setup_method(self, method):
109111 )
110112 self ._asyncio_to_thread_patch .start ()
111113
112- self .plugin = bigquery_logging_plugin .BigQueryAgentAnalyticsPlugin (
114+ self .plugin = asyncio .run (self ._create_plugin ())
115+
116+ async def _create_plugin (self , config = None ):
117+ plugin = bigquery_logging_plugin .BigQueryAgentAnalyticsPlugin (
113118 project_id = self .project_id ,
114119 dataset_id = self .dataset_id ,
115120 table_id = self .table_id ,
121+ config = config ,
116122 )
117- # Trigger lazy initialization by calling an async method once.
118- asyncio .run (self .plugin ._log_to_bigquery_async ({"event_type" : "INIT" }))
119- self .mock_bq_client .insert_rows_json .reset_mock ()
123+ if config is None or config .enabled :
124+ # Trigger lazy initialization by calling an async method once.
125+ await plugin ._log_to_bigquery_async ({"event_type" : "INIT" })
126+ self .mock_bq_client .insert_rows_json .reset_mock ()
127+ return plugin
120128
121129 def _get_logged_entry (self ):
122130 """Helper to get the single logged entry from the mocked client."""
@@ -134,6 +142,98 @@ def _assert_common_fields(self, log_entry, event_type):
134142 assert log_entry ["user_id" ] == "user-456"
135143 assert log_entry ["timestamp" ] is not None
136144
145+ @pytest .mark .asyncio
146+ async def test_plugin_disabled (self ):
147+ self .mock_bq_client_cls .reset_mock ()
148+ config = BigQueryLoggerConfig (enabled = False )
149+ plugin = await self ._create_plugin (config )
150+ user_message = types .Content (parts = [types .Part (text = "Test" )])
151+ await plugin .on_user_message_callback (
152+ invocation_context = self .invocation_context , user_message = user_message
153+ )
154+ self .mock_bq_client_cls .assert_not_called ()
155+ self .mock_bq_client .insert_rows_json .assert_not_called ()
156+
157+ @pytest .mark .asyncio
158+ async def test_event_allowlist (self ):
159+ config = BigQueryLoggerConfig (event_allowlist = ["LLM_REQUEST" ])
160+ plugin = await self ._create_plugin (config )
161+
162+ # This should be logged
163+ llm_request = llm_request_lib .LlmRequest (
164+ model = "gemini-pro" ,
165+ contents = [types .Content (parts = [types .Part (text = "Prompt" )])],
166+ )
167+ await plugin .before_model_callback (
168+ callback_context = self .callback_context , llm_request = llm_request
169+ )
170+ self .mock_bq_client .insert_rows_json .assert_called_once ()
171+ self .mock_bq_client .insert_rows_json .reset_mock ()
172+
173+ # This should NOT be logged
174+ user_message = types .Content (parts = [types .Part (text = "What is up?" )])
175+ await plugin .on_user_message_callback (
176+ invocation_context = self .invocation_context , user_message = user_message
177+ )
178+ self .mock_bq_client .insert_rows_json .assert_not_called ()
179+
180+ @pytest .mark .asyncio
181+ async def test_event_denylist (self ):
182+ config = BigQueryLoggerConfig (event_denylist = ["USER_MESSAGE_RECEIVED" ])
183+ plugin = await self ._create_plugin (config )
184+
185+ # This should NOT be logged
186+ user_message = types .Content (parts = [types .Part (text = "What is up?" )])
187+ await plugin .on_user_message_callback (
188+ invocation_context = self .invocation_context , user_message = user_message
189+ )
190+ self .mock_bq_client .insert_rows_json .assert_not_called ()
191+
192+ # This should be logged
193+ await plugin .before_run_callback (invocation_context = self .invocation_context )
194+ self .mock_bq_client .insert_rows_json .assert_called_once ()
195+
196+ @pytest .mark .asyncio
197+ async def test_content_formatter (self ):
198+ def redact_content (content ):
199+ return "[REDACTED]"
200+
201+ config = BigQueryLoggerConfig (content_formatter = redact_content )
202+ plugin = await self ._create_plugin (config )
203+
204+ user_message = types .Content (parts = [types .Part (text = "Secret message" )])
205+ await plugin .on_user_message_callback (
206+ invocation_context = self .invocation_context , user_message = user_message
207+ )
208+
209+ log_entry = self ._get_logged_entry ()
210+ self ._assert_common_fields (log_entry , "USER_MESSAGE_RECEIVED" )
211+ assert log_entry ["content" ] == "[REDACTED]"
212+
213+ @pytest .mark .asyncio
214+ async def test_content_formatter_error (self ):
215+ def error_formatter (content ):
216+ raise ValueError ("Formatter failed" )
217+
218+ config = BigQueryLoggerConfig (content_formatter = error_formatter )
219+ plugin = await self ._create_plugin (config )
220+
221+ user_message = types .Content (parts = [types .Part (text = "Test" )])
222+ with mock .patch .object (logging , "warning" ) as mock_log_warning :
223+ await plugin .on_user_message_callback (
224+ invocation_context = self .invocation_context , user_message = user_message
225+ )
226+ mock_log_warning .assert_called_once_with (
227+ "Error applying custom content formatter for event type %s: %s" ,
228+ "USER_MESSAGE_RECEIVED" ,
229+ mock .ANY ,
230+ )
231+
232+ log_entry = self ._get_logged_entry ()
233+ # Content should be a string, even if formatter failed
234+ assert isinstance (log_entry ["content" ], str )
235+ assert "User Content: text: 'Test'" in log_entry ["content" ]
236+
137237 @pytest .mark .asyncio
138238 async def test_on_user_message_callback_logs_correctly (self ):
139239 user_message = types .Content (parts = [types .Part (text = "What is up?" )])
@@ -371,8 +471,9 @@ async def test_after_tool_callback_logs_correctly(self):
371471
372472 @pytest .mark .asyncio
373473 async def test_on_model_error_callback_logs_correctly (self ):
374- llm_request = mock .create_autospec (
375- llm_request_lib .LlmRequest , instance = True
474+ llm_request = llm_request_lib .LlmRequest (
475+ model = "gemini-pro" ,
476+ contents = [types .Content (parts = [types .Part (text = "Prompt" )])],
376477 )
377478 error = ValueError ("LLM failed" )
378479 await self .plugin .on_model_error_callback (
@@ -382,21 +483,26 @@ async def test_on_model_error_callback_logs_correctly(self):
382483 )
383484 log_entry = self ._get_logged_entry ()
384485 self ._assert_common_fields (log_entry , "LLM_ERROR" )
385- assert log_entry ["content" ] is None
486+ assert (
487+ log_entry ["content" ] is None
488+ or "Request Content: " in log_entry ["content" ]
489+ )
386490 assert log_entry ["error_message" ] == "LLM failed"
387491
388492 @pytest .mark .asyncio
389493 async def test_on_tool_error_callback_logs_correctly (self ):
390494 mock_tool = mock .create_autospec (base_tool_lib .BaseTool , instance = True )
391495 mock_tool .name = "MyTool"
496+ tool_args = {"param" : "value" }
392497 error = TimeoutError ("Tool timed out" )
393498 await self .plugin .on_tool_error_callback (
394499 tool = mock_tool ,
395- tool_args = { "param" : "value" } ,
500+ tool_args = tool_args ,
396501 tool_context = self .tool_context ,
397502 error = error ,
398503 )
399504 log_entry = self ._get_logged_entry ()
400505 self ._assert_common_fields (log_entry , "TOOL_ERROR" )
401- assert log_entry ["content" ] == "Tool Name: MyTool"
506+ assert "Tool Name: MyTool" in log_entry ["content" ]
507+ assert "Arguments: {'param': 'value'}" in log_entry ["content" ]
402508 assert log_entry ["error_message" ] == "Tool timed out"
0 commit comments