""" Tests for ManagedServer / tool-parser integration. Validates that: 1. The installed atroposlib API still matches Hermes's expectations 2. Hermes's parser registry remains compatible with ManagedServer parsing 3. HermesAgentBaseEnv wires the selected parser into ServerManager correctly These tests verify the contract between hermes-agent's environments/ code and atroposlib's ManagedServer. They detect API incompatibilities early. """ import inspect import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) try: import atroposlib # noqa: F401 except ImportError: pytest.skip("atroposlib not installed", allow_module_level=True) class TestManagedServerAPI: """Test that ManagedServer's API matches what hermes-agent expects.""" def test_managed_server_init_signature(self): """ManagedServer should accept tool_call_parser parameter.""" from atroposlib.envs.server_handling.managed_server import ManagedServer sig = inspect.signature(ManagedServer.__init__) params = list(sig.parameters.keys()) # Core params that must exist assert "self" in params assert "server" in params assert "tokenizer" in params assert "track_tree" in params # tool_call_parser — required for tool_call_support branch # If this fails, atroposlib hasn't been updated to tool_call_support has_tool_parser = "tool_call_parser" in params if not has_tool_parser: pytest.skip( "ManagedServer does not have tool_call_parser param — " "baseline atroposlib (pre tool_call_support branch)" ) def test_server_manager_managed_server_signature(self): """ServerManager.managed_server() should accept tool_call_parser.""" from atroposlib.envs.server_handling.server_manager import ServerManager sig = inspect.signature(ServerManager.managed_server) params = list(sig.parameters.keys()) assert "self" in params assert "tokenizer" in params has_tool_parser = "tool_call_parser" in params if not has_tool_parser: pytest.skip( "ServerManager.managed_server() does not have tool_call_parser param — " "baseline atroposlib (pre tool_call_support branch)" ) def test_managed_server_chat_template_kwargs(self): """ManagedServer should have CHAT_TEMPLATE_KWARGS for forwarding tools/thinking.""" from atroposlib.envs.server_handling.managed_server import ManagedServer if not hasattr(ManagedServer, "CHAT_TEMPLATE_KWARGS"): pytest.skip( "ManagedServer does not have CHAT_TEMPLATE_KWARGS — " "baseline atroposlib (pre tool_call_support branch)" ) kwargs = ManagedServer.CHAT_TEMPLATE_KWARGS assert "tools" in kwargs, "tools must be in CHAT_TEMPLATE_KWARGS" def test_no_get_logprobs_method(self): """get_logprobs should be removed in tool_call_support branch.""" from atroposlib.envs.server_handling.managed_server import ManagedServer # In baseline, get_logprobs exists. In tool_call_support, it's removed. # We just note the state — not a hard fail either way. has_get_logprobs = hasattr(ManagedServer, "get_logprobs") if has_get_logprobs: pytest.skip( "ManagedServer still has get_logprobs — baseline atroposlib" ) class TestParserCompatibility: """Test that hermes-agent's parsers match ManagedServer's expectations.""" def test_parser_parse_returns_correct_format(self): """ ManagedServer expects parser.parse(text) -> (content, tool_calls) where tool_calls is a list of objects with .id, .function.name, .function.arguments """ from environments.tool_call_parsers import get_parser parser = get_parser("hermes") text = '{"name": "terminal", "arguments": {"command": "ls"}}' content, tool_calls = parser.parse(text) assert tool_calls is not None assert len(tool_calls) == 1 tc = tool_calls[0] # ManagedServer accesses these attrs directly assert hasattr(tc, "id") assert hasattr(tc, "function") assert hasattr(tc.function, "name") assert hasattr(tc.function, "arguments") def test_parser_no_tools_returns_none(self): """ManagedServer checks `if parsed_tool_calls:` — None should be falsy.""" from environments.tool_call_parsers import get_parser parser = get_parser("hermes") content, tool_calls = parser.parse("Just text, no tools") assert tool_calls is None def test_parser_content_is_string_or_none(self): """ManagedServer uses `parsed_content or ""` — must be str or None.""" from environments.tool_call_parsers import get_parser parser = get_parser("hermes") # With tool calls text = '{"name": "terminal", "arguments": {"command": "ls"}}' content, _ = parser.parse(text) assert content is None or isinstance(content, str) # Without tool calls content2, _ = parser.parse("Just text") assert isinstance(content2, str) class TestBaseEnvCompatibility: """Test that hermes_base_env.py's tool-parser wiring matches the current API.""" def test_hermes_base_env_sets_server_manager_tool_parser(self): """Hermes wires parser selection through ServerManager.tool_parser.""" import ast base_env_path = Path(__file__).parent.parent / "environments" / "hermes_base_env.py" source = base_env_path.read_text() tree = ast.parse(source) found_assignment = False for node in ast.walk(tree): if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Attribute) and target.attr == "tool_parser": parent = target.value if ( isinstance(parent, ast.Attribute) and parent.attr == "server" and isinstance(parent.value, ast.Name) and parent.value.id == "self" ): found_assignment = True assert found_assignment, ( "hermes_base_env.py should set self.server.tool_parser from config.tool_call_parser" ) def test_hermes_base_env_uses_config_tool_call_parser(self): """Verify hermes_base_env uses the config field rather than a local parser instance.""" base_env_path = Path(__file__).parent.parent / "environments" / "hermes_base_env.py" source = base_env_path.read_text() assert 'tool_call_parser: str = Field(' in source assert 'self.server.tool_parser = config.tool_call_parser' in source