Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

205 lines
6.4 KiB
Python

"""Unit tests for application/llm/base.py — BaseLLM.
Covers initialisation, static helpers, supports_* introspection,
structured-output defaults, and attachment-type defaults.
Fallback behaviour is covered separately in test_fallback.py.
"""
from unittest.mock import MagicMock, Mock
import pytest
from application.llm.base import BaseLLM
# ---------------------------------------------------------------------------
# Concrete stub so we can instantiate the abstract base
# ---------------------------------------------------------------------------
class StubLLM(BaseLLM):
"""Minimal concrete BaseLLM for unit-testing non-abstract members."""
def _raw_gen(self, baseself, model, messages, stream, tools=None, **kw):
return "raw_gen_result"
def _raw_gen_stream(self, baseself, model, messages, stream, tools=None, **kw):
yield "chunk"
# ---------------------------------------------------------------------------
# __init__
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestBaseLLMInit:
def test_defaults(self):
llm = StubLLM()
assert llm.decoded_token is None
assert llm.agent_id is None
assert llm.model_id is None
assert llm.base_url is None
assert llm.token_usage == {"prompt_tokens": 0, "generated_tokens": 0}
assert llm._backup_models == []
assert llm._fallback_llm is None
def test_agent_id_cast_to_str(self):
llm = StubLLM(agent_id=42)
assert llm.agent_id == "42"
def test_agent_id_none_stays_none(self):
llm = StubLLM(agent_id=None)
assert llm.agent_id is None
def test_custom_params(self):
token = {"sub": "u1"}
llm = StubLLM(
decoded_token=token,
agent_id="abc",
model_id="gpt-4",
base_url="http://x",
backup_models=["m1", "m2"],
)
assert llm.decoded_token is token
assert llm.agent_id == "abc"
assert llm.model_id == "gpt-4"
assert llm.base_url == "http://x"
assert llm._backup_models == ["m1", "m2"]
# ---------------------------------------------------------------------------
# _remove_null_values
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestRemoveNullValues:
def test_removes_none_values(self):
result = BaseLLM._remove_null_values({"a": 1, "b": None, "c": "x"})
assert result == {"a": 1, "c": "x"}
def test_keeps_falsy_non_none(self):
result = BaseLLM._remove_null_values({"a": 0, "b": "", "c": False, "d": []})
assert result == {"a": 0, "b": "", "c": False, "d": []}
def test_non_dict_passthrough(self):
assert BaseLLM._remove_null_values("hello") == "hello"
assert BaseLLM._remove_null_values(42) == 42
assert BaseLLM._remove_null_values([1, 2]) == [1, 2]
def test_empty_dict(self):
assert BaseLLM._remove_null_values({}) == {}
def test_all_none(self):
assert BaseLLM._remove_null_values({"a": None, "b": None}) == {}
# ---------------------------------------------------------------------------
# supports_tools / _supports_tools
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestSupportsTools:
def test_supports_tools_true_when_callable(self):
llm = StubLLM()
assert llm.supports_tools() is True
def test_supports_tools_false_when_not_callable(self):
llm = StubLLM()
llm._supports_tools = "not_callable"
assert llm.supports_tools() is False
def test_default_supports_tools_raises(self):
llm = StubLLM()
with pytest.raises(NotImplementedError):
llm._supports_tools()
# ---------------------------------------------------------------------------
# supports_structured_output / _supports_structured_output
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestSupportsStructuredOutput:
def test_supports_structured_output_true(self):
llm = StubLLM()
assert llm.supports_structured_output() is True
def test_default_supports_structured_output_returns_false(self):
llm = StubLLM()
assert llm._supports_structured_output() is False
# ---------------------------------------------------------------------------
# prepare_structured_output_format
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestPrepareStructuredOutputFormat:
def test_returns_none_by_default(self):
llm = StubLLM()
assert llm.prepare_structured_output_format({"type": "object"}) is None
# ---------------------------------------------------------------------------
# get_supported_attachment_types
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestGetSupportedAttachmentTypes:
def test_returns_empty_list(self):
llm = StubLLM()
assert llm.get_supported_attachment_types() == []
# ---------------------------------------------------------------------------
# fallback_llm — caching
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestFallbackLLMCaching:
def test_returns_cached_instance(self, monkeypatch):
"""Once resolved, the same fallback instance is returned."""
sentinel = StubLLM()
llm = StubLLM()
llm._fallback_llm = sentinel
assert llm.fallback_llm is sentinel
def test_none_when_no_backup_and_no_global(self, monkeypatch):
monkeypatch.setattr(
"application.llm.base.settings",
MagicMock(FALLBACK_LLM_PROVIDER=None),
)
llm = StubLLM(backup_models=[])
assert llm.fallback_llm is None
def test_global_fallback_init_failure_returns_none(self, monkeypatch):
monkeypatch.setattr(
"application.llm.base.settings",
MagicMock(
FALLBACK_LLM_PROVIDER="openai",
FALLBACK_LLM_NAME="gpt-4",
FALLBACK_LLM_API_KEY="k",
API_KEY="k",
),
)
monkeypatch.setattr(
"application.llm.llm_creator.LLMCreator.create_llm",
Mock(side_effect=RuntimeError("boom")),
)
llm = StubLLM(backup_models=[])
assert llm.fallback_llm is None