diff --git a/test/log/__pycache__/test_logger.cpython-310-pytest-9.0.3.pyc b/test/log/__pycache__/test_logger.cpython-310-pytest-9.0.3.pyc new file mode 100644 index 00000000..19897c73 Binary files /dev/null and b/test/log/__pycache__/test_logger.cpython-310-pytest-9.0.3.pyc differ diff --git a/test/log/__pycache__/test_logger.cpython-313-pytest-9.0.2.pyc b/test/log/__pycache__/test_logger.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 00000000..2882287c Binary files /dev/null and b/test/log/__pycache__/test_logger.cpython-313-pytest-9.0.2.pyc differ diff --git a/test/log/test_logger.py b/test/log/test_logger.py new file mode 100644 index 00000000..c73b4af1 --- /dev/null +++ b/test/log/test_logger.py @@ -0,0 +1,72 @@ +"""Tests for rdagent/log/logger.py — RDAgentLog wrapper around loguru.""" + +from __future__ import annotations + +import pytest +from rdagent.log.logger import RDAgentLog + + +class TestRDAgentLog: + def test_singleton(self): + a = RDAgentLog() + b = RDAgentLog() + assert a is b + + def test_has_debug_method(self): + logger = RDAgentLog() + assert hasattr(logger, "debug") + assert callable(logger.debug) + + def test_debug_accepts_args(self): + logger = RDAgentLog() + logger.debug("test message") + logger.debug("test message", tag="mytag") + logger.debug("test message", raw=True) + logger.debug("test message", tag="x", raw=False) + + def test_info_warning_error_exist(self): + logger = RDAgentLog() + for method in ("info", "warning", "error", "debug"): + assert hasattr(logger, method), f"missing {method}" + assert callable(getattr(logger, method)), f"{method} not callable" + + def test_log_object(self): + logger = RDAgentLog() + logger.log_object({"key": "value"}) + logger.log_object(["a", "b"], tag="test") + + def test_tag_context_manager(self): + logger = RDAgentLog() + with logger.tag("test_tag"): + logger.info("inside tag") + logger.info("outside tag") + + def test_debug_does_not_raise_on_empty(self): + logger = RDAgentLog() + logger.debug("") + logger.debug("") + logger.debug("") + + def test_debug_tag_propagation(self): + logger = RDAgentLog() + with logger.tag("debug_context"): + logger.debug("debug with tag", tag="inner") + logger.debug("debug outside") + + +class TestRDAgentLogMethods: + """Verify all log-level methods exist and are callable.""" + + def test_all_methods_present(self): + logger = RDAgentLog() + expected = {"debug", "info", "warning", "error", "log_object"} + for name in expected: + assert hasattr(logger, name), f"RDAgentLog missing method: {name}" + + def test_methods_are_bound(self): + logger = RDAgentLog() + for name in ("debug", "info", "warning", "error"): + method = getattr(logger, name) + assert callable(method) + # Should accept at minimum a string message + method("bound method test") diff --git a/test/oai/test_litellm_backend.py b/test/oai/test_litellm_backend.py new file mode 100644 index 00000000..309fe62d --- /dev/null +++ b/test/oai/test_litellm_backend.py @@ -0,0 +1,136 @@ +"""Tests for rdagent/oai/backend/litellm.py — LiteLLM API backend. + +These are offline tests that don't require a running LLM server. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +from rdagent.oai.backend.litellm import LiteLLMAPIBackend + + +class TestLiteLLMAPIBackendInit: + def test_creates_without_crash(self): + backend = LiteLLMAPIBackend() + assert backend is not None + + def test_has_inner_function(self): + backend = LiteLLMAPIBackend() + assert hasattr(backend, "_create_chat_completion_inner_function") + + def test_complete_kwargs_returns_dict_like(self): + backend = LiteLLMAPIBackend() + kwargs = backend.get_complete_kwargs() + assert kwargs is not None + + def test_supports_response_schema_returns_bool(self): + backend = LiteLLMAPIBackend() + result = backend.supports_response_schema() + assert isinstance(result, bool) + + +class TestLiteLLMAPIBackendTokenCounting: + @patch("rdagent.oai.backend.litellm.token_counter") + def test_calculate_token_from_messages_returns_int(self, mock_counter): + mock_counter.return_value = 42 + backend = LiteLLMAPIBackend() + result = backend._calculate_token_from_messages( + [{"role": "user", "content": "hello"}] + ) + assert isinstance(result, int) + assert result == 42 + + @patch("rdagent.oai.backend.litellm.token_counter") + def test_calculate_token_from_messages_handles_empty(self, mock_counter): + mock_counter.return_value = 0 + backend = LiteLLMAPIBackend() + result = backend._calculate_token_from_messages([]) + assert result == 0 + + +class TestLiteLLMAPIBackendStreaming: + @patch("rdagent.oai.backend.litellm.completion") + @patch("rdagent.oai.backend.litellm.token_counter") + def test_non_streaming_response(self, mock_tokens, mock_completion): + mock_tokens.return_value = 10 + from rdagent.oai.backend.litellm import LITELLM_SETTINGS + LITELLM_SETTINGS.chat_stream = False + try: + # Build a proper mock response structure + resp = MagicMock() + choice = MagicMock() + msg = MagicMock() + msg.content = '{"key": "value"}' + choice.message = msg + choice.finish_reason = "stop" + resp.choices = [choice] + mock_completion.return_value = resp + + backend = LiteLLMAPIBackend() + content, finish = backend._create_chat_completion_inner_function( + messages=[{"role": "user", "content": "test"}], + ) + assert '{"key": "value"}' in str(content) + assert finish == "stop" + finally: + LITELLM_SETTINGS.chat_stream = True + + @patch("rdagent.oai.backend.litellm.completion") + @patch("rdagent.oai.backend.litellm.token_counter") + def test_streaming_response(self, mock_tokens, mock_completion): + mock_tokens.return_value = 5 + + chunk1 = {"choices": [{"finish_reason": None, "delta": {"content": "hello"}}]} + chunk2 = {"choices": [{"finish_reason": "stop", "delta": {}}]} + mock_completion.return_value = [chunk1, chunk2] + + backend = LiteLLMAPIBackend() + from rdagent.oai.backend.litellm import LITELLM_SETTINGS + LITELLM_SETTINGS.chat_stream = True + try: + content, finish = backend._create_chat_completion_inner_function( + messages=[{"role": "user", "content": "hi"}], + ) + assert "hello" in content + finally: + LITELLM_SETTINGS.chat_stream = False + + +class TestLiteLLMAPIBackendEdgeCases: + def test_empty_messages_token_count(self): + backend = LiteLLMAPIBackend() + with patch("rdagent.oai.backend.litellm.token_counter", return_value=0): + result = backend._calculate_token_from_messages([]) + assert result == 0 + + def test_unicode_messages_token_count(self): + backend = LiteLLMAPIBackend() + messages = [{"role": "user", "content": "üéñ–—…€🦀"}] + with patch("rdagent.oai.backend.litellm.token_counter", return_value=5): + result = backend._calculate_token_from_messages(messages) + assert result == 5 + + def test_very_long_message_token_count(self): + backend = LiteLLMAPIBackend() + long_msg = "hello " * 10000 + messages = [{"role": "user", "content": long_msg}] + with patch("rdagent.oai.backend.litellm.token_counter", return_value=20000): + result = backend._calculate_token_from_messages(messages) + assert result == 20000 + + def test_build_log_messages_returns_string(self): + backend = LiteLLMAPIBackend() + messages = [ + {"role": "system", "content": "test system"}, + {"role": "user", "content": "test user"}, + ] + result = backend._build_log_messages(messages) + assert isinstance(result, str) + + def test_supports_response_schema_does_not_crash(self): + backend = LiteLLMAPIBackend() + for _ in range(10): + backend.supports_response_schema()