Private
Public Access
0
0
Files
manual_slop/tests/test_gemini_thinking_format.py
T
ed c5a119d63f refactor(ai_client): obliterate 5 legacy model-list wrappers (Phase 4)
Phase 4 (5 of 9 cruft sites obliterated):

OBLITERATED wrappers:
1. _reread_file_items (4 callers in _send_gemini + _send_gemini_cli + 2 others)
2. _list_anthropic_models (1 caller in list_models)
3. _list_gemini_models (1 caller in list_models)
4. _extract_gemini_thoughts (1 caller in _send_gemini)
5. _list_minimax_models (2 callers in _set_minimax_provider_result + set_provider)

Migration: each caller now uses the _result sibling directly with .ok check
+ .data extraction. The Result[T] error context (structured ErrorInfo) is now
propagated instead of dropped. _send_gemini gets .data with explicit .ok check.

Updated tests to assert OBLITERATED state (5 sub-track 5 tests inverted from
'_legacy_preserved' to '_legacy_obliterated'):
- tests/test_baseline_result.py: test_phase9_redo_modules_import_cleanly
- tests/tier2/phase10_invariant_test.py: _list_gemini_models removed from list
- tests/tier2/phase10_site1_test.py: _legacy_unchanged -> _legacy_obliterated
- tests/tier2/phase11_invariant_test.py: _extract/_list_minimax moved to obliterated
- tests/tier2/phase11_sites78_test.py: _legacy_preserved -> _legacy_obliterated
- tests/tier2/phase12_invariant_test.py: _list_anthropic moved to obliterated
- tests/tier2/phase12_site4_test.py: _legacy_preserved -> _legacy_obliterated
- tests/test_gemini_thinking_format.py: helper uses _result directly
- tests/test_cruft_removal.py: 5 new obliterated-wrappers invariant tests

Test result: 122/122 pass (31 baseline + 16 heuristic + 9 cruft + 5 thinking + 61 tier2).
Audit gate: src/ai_client.py --strict exits 0 (no new violations introduced).
Wrapper count: 9 -> 3 (Phase 5-6 remaining: rag_engine 1, gui_2 2).
2026-06-20 20:01:25 -04:00

85 lines
3.2 KiB
Python

"""Verify Gemini thinking content is properly extracted and wrapped for parse_thinking_trace.
The google-genai SDK separates thinking content from visible text by marking parts with
thought=True. The SDK filters these out of resp.text, so thinking monologues don't render
in the Discussion Hub. _send_gemini now calls _extract_gemini_thoughts and wraps the
extracted text in <thinking>...</thinking> tags so thinking_parser can extract a
ThinkingSegment.
"""
from unittest.mock import MagicMock, patch
from google.genai.types import Part, Content, Candidate, GenerateContentResponse
from src.ai_client import _extract_gemini_thoughts_result
from src.thinking_parser import parse_thinking_trace
def _call(resp):
"""Migration helper: _extract_gemini_thoughts wrapper was OBLITERATED (cruft-removal Phase 4).
Call _extract_gemini_thoughts_result(...).data directly; on failure return empty string.
"""
result = _extract_gemini_thoughts_result(resp)
return result.data if result.ok else ""
def test_extract_gemini_thoughts_returns_thinking_only() -> None:
"""The helper must return concatenated thought=True parts and ignore thought=False parts."""
resp = GenerateContentResponse(
candidates=[Candidate(content=Content(parts=[
Part(text="step 1 reasoning", thought=True),
Part(text="visible text 1", thought=False),
Part(text="step 2 reasoning", thought=True),
Part(text="visible text 2"),
]))]
)
thoughts = _call(resp)
assert thoughts == "step 1 reasoningstep 2 reasoning"
def test_extract_gemini_thoughts_returns_empty_when_no_thoughts() -> None:
"""No thought parts => empty string (the wrap is conditional)."""
resp = GenerateContentResponse(
candidates=[Candidate(content=Content(parts=[Part(text="just visible")]))]
)
assert _call(resp) == ""
def test_extract_gemini_thoughts_handles_missing_attributes() -> None:
"""Defensive: must not crash on objects without expected attributes."""
fake = MagicMock()
fake.candidates = [MagicMock()]
fake.candidates[0].content.parts = [MagicMock(thought=True, text="thinking text")]
assert _call(fake) == "thinking text"
fake.candidates = []
assert _call(fake) == ""
def test_gemini_thinking_segment_extractable_after_wrap() -> None:
"""End-to-end: the wrapped output must be parseable by thinking_parser.parse_thinking_trace."""
resp = GenerateContentResponse(
candidates=[Candidate(content=Content(parts=[
Part(text="my reasoning chain", thought=True),
Part(text="final answer"),
]))]
)
thoughts = _call(resp)
wrapped = f"<thinking>\n{thoughts}\n</thinking>\n\nfinal answer"
segments, response = parse_thinking_trace(wrapped)
assert len(segments) == 1
assert segments[0].content == "my reasoning chain"
assert segments[0].marker == "thinking"
assert response == "final answer"
def test_extract_gemini_thoughts_handles_none_resp() -> None:
"""Defensive: must not crash on None response."""
assert _call(None) == ""
if __name__ == "__main__":
test_extract_gemini_thoughts_returns_thinking_only()
test_extract_gemini_thoughts_returns_empty_when_no_thoughts()
test_extract_gemini_thoughts_handles_missing_attributes()
test_gemini_thinking_segment_extractable_after_wrap()
test_extract_gemini_thoughts_handles_none_resp()
print("All Gemini thinking format tests passed!")