@@ -84,3 +84,32 @@ def test_rate_limit_reports_to_global_limiter():
8484 max_tokens = 8 ,
8585 )
8686 assert limiter .is_in_backoff (), "Google 429 must trigger global backoff (H1)"
87+
88+
89+ def test_present_candidate_with_empty_parts_raises_not_clean_end_turn ():
90+ # A candidate present with a clean STOP finish but NO usable parts (thinking-only
91+ # or blank) must raise -- returning an empty end_turn reads as a clean, passing
92+ # result for a security tool (silent false-negative). Mirrors the no-candidates
93+ # guard and the Anthropic/OpenAI empty-content guards.
94+ from types import SimpleNamespace
95+ from utilities .llm import LLMResponseError
96+ from utilities .llm .providers .google import _response_to_unified
97+ cand = SimpleNamespace (finish_reason = "STOP" , content = SimpleNamespace (parts = []))
98+ resp = SimpleNamespace (candidates = [cand ], usage_metadata = SimpleNamespace (
99+ prompt_token_count = 1 , candidates_token_count = 0 , total_token_count = 1 ))
100+ with pytest .raises (LLMResponseError ):
101+ _response_to_unified (resp )
102+
103+
104+ def test_tool_use_only_candidate_is_valid_not_empty ():
105+ # Control: a function_call part with no text is a VALID response (content
106+ # non-empty) and must NOT be caught by the empty-content guard.
107+ from types import SimpleNamespace
108+ from utilities .llm .providers .google import _response_to_unified
109+ fc = SimpleNamespace (name = "do_it" , args = {"x" : 1 }, id = "g1" )
110+ part = SimpleNamespace (text = None , function_call = fc )
111+ cand = SimpleNamespace (finish_reason = "STOP" , content = SimpleNamespace (parts = [part ]))
112+ resp = SimpleNamespace (candidates = [cand ], usage_metadata = SimpleNamespace (
113+ prompt_token_count = 1 , candidates_token_count = 1 , total_token_count = 2 ))
114+ result = _response_to_unified (resp )
115+ assert result .content and result .stop_reason == "tool_use"
0 commit comments