From 4d9459a9f931e565a0030e5d1b73df213f57dedb Mon Sep 17 00:00:00 2001 From: TPTBusiness Date: Fri, 8 May 2026 22:47:41 +0200 Subject: [PATCH] test: add tests for RDAgentLog debug() and LiteLLMAPIBackend --- .../test_logger.cpython-310-pytest-9.0.3.pyc | Bin 0 -> 5406 bytes .../test_logger.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 10611 bytes test/log/test_logger.py | 72 ++++++++++ test/oai/test_litellm_backend.py | 136 ++++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 test/log/__pycache__/test_logger.cpython-310-pytest-9.0.3.pyc create mode 100644 test/log/__pycache__/test_logger.cpython-313-pytest-9.0.2.pyc create mode 100644 test/log/test_logger.py create mode 100644 test/oai/test_litellm_backend.py 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 0000000000000000000000000000000000000000..19897c73e528891059e78c183598a6eb8cbd7876 GIT binary patch literal 5406 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 literal 0 HcmV?d00001 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()