mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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
109 lines
5.1 KiB
Python
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"
|
|
)
|