"""Tests for None guard on browser_tool LLM response content. browser_tool.py has two call sites that access response.choices[0].message.content without checking for None — _extract_relevant_content (line 996) and browser_vision (line 1626). When reasoning-only models (DeepSeek-R1, QwQ) return content=None, these produce null snapshots or null analysis. These tests verify both sites are guarded. """ import types from unittest.mock import MagicMock, patch import pytest # ── helpers ──────────────────────────────────────────────────────────────── def _make_response(content): """Build a minimal OpenAI-compatible ChatCompletion response stub.""" message = types.SimpleNamespace(content=content) choice = types.SimpleNamespace(message=message) return types.SimpleNamespace(choices=[choice]) # ── _extract_relevant_content (line 996) ────────────────────────────────── class TestExtractRelevantContentNoneGuard: """tools/browser_tool.py — _extract_relevant_content()""" def test_none_content_falls_back_to_truncated(self): """When LLM returns None content, should fall back to truncated snapshot.""" with patch("tools.browser_tool.call_llm", return_value=_make_response(None)), \ patch("tools.browser_tool._get_extraction_model", return_value="test-model"): from tools.browser_tool import _extract_relevant_content result = _extract_relevant_content("This is a long snapshot text", "find the button") assert result is not None assert isinstance(result, str) assert len(result) > 0 def test_normal_content_returned(self): """Normal string content should pass through.""" with patch("tools.browser_tool.call_llm", return_value=_make_response("Extracted content here")), \ patch("tools.browser_tool._get_extraction_model", return_value="test-model"): from tools.browser_tool import _extract_relevant_content result = _extract_relevant_content("snapshot text", "task") assert result == "Extracted content here" def test_empty_string_content_falls_back(self): """Empty string content should also fall back to truncated.""" with patch("tools.browser_tool.call_llm", return_value=_make_response(" ")), \ patch("tools.browser_tool._get_extraction_model", return_value="test-model"): from tools.browser_tool import _extract_relevant_content result = _extract_relevant_content("This is a long snapshot text", "task") assert result is not None assert len(result) > 0 # ── browser_vision (line 1626) ──────────────────────────────────────────── class TestBrowserVisionNoneGuard: """tools/browser_tool.py — browser_vision() analysis extraction""" def test_none_content_produces_fallback_message(self): """When LLM returns None content, analysis should have a fallback message.""" response = _make_response(None) analysis = (response.choices[0].message.content or "").strip() fallback = analysis or "Vision analysis returned no content." assert fallback == "Vision analysis returned no content." def test_normal_content_passes_through(self): """Normal analysis content should pass through unchanged.""" response = _make_response(" The page shows a login form. ") analysis = (response.choices[0].message.content or "").strip() fallback = analysis or "Vision analysis returned no content." assert fallback == "The page shows a login form." # ── source line verification ────────────────────────────────────────────── class TestBrowserSourceLinesAreGuarded: """Verify the actual source file has the fix applied.""" @staticmethod def _read_file() -> str: import os base = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) with open(os.path.join(base, "tools", "browser_tool.py")) as f: return f.read() def test_extract_relevant_content_guarded(self): src = self._read_file() # The old unguarded pattern should NOT exist assert "return response.choices[0].message.content\n" not in src, ( "browser_tool.py _extract_relevant_content still has unguarded " ".content return — apply None guard" ) def test_browser_vision_guarded(self): src = self._read_file() assert "analysis = response.choices[0].message.content\n" not in src, ( "browser_tool.py browser_vision still has unguarded " ".content assignment — apply None guard" )