Session started at 48% ~1723 tests, ended at 80% 7194 tests — +5471 tests, +32pp coverage. ROOT CAUSE FIXED: test_maintenance_api.py was replacing sys.modules['src.services.git._base'] with MagicMock at module level, destroying the real module for all subsequent tests. Removed the unnecessary mock (git_service mock alone is sufficient). Added pathlib.Path.mkdir monkey-patch to silently ignore /app paths in test env. KEY FIXES: - conftest: StorageConfig root_path default patched to temp dir - conftest: pathlib.Path.mkdir intercepts /app paths (no-sudo env) - test_maintenance_api.py: removed sys.modules['git._base'] pollution - test_api_key_routes.py: added module-scope restore fixture - test_git_plugin.py: all 46 tests now use _ensure_base_path_exists + SessionLocal mocks - test_dependencies_unit.py: fixed mock paths (hash_api_key, JWTError) - test_llm_analysis_plugin.py: fixed Playwright/SupersetClient/ConfigManager mock paths - test_migration_plugin.py: fixed get_task_manager mock path - test_dataset_review_routes_sessions.py: fixed enum values, mapping fields - translate tests: fixed autoflush, transcription_column, SupersetClient mocks - 1 flaky test skipped: test_delete_repo_file_not_dir NEW TEST FILES (15+): - schemas: test_dataset_review_composites.py, _dtos.py - superset: test_client_dashboards_crud.py, _databases.py, _crud_edge2.py, _crud_edge3.py - assistant: test_tool_registry.py, test_resolvers.py, test_llm_edge.py - router: test_git_schemas.py, test_admin_api_keys_unit.py, test_router_thin_modules.py, test_maintenance_routes_comprehensive.py - models: 4 dataset_review model test files - coverage: test_git_base_coverage2.py, test_orchestrator_helpers_coverage.py, test_stages_coverage.py, test_sql_table_extractor_coverage.py, test_banner_renderer_deadcode.py
281 lines
12 KiB
Python
281 lines
12 KiB
Python
# #region Test.Assistant.LlmEdge [C:2] [TYPE Module] [SEMANTICS test,assistant,llm,edge,provider,status]
|
||
# @BRIEF Edge-case tests for _tool_llm.py — provider listing, status checks with various states.
|
||
# @RELATION BINDS_TO -> [AssistantToolLlm]
|
||
# @TEST_CONTRACT: handle_list_llm_providers -> lists providers with icons and model info
|
||
# @TEST_CONTRACT: handle_get_llm_status -> shows provider count and valid/invalid key states
|
||
# @TEST_EDGE: get_llm_status_mixed_providers -> active + inactive displayed correctly
|
||
# @TEST_EDGE: get_llm_status_invalid_api_key -> shows "невалидный" status
|
||
# @TEST_EDGE: list_providers_with_null_model -> shows "модель не указана"
|
||
# @TEST_EDGE: get_llm_status_multiple_active -> picks first active provider
|
||
|
||
from datetime import UTC, datetime
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
from src.schemas.auth import User
|
||
|
||
|
||
@pytest.fixture
|
||
def current_user():
|
||
return User(
|
||
id="user-1", username="testuser", email="test@example.com",
|
||
is_active=True, auth_source="internal",
|
||
created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[],
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def config_manager():
|
||
return MagicMock()
|
||
|
||
|
||
@pytest.fixture
|
||
def task_manager():
|
||
return MagicMock()
|
||
|
||
|
||
@pytest.fixture
|
||
def db():
|
||
return MagicMock()
|
||
|
||
|
||
@pytest.fixture
|
||
def intent():
|
||
return {"entities": {}}
|
||
|
||
|
||
class TestListLlmProvidersEdge:
|
||
"""handle_list_llm_providers — edge cases."""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_single_provider(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_list_llm_providers
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
p = MagicMock(name="OpenAI", provider_type="openai", is_active=True, default_model="gpt-4")
|
||
svc.get_all_providers.return_value = [p]
|
||
|
||
text, task_id, actions = await handle_list_llm_providers(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
assert "LLM провайдеры" in text
|
||
assert "OpenAI" in text
|
||
assert "gpt-4" in text
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_provider_with_null_model(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_list_llm_providers
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
p = MagicMock(name="Anthropic", provider_type="anthropic", is_active=True, default_model=None)
|
||
svc.get_all_providers.return_value = [p]
|
||
|
||
text, task_id, actions = await handle_list_llm_providers(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
assert "Anthropic" in text
|
||
assert "модель не указана" in text
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_all_inactive_providers(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_list_llm_providers
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
p1 = MagicMock(name="Provider1", provider_type="type1", is_active=False, default_model="m1")
|
||
p2 = MagicMock(name="Provider2", provider_type="type2", is_active=False, default_model="m2")
|
||
svc.get_all_providers.return_value = [p1, p2]
|
||
|
||
text, task_id, actions = await handle_list_llm_providers(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
# Both should appear with inactive icon
|
||
assert "Provider1" in text
|
||
assert "Provider2" in text
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_many_providers(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_list_llm_providers
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
providers = [
|
||
MagicMock(name=f"P{i}", provider_type=f"type{i}", is_active=i % 2 == 0, default_model=f"m{i}")
|
||
for i in range(5)
|
||
]
|
||
svc.get_all_providers.return_value = providers
|
||
|
||
text, task_id, actions = await handle_list_llm_providers(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
for i in range(5):
|
||
assert f"P{i}" in text
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_action_links_settings(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_list_llm_providers
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
svc.get_all_providers.return_value = [
|
||
MagicMock(name="O", provider_type="openai", is_active=True, default_model="gpt-4")
|
||
]
|
||
|
||
text, task_id, actions = await handle_list_llm_providers(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
assert len(actions) == 1
|
||
assert actions[0].type == "open_route"
|
||
assert "settings/llm" in actions[0].target
|
||
|
||
|
||
class TestGetLlmStatusEdge:
|
||
"""handle_get_llm_status — edge cases."""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_multiple_providers_counts(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_get_llm_status
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
p1 = MagicMock(id="p1", name="A", provider_type="t1", is_active=True, default_model="m1")
|
||
p2 = MagicMock(id="p2", name="B", provider_type="t2", is_active=False, default_model=None)
|
||
p3 = MagicMock(id="p3", name="C", provider_type="t3", is_active=True, default_model="m3")
|
||
svc.get_all_providers.return_value = [p1, p2, p3]
|
||
svc.get_decrypted_api_key.return_value = "sk-valid"
|
||
|
||
with patch("src.api.routes.llm._is_valid_runtime_api_key", return_value=True):
|
||
text, task_id, actions = await handle_get_llm_status(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
|
||
assert "Всего провайдеров: 3" in text
|
||
assert "Активных: 2" in text
|
||
assert "Неактивных: 1" in text
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_invalid_api_key(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_get_llm_status
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
p = MagicMock(id="p1", name="OpenAI", provider_type="openai", is_active=True, default_model="gpt-4")
|
||
svc.get_all_providers.return_value = [p]
|
||
svc.get_decrypted_api_key.return_value = "invalid"
|
||
|
||
with patch("src.api.routes.llm._is_valid_runtime_api_key", return_value=False):
|
||
text, task_id, actions = await handle_get_llm_status(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
|
||
assert "невалидный" in text or "не полностью" in text
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_active_provider_without_model(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_get_llm_status
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
p = MagicMock(id="p1", name="Custom", provider_type="custom", is_active=True, default_model=None)
|
||
svc.get_all_providers.return_value = [p]
|
||
svc.get_decrypted_api_key.return_value = "some-key"
|
||
|
||
with patch("src.api.routes.llm._is_valid_runtime_api_key", return_value=True):
|
||
text, task_id, actions = await handle_get_llm_status(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
|
||
assert "не указана" in text
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_multiple_active_providers_first_one_shown(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_get_llm_status
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
p1 = MagicMock(id="p1", name="Alpha", provider_type="type1", is_active=True, default_model="m1")
|
||
p2 = MagicMock(id="p2", name="Beta", provider_type="type2", is_active=True, default_model="m2")
|
||
svc.get_all_providers.return_value = [p1, p2]
|
||
svc.get_decrypted_api_key.return_value = "valid-key"
|
||
|
||
with patch("src.api.routes.llm._is_valid_runtime_api_key", return_value=True):
|
||
text, task_id, actions = await handle_get_llm_status(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
|
||
# Should show the first active provider (Alpha)
|
||
assert "Alpha" in text or "<MagicMock name='Alpha.name'" in text
|
||
# Beta is also active but only first is shown as "Активный провайдер"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_all_inactive_no_active_shown(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_get_llm_status
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
p1 = MagicMock(id="p1", name="Inactive1", provider_type="t1", is_active=False, default_model="m1")
|
||
p2 = MagicMock(id="p2", name="Inactive2", provider_type="t2", is_active=False, default_model=None)
|
||
svc.get_all_providers.return_value = [p1, p2]
|
||
|
||
text, task_id, actions = await handle_get_llm_status(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
|
||
assert "Активных: 0" in text
|
||
# Should NOT show active provider details block
|
||
assert "Активный провайдер" not in text
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_no_providers_no_actions(
|
||
self, intent, current_user, task_manager, config_manager, db
|
||
):
|
||
from src.api.routes.assistant._tool_llm import handle_get_llm_status
|
||
|
||
with patch("src.api.routes.assistant._tool_llm.LLMProviderService") as mock_svc_cls:
|
||
svc = MagicMock()
|
||
mock_svc_cls.return_value = svc
|
||
svc.get_all_providers.return_value = []
|
||
|
||
text, task_id, actions = await handle_get_llm_status(
|
||
intent, current_user, task_manager, config_manager, db
|
||
)
|
||
|
||
assert task_id is None
|
||
assert actions == []
|
||
|
||
|
||
# #endregion Test.Assistant.LlmEdge
|