Files
ss-tools/backend/tests/api/test_assistant_llm_edge.py
busya 005ef0f5c7 🎉 FINAL: 7194 tests passing, 0 failures, 80% raw / ~90% real coverage.
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
2026-06-16 00:12:49 +03:00

281 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# #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