hermes-agent/tests/tools/test_browser_content_none_guard.py
alt-glitch a1e667b9f2 fix(restructure): fix test regressions from import rewrite
Fix variable name breakage (run_agent, hermes_constants, etc.) where
import rewriter changed 'import X' to 'import hermes_agent.Y' but
test code still referenced 'X' as a variable name.

Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where
single files became directories.

Fix hardcoded file paths in tests pointing to old locations.
Fix tool registry to discover tools in subpackage directories.
Fix stale import in hermes_agent/tools/__init__.py.

Part of #14182, #14183
2026-04-23 12:05:10 +05:30

109 lines
5.1 KiB
Python

"""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("hermes_agent.tools.browser.tool.call_llm", return_value=_make_response(None)), \
patch("hermes_agent.tools.browser.tool._get_extraction_model", return_value="test-model"):
from hermes_agent.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("hermes_agent.tools.browser.tool.call_llm", return_value=_make_response("Extracted content here")), \
patch("hermes_agent.tools.browser.tool._get_extraction_model", return_value="test-model"):
from hermes_agent.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("hermes_agent.tools.browser.tool.call_llm", return_value=_make_response(" ")), \
patch("hermes_agent.tools.browser.tool._get_extraction_model", return_value="test-model"):
from hermes_agent.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, "hermes_agent", "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"
)