hermes-agent/tests/tools/test_browser_content_none_guard.py
kshitijk4poor 66827f8947 chore: prune unused imports and duplicate import redefinitions
Remove unused imports (F401) and duplicate/shadowed import
redefinitions (F811) across the codebase using ruff's safe
autofixes. No behavioral changes -- imports only.

- ~1400 safe autofixes applied across 644 files (net -1072 lines)
- __init__.py re-exports preserved (excluded from F401 removal so
  public re-export surfaces stay intact)
- Re-exports that are imported or monkeypatched by tests but look
  unused in their defining module are kept with explicit # noqa:
  F401 (gateway/run.py load_dotenv; run_agent re-exports from
  agent.message_sanitization, agent.context_compressor,
  agent.retry_utils, agent.prompt_builder, agent.process_bootstrap,
  agent.codex_responses_adapter)
- Unsafe F841 (unused-variable) fixes deliberately skipped -- those
  can change behavior when the RHS has side effects
- ruff lints remain disabled in pyproject.toml (only PLW1514 is
  selected); this is a one-time cleanup, not a config change

Verification:
- python -m compileall: clean
- pytest --collect-only: all 27161 tests collect (zero import errors)
- core entry points import clean (run_agent, model_tools, cli,
  toolsets, hermes_state, batch_runner, gateway)
- static scan: every name any test imports directly from an edited
  module still resolves
2026-05-28 22:26:25 -07:00

108 lines
4.9 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 patch
# ── 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"
)