Files
ss-tools/backend/tests/api/test_assistant_tools.py
busya 4dce669844 fix(tests): 61 failed backend unit tests — async/await mocks, deadlock fix, SyntaxError repairs
Группы исправлений:
- Группа 1 (async/await misuse): MagicMock → AsyncMock для get_dashboards,
  export_dashboard, import_dashboard, sync_environment, get_run_detail,
  list_all_runs, create_task и др. — 23 теста
- Группа 2 (runner.run deadlock): добавлены моки get_async_job_runner +
  IdMappingService/AsyncSupersetClient в migration plugin + API tests
  для предотвращения вечной блокировки future.result() — 16 тестов
- Группа 3 (SyntaxError): исправлены 7 незакрытых скобок ')' в
  test_validation_tasks_comprehensive.py (QA-агент оставил AsyncMock
  без закрывающих скобок)
- Группа 4 (mock verification): logger mock error→explore, scheduler tests
  skipped (удалён из production), dataset mapper — 11 тестов
- Группа 5 (search/assistant): MagicMock → AsyncMock — 9 тестов
- Группа 6 (extractor parsing): AsyncMock для async методов — 9 тестов

Итого: 61 ранее FAILED → 274 passed, 4 skipped, 0 failed
2026-06-18 14:41:13 +03:00

1894 lines
89 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.Tools [C:2] [TYPE Module] [SEMANTICS test,assistant,tools,handlers]
# @BRIEF Tests for all assistant tool handlers, registry, resolvers, dispatch, and routes.
# @RELATION BINDS_TO -> [AssistantToolRegistry]
# @RELATION BINDS_TO -> [AssistantResolvers]
# @RELATION BINDS_TO -> [AssistantDispatch]
# @RELATION BINDS_TO -> [AssistantRoutes]
from __future__ import annotations
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException
from src.schemas.auth import User
# ── Shared fixtures ──────────────────────────────────────────────────────────
def _make_env(id_: str, name_: str, stage="DEV", is_production=False, is_default=False):
"""Create a mock environment object with proper attribute access."""
env = MagicMock()
env.id = id_
env.name = name_
env.stage = stage
env.is_production = is_production
env.is_default = is_default
return env
@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 task_manager():
tm = MagicMock()
tm.create_task = AsyncMock(return_value=MagicMock(id="task-42"))
tm.get_task = MagicMock(return_value=None)
tm.get_tasks = MagicMock(return_value=[])
return tm
@pytest.fixture
def config_manager():
cm = MagicMock()
cm.get_environments = MagicMock(return_value=[])
cm.get_config = MagicMock()
return cm
@pytest.fixture
def db():
return MagicMock()
@pytest.fixture
def intent():
return {"entities": {}}
# ── _tool_capabilities.py ────────────────────────────────────────────────────
class TestToolCapabilities:
"""handle_show_capabilities — list available commands."""
# #region test_show_capabilities_with_tools [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_capabilities.get_catalog")
async def test_show_capabilities_with_tools(
self, mock_get_catalog, intent, current_user, task_manager, config_manager, db
):
from src.api.routes.assistant._tool_capabilities import handle_show_capabilities
mock_get_catalog.return_value = [
{"operation": "create_branch"},
{"operation": "commit_changes"},
{"operation": "deploy_dashboard"},
{"operation": "execute_migration"},
{"operation": "run_backup"},
{"operation": "run_llm_validation"},
{"operation": "run_llm_documentation"},
{"operation": "get_task_status"},
{"operation": "get_health_summary"},
]
text, task_id, actions = await handle_show_capabilities(
intent, current_user, task_manager, config_manager, db
)
assert "Вот что я могу сделать для вас" in text
assert task_id is None
assert actions == []
# #endregion test_show_capabilities_with_tools
# #region test_show_capabilities_empty [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_capabilities.get_catalog")
async def test_show_capabilities_empty(
self, mock_get_catalog, intent, current_user, task_manager, config_manager, db
):
from src.api.routes.assistant._tool_capabilities import handle_show_capabilities
mock_get_catalog.return_value = [{"operation": "unknown_op"}]
text, task_id, actions = await handle_show_capabilities(
intent, current_user, task_manager, config_manager, db
)
assert "нет доступных" in text.lower()
assert task_id is None
# #endregion test_show_capabilities_empty
# ── _tool_commit.py ──────────────────────────────────────────────────────────
class TestToolCommit:
"""handle_commit_changes — commit dashboard changes."""
# #region test_commit_changes_success [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_commit._get_git_service")
@patch("src.api.routes.assistant._tool_commit._check_any_permission")
@patch("src.api.routes.assistant._tool_commit._resolve_dashboard_id_entity")
async def test_commit_changes_success(
self, mock_resolve, mock_check, mock_git_service,
current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_commit import handle_commit_changes
mock_resolve.return_value = 42
mock_git = MagicMock()
mock_git_service.return_value = mock_git
intent_data = {"entities": {"dashboard_id": "42", "message": "my commit message"}}
text, task_id, actions = await handle_commit_changes(
intent_data, current_user, task_manager, config_manager, db
)
assert "Коммит выполнен" in text
mock_git.commit_changes.assert_called_with(42, "my commit message", None)
# #endregion test_commit_changes_success
# #region test_commit_changes_missing_dashboard [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_commit._check_any_permission")
@patch("src.api.routes.assistant._tool_commit._resolve_dashboard_id_entity")
async def test_commit_changes_missing_dashboard(
self, mock_resolve, mock_check,
current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_commit import handle_commit_changes
mock_resolve.return_value = None
with pytest.raises(HTTPException) as exc:
await handle_commit_changes(
{"entities": {}}, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
# #endregion test_commit_changes_missing_dashboard
# ── _tool_create_branch.py ────────────────────────────────────────────────────
class TestToolCreateBranch:
"""handle_create_branch — create git branch."""
# #region test_create_branch_success [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_create_branch._get_git_service")
@patch("src.api.routes.assistant._tool_create_branch._check_any_permission")
@patch("src.api.routes.assistant._tool_create_branch._resolve_dashboard_id_entity")
async def test_create_branch_success(
self, mock_resolve, mock_check, mock_git_service,
current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_create_branch import handle_create_branch
mock_resolve.return_value = 42
mock_git = MagicMock()
mock_git_service.return_value = mock_git
intent_data = {"entities": {"dashboard_id": "42", "branch_name": "feature/test"}}
text, task_id, actions = await handle_create_branch(
intent_data, current_user, task_manager, config_manager, db
)
assert "Ветка" in text
assert "feature/test" in text
assert "42" in text
mock_git.create_branch.assert_called_with(42, "feature/test", "main")
# #endregion test_create_branch_success
# #region test_create_branch_missing_params [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_create_branch._check_any_permission", return_value=True)
@patch("src.api.routes.assistant._tool_create_branch._resolve_dashboard_id_entity")
async def test_create_branch_missing_params(
self, mock_resolve, mock_perm,
current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_create_branch import handle_create_branch
mock_resolve.return_value = None
with pytest.raises(HTTPException) as exc:
await handle_create_branch(
{"entities": {}}, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
# #endregion test_create_branch_missing_params
# ── _tool_deploy.py ───────────────────────────────────────────────────────────
class TestToolDeploy:
"""handle_deploy_dashboard — deploy dashboard to environment."""
# #region test_deploy_dashboard_success [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_deploy._check_any_permission")
@patch("src.api.routes.assistant._tool_deploy._resolve_dashboard_id_entity")
@patch("src.api.routes.assistant._tool_deploy._resolve_env_id")
async def test_deploy_dashboard_success(
self, mock_env, mock_resolve, mock_check,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_deploy import handle_deploy_dashboard
mock_env.return_value = "env-prod"
mock_resolve.return_value = 42
intent_data = {"entities": {"environment": "prod", "dashboard_id": "42"}}
text, task_id, actions = await handle_deploy_dashboard(
intent_data, current_user, task_manager, config_manager, db
)
assert "Деплой запущен" in text
assert task_id == "task-42"
assert len(actions) == 2
task_manager.create_task.assert_awaited_once()
# #endregion test_deploy_dashboard_success
# #region test_deploy_dashboard_missing_params [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_deploy._check_any_permission", return_value=True)
@patch("src.api.routes.assistant._tool_deploy._resolve_env_id")
@patch("src.api.routes.assistant._tool_deploy._resolve_dashboard_id_entity")
async def test_deploy_dashboard_missing_params(
self, mock_resolve_dash, mock_resolve_env, mock_perm,
current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_deploy import handle_deploy_dashboard
mock_resolve_dash.return_value = None
with pytest.raises(HTTPException) as exc:
await handle_deploy_dashboard(
{"entities": {}}, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
# #endregion test_deploy_dashboard_missing_params
# ── _tool_llm_documentation.py ────────────────────────────────────────────────
class TestToolLlmDocumentation:
"""handle_run_llm_documentation — generate LLM documentation."""
# #region test_llm_documentation_success [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm_documentation._check_any_permission")
@patch("src.api.routes.assistant._tool_llm_documentation._resolve_env_id")
@patch("src.api.routes.assistant._tool_llm_documentation._resolve_provider_id")
async def test_llm_documentation_success(
self, mock_provider, mock_env, mock_check,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_llm_documentation import handle_run_llm_documentation
mock_env.return_value = "env-dev"
mock_provider.return_value = "provider-1"
intent_data = {"entities": {"dataset_id": "ds-1", "environment": "dev", "provider": "openai"}}
text, task_id, actions = await handle_run_llm_documentation(
intent_data, current_user, task_manager, config_manager, db
)
assert "Генерация документации запущена" in text
assert task_id == "task-42"
assert len(actions) == 2
# #endregion test_llm_documentation_success
# #region test_llm_documentation_missing_params [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm_documentation._check_any_permission", return_value=True)
async def test_llm_documentation_missing_params(
self, mock_perm,
current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_llm_documentation import handle_run_llm_documentation
with pytest.raises(HTTPException) as exc:
await handle_run_llm_documentation(
{"entities": {}}, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 400
# #endregion test_llm_documentation_missing_params
# ── _tool_list_environments.py ────────────────────────────────────────────────
class TestToolListEnvironments:
"""handle_list_environments — list configured environments."""
# #region test_list_environments_with_envs [C:2] [TYPE Function]
async def test_list_environments_with_envs(
self, intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_list_environments import handle_list_environments
env1 = MagicMock(id="dev", name="Development", stage="DEV", is_production=False, is_default=True)
env2 = MagicMock(id="prod", name="Production", stage="PROD", is_production=True, is_default=False)
config_manager.get_environments.return_value = [env1, env2]
text, task_id, actions = await handle_list_environments(
intent, current_user, task_manager, config_manager, db
)
assert "Доступные окружения" in text
assert "dev" in text
assert "prod" in text
assert task_id is None
assert len(actions) == 1
# #endregion test_list_environments_with_envs
# #region test_list_environments_empty [C:2] [TYPE Function]
async def test_list_environments_empty(
self, intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_list_environments import handle_list_environments
config_manager.get_environments.return_value = []
text, task_id, actions = await handle_list_environments(
intent, current_user, task_manager, config_manager, db
)
assert "Нет доступных окружений" in text
assert task_id is None
# #endregion test_list_environments_empty
# ── _tool_health_summary.py ───────────────────────────────────────────────────
class TestToolHealthSummary:
"""handle_get_health_summary — dashboard health summary."""
# #region test_health_summary_success [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_health_summary.HealthService")
@patch("src.api.routes.assistant._tool_health_summary._resolve_env_id")
@patch("src.api.routes.assistant._tool_health_summary._get_environment_name_by_id")
async def test_health_summary_success(
self, mock_env_name, mock_env, mock_health,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_health_summary import handle_get_health_summary
mock_env.return_value = "env-dev"
mock_env_name.return_value = "Development"
health_svc = MagicMock()
mock_health.return_value = health_svc
summary = MagicMock(
pass_count=5, warn_count=2, fail_count=0, unknown_count=1, items=[]
)
health_svc.get_health_summary = AsyncMock(return_value=summary)
text, task_id, actions = await handle_get_health_summary(
intent, current_user, task_manager, config_manager, db
)
assert "Сводка здоровья" in text
assert "5" in text
assert task_id is None
assert len(actions) == 1
# #endregion test_health_summary_success
# #region test_health_summary_with_failures [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_health_summary.HealthService")
@patch("src.api.routes.assistant._tool_health_summary._resolve_env_id")
@patch("src.api.routes.assistant._tool_health_summary._get_environment_name_by_id")
async def test_health_summary_with_failures(
self, mock_env_name, mock_env, mock_health,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_health_summary import handle_get_health_summary
mock_env.return_value = None
mock_env_name.return_value = "всех окружений"
health_svc = MagicMock()
mock_health.return_value = health_svc
item1 = MagicMock(status="FAIL", dashboard_id="dash-1", environment_id="env-dev", summary="Broken", task_id="task-1")
summary = MagicMock(
pass_count=3, warn_count=1, fail_count=2, unknown_count=0,
items=[item1],
)
health_svc.get_health_summary = AsyncMock(return_value=summary)
text, task_id, actions = await handle_get_health_summary(
intent, current_user, task_manager, config_manager, db
)
assert "Обнаружены ошибки" in text
assert "dash-1" in text
assert len(actions) >= 2
# #endregion test_health_summary_with_failures
# ── _tool_backup.py ───────────────────────────────────────────────────────────
class TestToolBackup:
"""handle_run_backup — run backup."""
# #region test_run_backup_env_only [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_backup._check_any_permission")
@patch("src.api.routes.assistant._tool_backup._resolve_env_id")
async def test_run_backup_env_only(
self, mock_env, mock_check,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_backup import handle_run_backup
mock_env.return_value = "env-dev"
intent_data = {"entities": {"environment": "dev"}}
text, task_id, actions = await handle_run_backup(
intent_data, current_user, task_manager, config_manager, db
)
assert "Бэкап запущен" in text
assert task_id == "task-42"
task_manager.create_task.assert_awaited_once_with(
plugin_id="superset-backup",
params={"environment_id": "env-dev"},
user_id=current_user.id,
)
# #endregion test_run_backup_env_only
# #region test_run_backup_with_dashboard [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_backup._check_any_permission")
@patch("src.api.routes.assistant._tool_backup._resolve_dashboard_id_entity")
@patch("src.api.routes.assistant._tool_backup._resolve_env_id")
@patch("src.api.routes.assistant._tool_backup._get_environment_name_by_id")
async def test_run_backup_with_dashboard(
self, mock_env_name, mock_env, mock_resolve, mock_check,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_backup import handle_run_backup
mock_env.return_value = "env-dev"
mock_env_name.return_value = "Development"
mock_resolve.return_value = 42
intent_data = {"entities": {"environment": "dev", "dashboard_id": "42"}}
text, task_id, actions = await handle_run_backup(
intent_data, current_user, task_manager, config_manager, db
)
assert "Бэкап запущен" in text
assert task_id == "task-42"
assert len(actions) == 4
# #endregion test_run_backup_with_dashboard
# #region test_run_backup_missing_env [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_backup._check_any_permission", return_value=True)
@patch("src.api.routes.assistant._tool_backup._resolve_env_id", return_value=None)
async def test_run_backup_missing_env(
self, mock_resolve_env, mock_perm,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_backup import handle_run_backup
with pytest.raises(HTTPException) as exc:
await handle_run_backup(
{"entities": {}}, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 400
# #endregion test_run_backup_missing_env
# ── _tool_migration.py ─────────────────────────────────────────────────────────
class TestToolMigration:
"""handle_execute_migration — execute migration."""
# #region test_execute_migration_missing_envs [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_migration._check_any_permission", return_value=True)
async def test_execute_migration_missing_envs(
self, mock_perm,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_migration import handle_execute_migration
with pytest.raises(HTTPException) as exc:
await handle_execute_migration(
{"entities": {}}, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
# #endregion test_execute_migration_missing_envs
# #region test_execute_migration_success [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_migration._check_any_permission", return_value=True)
@patch("src.api.routes.assistant._tool_migration._resolve_env_id", return_value="env-1")
@patch("src.api.routes.assistant._tool_migration._resolve_dashboard_id_entity", return_value=42)
async def test_execute_migration_success(
self, mock_dash, mock_env, mock_perm,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_migration import handle_execute_migration
task_manager.create_task.return_value = MagicMock(id="task-99")
text, task_id, actions = await handle_execute_migration(
{"entities": {"environment": "prod", "dashboard_id": "42"}},
current_user, task_manager, config_manager, db,
)
assert "Миграция запущена" in text
assert task_id == "task-99"
# #endregion test_execute_migration_success
# ── _tool_task_status.py ──────────────────────────────────────────────────────
class TestToolTaskStatus:
"""handle_get_task_status — get task status."""
# #region test_get_task_status_by_id [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_task_status._check_any_permission")
@patch("src.api.routes.assistant._tool_task_status._build_task_observability_summary")
@patch("src.api.routes.assistant._tool_task_status._extract_result_deep_links")
async def test_get_task_status_by_id(
self, mock_links, mock_summary, mock_check,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_task_status import handle_get_task_status
mock_summary.return_value = "Detailed summary"
mock_links.return_value = []
task = MagicMock(id="task-42", status="RUNNING", user_id=current_user.id)
task_manager.get_task.return_value = task
intent_data = {"entities": {"task_id": "task-42"}}
text, task_id, actions = await handle_get_task_status(
intent_data, current_user, task_manager, config_manager, db
)
assert "task-42" in text
assert "RUNNING" in text
assert task_id == "task-42"
# #endregion test_get_task_status_by_id
# #region test_get_task_status_latest [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_task_status._check_any_permission")
@patch("src.api.routes.assistant._tool_task_status._build_task_observability_summary")
@patch("src.api.routes.assistant._tool_task_status._extract_result_deep_links")
async def test_get_task_status_latest(
self, mock_links, mock_summary, mock_check,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_task_status import handle_get_task_status
mock_summary.return_value = ""
mock_links.return_value = []
task = MagicMock(id="task-1", status="SUCCESS", user_id=current_user.id)
task_manager.get_tasks.return_value = [task]
text, task_id, actions = await handle_get_task_status(
intent, current_user, task_manager, config_manager, db
)
assert "Последняя задача" in text
assert task_id == "task-1"
# #endregion test_get_task_status_latest
# #region test_get_task_status_no_history [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_task_status._check_any_permission")
async def test_get_task_status_no_history(
self, mock_check,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_task_status import handle_get_task_status
task_manager.get_tasks.return_value = []
text, task_id, actions = await handle_get_task_status(
intent, current_user, task_manager, config_manager, db
)
assert "пока нет задач" in text
assert task_id is None
# #endregion test_get_task_status_no_history
# #region test_get_task_status_not_found [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_task_status._check_any_permission")
async def test_get_task_status_not_found(
self, mock_check,
current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_task_status import handle_get_task_status
task_manager.get_task.return_value = None
with pytest.raises(HTTPException) as exc:
await handle_get_task_status(
{"entities": {"task_id": "nonexistent"}}, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 404
# #endregion test_get_task_status_not_found
# ── _tool_llm_validation.py ───────────────────────────────────────────────────
class TestToolLlmValidation:
"""handle_run_llm_validation — create and run LLM validation."""
# #region test_run_llm_validation_success [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm_validation.ValidationTaskService")
@patch("src.api.routes.assistant._tool_llm_validation._check_any_permission")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_dashboard_id_entity")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_env_id")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_provider_id")
async def test_run_llm_validation_success(
self, mock_provider, mock_env, mock_resolve, mock_check,
mock_validation_svc,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_llm_validation import handle_run_llm_validation
mock_env.return_value = "env-dev"
mock_resolve.return_value = 42
mock_provider.return_value = "provider-1"
svc = MagicMock()
mock_validation_svc.return_value = svc
svc.create_task = AsyncMock(return_value=MagicMock(id="vt-1", name="Validation Task"))
svc.trigger_run = AsyncMock(return_value={"task_id": "task-42", "run_id": "run-1"})
intent_data = {
"entities": {
"dashboard_ref": "my-dashboard",
"environment": "dev",
"provider": "openai",
}
}
text, task_id, actions = await handle_run_llm_validation(
intent_data, current_user, task_manager, config_manager, db
)
assert "LLM-валидация запущена" in text
assert task_id == "task-42"
assert len(actions) == 2
# #endregion test_run_llm_validation_success
# #region test_run_llm_validation_missing_params [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm_validation._check_any_permission", return_value=True)
async def test_run_llm_validation_missing_params(
self, mock_perm, task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_llm_validation import handle_run_llm_validation
with pytest.raises(HTTPException) as exc:
await handle_run_llm_validation(
{"entities": {}}, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
# #endregion test_run_llm_validation_missing_params
# #region test_run_llm_validation_create_task_error [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm_validation.ValidationTaskService")
@patch("src.api.routes.assistant._tool_llm_validation._check_any_permission")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_dashboard_id_entity")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_env_id")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_provider_id")
async def test_run_llm_validation_create_task_error(
self, mock_provider, mock_env, mock_resolve, mock_check,
mock_validation_svc,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_llm_validation import handle_run_llm_validation
mock_env.return_value = "env-dev"
mock_resolve.return_value = 42
mock_provider.return_value = "provider-1"
svc = MagicMock()
mock_validation_svc.return_value = svc
svc.create_task = AsyncMock(side_effect=ValueError("Invalid config"))
with pytest.raises(HTTPException) as exc:
await handle_run_llm_validation(
{"entities": {"dashboard_ref": "d", "environment": "dev", "provider": "openai"}},
current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
# #endregion test_run_llm_validation_create_task_error
# #region test_run_llm_validation_trigger_run_error [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm_validation.ValidationTaskService")
@patch("src.api.routes.assistant._tool_llm_validation._check_any_permission")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_dashboard_id_entity")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_env_id")
@patch("src.api.routes.assistant._tool_llm_validation._resolve_provider_id")
async def test_run_llm_validation_trigger_run_error(
self, mock_provider, mock_env, mock_resolve, mock_check,
mock_validation_svc,
task_manager, current_user, config_manager, db,
):
from src.api.routes.assistant._tool_llm_validation import handle_run_llm_validation
mock_env.return_value = "env-dev"
mock_resolve.return_value = 42
mock_provider.return_value = "provider-1"
svc = MagicMock()
mock_validation_svc.return_value = svc
svc.create_task = AsyncMock(return_value=MagicMock(id="vt-1", name="Validation Task"))
svc.trigger_run = AsyncMock(side_effect=ValueError("Trigger failed"))
with pytest.raises(HTTPException) as exc:
await handle_run_llm_validation(
{"entities": {"dashboard_ref": "d", "environment": "dev", "provider": "openai"}},
current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
# #endregion test_run_llm_validation_trigger_run_error
# ── _tool_search_dashboards.py ────────────────────────────────────────────────
class TestToolSearchDashboards:
"""handle_search_dashboards — search and list dashboards."""
# #region test_search_dashboards_success [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_search_dashboards.SupersetClient")
@patch("src.api.routes.assistant._tool_search_dashboards._resolve_env_id")
async def test_search_dashboards_success(
self, mock_env, mock_client_cls,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_search_dashboards import handle_search_dashboards
mock_env.return_value = "env-dev"
env_obj = _make_env("env-dev", "Development")
config_manager.get_environments.return_value = [env_obj]
client = MagicMock()
mock_client_cls.return_value = client
client.get_dashboards_summary = AsyncMock(return_value=[
{"id": 1, "title": "Dashboard One", "slug": "dash-one"},
{"id": 2, "title": "Dashboard Two", "slug": "dash-two"},
])
intent_data = {"entities": {"environment": "dev"}}
text, task_id, actions = await handle_search_dashboards(
intent_data, current_user, task_manager, config_manager, db
)
assert "Дашборды" in text
assert "Dashboard One" in text
assert task_id is None
assert len(actions) == 1
# #endregion test_search_dashboards_success
# #region test_search_dashboards_with_search [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_search_dashboards.SupersetClient")
@patch("src.api.routes.assistant._tool_search_dashboards._resolve_env_id")
async def test_search_dashboards_with_search(
self, mock_env, mock_client_cls,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_search_dashboards import handle_search_dashboards
mock_env.return_value = "env-dev"
env_obj = _make_env("env-dev", "Development")
config_manager.get_environments.return_value = [env_obj]
client = MagicMock()
mock_client_cls.return_value = client
client.get_dashboards_summary = AsyncMock(return_value=[
{"id": 1, "title": "Dashboard One", "slug": "dash-one"},
{"id": 2, "title": "Dashboard Two", "slug": "dash-two"},
])
intent_data = {"entities": {"environment": "dev", "search": "One"}}
text, task_id, actions = await handle_search_dashboards(
intent_data, current_user, task_manager, config_manager, db
)
assert "Dashboard One" in text
assert "Dashboard Two" not in text # filtered by search
# #endregion test_search_dashboards_with_search
# #region test_search_dashboards_no_env [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_search_dashboards._resolve_env_id")
async def test_search_dashboards_no_env(
self, mock_env,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_search_dashboards import handle_search_dashboards
mock_env.return_value = None
env1 = MagicMock(id="dev", name="Development")
config_manager.get_environments.return_value = [env1]
text, task_id, actions = await handle_search_dashboards(
intent, current_user, task_manager, config_manager, db
)
assert "Укажите окружение" in text
# #endregion test_search_dashboards_no_env
# #region test_search_dashboards_no_envs_at_all [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_search_dashboards._resolve_env_id")
async def test_search_dashboards_no_envs_at_all(
self, mock_env,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_search_dashboards import handle_search_dashboards
mock_env.return_value = None
config_manager.get_environments.return_value = []
text, task_id, actions = await handle_search_dashboards(
intent, current_user, task_manager, config_manager, db
)
assert "Нет доступных окружений" in text
# #endregion test_search_dashboards_no_envs_at_all
# #region test_search_dashboards_env_not_found [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_search_dashboards._resolve_env_id")
async def test_search_dashboards_env_not_found(
self, mock_env,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_search_dashboards import handle_search_dashboards
mock_env.return_value = "env-unknown"
config_manager.get_environments.return_value = [MagicMock(id="dev", name="Dev")]
intent_data = {"entities": {"environment": "unknown"}}
text, task_id, actions = await handle_search_dashboards(
intent_data, current_user, task_manager, config_manager, db
)
assert "не найдено" in text
# #endregion test_search_dashboards_env_not_found
# ── _tool_llm.py ──────────────────────────────────────────────────────────────
class TestToolLlm:
"""handle_list_llm_providers and handle_get_llm_status."""
# #region test_list_llm_providers [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm.LLMProviderService")
async def test_list_llm_providers(
self, mock_svc_cls,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_llm import handle_list_llm_providers
svc = MagicMock()
mock_svc_cls.return_value = svc
p1 = MagicMock(name="OpenAI", provider_type="openai", is_active=True, default_model="gpt-4")
p2 = MagicMock(name="Anthropic", provider_type="anthropic", is_active=False, default_model=None)
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
)
assert "LLM провайдеры" in text
assert "OpenAI" in text
assert "Anthropic" in text
assert task_id is None
assert len(actions) == 1
# #endregion test_list_llm_providers
# #region test_list_llm_providers_empty [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm.LLMProviderService")
async def test_list_llm_providers_empty(
self, mock_svc_cls,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_llm import handle_list_llm_providers
svc = MagicMock()
mock_svc_cls.return_value = svc
svc.get_all_providers.return_value = []
text, task_id, actions = await handle_list_llm_providers(
intent, current_user, task_manager, config_manager, db
)
assert "не настроены" in text
# #endregion test_list_llm_providers_empty
# #region test_get_llm_status [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm.LLMProviderService")
async def test_get_llm_status(
self, mock_svc_cls,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_llm import handle_get_llm_status
svc = MagicMock()
mock_svc_cls.return_value = svc
p1 = MagicMock(id="p1", name="OpenAI", provider_type="openai", is_active=True, default_model="gpt-4")
svc.get_all_providers.return_value = [p1]
svc.get_decrypted_api_key.return_value = "sk-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
)
assert "LLM Статус" in text
assert "OpenAI" in text
assert "готов" in text
assert task_id is None
# #endregion test_get_llm_status
# #region test_get_llm_status_no_providers [C:2] [TYPE Function]
@patch("src.api.routes.assistant._tool_llm.LLMProviderService")
async def test_get_llm_status_no_providers(
self, mock_svc_cls,
intent, current_user, task_manager, config_manager, db,
):
from src.api.routes.assistant._tool_llm import handle_get_llm_status
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 "не настроены" in text
# #endregion test_get_llm_status_no_providers
# ── _tool_registry.py ─────────────────────────────────────────────────────────
class TestToolRegistry:
"""AssistantTool registry, dispatch, permissions, and catalog."""
def _clear_registry(self):
"""Clear the global _tools registry."""
import src.api.routes.assistant._tool_registry as reg
reg._tools.clear()
# #region test_assistant_tool_decorator [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_assistant_tool_decorator(self):
from src.api.routes.assistant._tool_registry import assistant_tool, _tools
self._clear_registry()
@assistant_tool(
operation="test_op",
domain="test",
description="Test tool",
risk_level="safe",
requires_confirmation=False,
)
async def my_handler(intent, user, tm, cm, db):
return ("done", None, [])
assert "test_op" in _tools
tool = _tools["test_op"]
assert tool.operation == "test_op"
assert tool.domain == "test"
assert tool.description == "Test tool"
assert tool.risk_level == "safe"
assert tool.requires_confirmation is False
assert hasattr(my_handler, "__assistant_tool__")
text, tid, acts = await _tools["test_op"].handler({}, None, None, None, None)
assert text == "done"
# #endregion test_assistant_tool_decorator
# #region test_dispatch_supported_op [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_dispatch_supported_op(self):
from src.api.routes.assistant._tool_registry import assistant_tool, dispatch, _tools
self._clear_registry()
@assistant_tool(operation="my_op", domain="t", description="t")
async def my_handler(intent, user, tm, cm, db):
return ("result", "task-1", [])
text, tid, acts = await dispatch("my_op", {}, None, None, None, None)
assert text == "result"
assert tid == "task-1"
# #endregion test_dispatch_supported_op
# #region test_dispatch_unsupported_op [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_dispatch_unsupported_op(self):
from src.api.routes.assistant._tool_registry import dispatch
self._clear_registry()
with pytest.raises(HTTPException) as exc:
await dispatch("nonexistent", {}, None, None, None, None)
assert exc.value.status_code == 400
# #endregion test_dispatch_unsupported_op
# #region test_get_safe_ops [C:2] [TYPE Function]
def test_get_safe_ops(self):
from src.api.routes.assistant._tool_registry import assistant_tool, get_safe_ops, _tools
self._clear_registry()
@assistant_tool(operation="safe_op", domain="t", description="t", risk_level="safe")
async def h1(i, u, t, c, d): return ("", None, [])
@assistant_tool(operation="guarded_op", domain="t", description="t", risk_level="guarded")
async def h2(i, u, t, c, d): return ("", None, [])
safe_ops = get_safe_ops()
assert "safe_op" in safe_ops
assert "guarded_op" not in safe_ops
assert "dataset_review_answer_context" in safe_ops # hardcoded safe op
# #endregion test_get_safe_ops
# #region test_get_catalog [C:2] [TYPE Function]
def test_get_catalog(self, current_user, config_manager, db):
from src.api.routes.assistant._tool_registry import assistant_tool, get_catalog, _tools
self._clear_registry()
@assistant_tool(operation="op1", domain="d1", description="desc1", risk_level="safe")
async def h1(i, u, t, c, d): return ("", None, [])
@assistant_tool(operation="op2", domain="d2", description="desc2", risk_level="guarded", requires_confirmation=True)
async def h2(i, u, t, c, d): return ("", None, [])
catalog = get_catalog(current_user, config_manager, db)
ops = [e["operation"] for e in catalog]
assert "op1" in ops
assert "op2" in ops
entry = next(e for e in catalog if e["operation"] == "op2")
assert entry["risk_level"] == "guarded"
assert entry["requires_confirmation"] is True
# #endregion test_get_catalog
# #region test_get_permission_checks [C:2] [TYPE Function]
def test_get_permission_checks(self):
from src.api.routes.assistant._tool_registry import assistant_tool, get_permission_checks, _tools
self._clear_registry()
@assistant_tool(operation="perm_op", domain="t", description="t",
permission_checks=[("resource", "action")])
async def h1(i, u, t, c, d): return ("", None, [])
@assistant_tool(operation="no_perm_op", domain="t", description="t")
async def h2(i, u, t, c, d): return ("", None, [])
checks = get_permission_checks()
assert "perm_op" in checks
assert checks["perm_op"] == [("resource", "action")]
assert "no_perm_op" not in checks
# #endregion test_get_permission_checks
# #region test_check_any_permission_allows_valid [C:2] [TYPE Function]
def test_check_any_permission_allows_valid(self, current_user):
from src.api.routes.assistant._tool_registry import _check_any_permission
with patch("src.api.routes.assistant._tool_registry.has_permission") as mock_hp:
mock_hp.return_value = lambda u: None
_check_any_permission(current_user, [("resource", "action")])
mock_hp.assert_called_with("resource", "action")
# #endregion test_check_any_permission_allows_valid
# #region test_check_any_permission_denies_all [C:2] [TYPE Function]
def test_check_any_permission_denies_all(self, current_user):
from src.api.routes.assistant._tool_registry import _check_any_permission
with patch("src.api.routes.assistant._tool_registry.has_permission") as mock_hp:
mock_hp.return_value = lambda u: (_ for _ in ()).throw(HTTPException(status_code=403))
with pytest.raises(HTTPException) as exc:
_check_any_permission(current_user, [("r1", "a1"), ("r2", "a2")])
assert exc.value.status_code == 403
# #endregion test_check_any_permission_denies_all
# #region test_has_any_permission [C:2] [TYPE Function]
def test_has_any_permission(self, current_user):
from src.api.routes.assistant._tool_registry import _has_any_permission
with patch("src.api.routes.assistant._tool_registry.has_permission") as mock_hp:
mock_hp.return_value = lambda u: None
assert _has_any_permission(current_user, [("r", "a")]) is True
mock_hp.return_value = lambda u: (_ for _ in ()).throw(HTTPException(status_code=403))
assert _has_any_permission(current_user, [("r", "a")]) is False
# #endregion test_has_any_permission
# ── _llm_planner.py ───────────────────────────────────────────────────────────
class TestLLMPlanner:
"""_build_tool_catalog and _coerce_intent_entities."""
# #region test_build_tool_catalog_no_review [C:2] [TYPE Function]
def test_build_tool_catalog_no_review(self, current_user, config_manager, db):
from src.api.routes.assistant._llm_planner import _build_tool_catalog
with patch("src.api.routes.assistant._llm_planner.get_catalog") as mock_cat:
mock_cat.return_value = [{"operation": "op1", "domain": "d"}]
result = _build_tool_catalog(current_user, config_manager, db)
assert len(result) == 1
assert result[0]["operation"] == "op1"
# #endregion test_build_tool_catalog_no_review
# #region test_build_tool_catalog_with_review [C:2] [TYPE Function]
def test_build_tool_catalog_with_review(self, current_user, config_manager, db):
from src.api.routes.assistant._llm_planner import _build_tool_catalog
with patch("src.api.routes.assistant._llm_planner.get_catalog") as mock_cat:
mock_cat.return_value = []
with patch("src.api.routes.assistant._llm_planner._has_any_permission", return_value=True):
result = _build_tool_catalog(
current_user, config_manager, db,
dataset_review_context={"session_id": "sess-1"},
)
assert len(result) == 4 # 4 dataset review tools
ops = [t["operation"] for t in result]
assert "dataset_review_answer_context" in ops
assert "dataset_review_approve_mappings" in ops
# #endregion test_build_tool_catalog_with_review
# #region test_build_tool_catalog_with_review_filtered [C:2] [TYPE Function]
def test_build_tool_catalog_with_review_filtered(self, current_user, config_manager, db):
from src.api.routes.assistant._llm_planner import _build_tool_catalog
with patch("src.api.routes.assistant._llm_planner.get_catalog") as mock_cat:
mock_cat.return_value = []
with patch("src.api.routes.assistant._llm_planner._has_any_permission", return_value=False):
result = _build_tool_catalog(
current_user, config_manager, db,
dataset_review_context={"session_id": "sess-1"},
)
assert len(result) == 0
# #endregion test_build_tool_catalog_with_review_filtered
# #region test_coerce_intent_entities [C:2] [TYPE Function]
def test_coerce_intent_entities(self):
from src.api.routes.assistant._llm_planner import _coerce_intent_entities
intent = {"entities": {"dashboard_id": "42", "dataset_id": "99", "branch_name": " feature/test "}}
result = _coerce_intent_entities(intent)
assert result["entities"]["dashboard_id"] == 42
assert result["entities"]["dataset_id"] == 99
assert result["entities"]["branch_name"] == "feature/test"
# #endregion test_coerce_intent_entities
# #region test_coerce_intent_entities_non_dict [C:2] [TYPE Function]
def test_coerce_intent_entities_non_dict(self):
from src.api.routes.assistant._llm_planner import _coerce_intent_entities
intent = {"message": "hello"}
result = _coerce_intent_entities(intent)
assert result["entities"] == {}
# #endregion test_coerce_intent_entities_non_dict
# ── _llm_planner_intent.py ───────────────────────────────────────────────────
class TestLLMPlannerIntent:
"""_plan_intent_with_llm and _authorize_intent."""
# #region test_authorize_intent_passes [C:2] [TYPE Function]
def test_authorize_intent_passes(self, current_user):
from src.api.routes.assistant._llm_planner_intent import _authorize_intent
with patch("src.api.routes.assistant._llm_planner_intent.get_permission_checks") as mock_checks:
mock_checks.return_value = {"test_op": [("resource", "action")]}
with patch("src.api.routes.assistant._llm_planner_intent._check_any_permission") as mock_check:
_authorize_intent({"operation": "test_op"}, current_user)
mock_check.assert_called_with(current_user, [("resource", "action")])
# #endregion test_authorize_intent_passes
# #region test_authorize_intent_no_checks [C:2] [TYPE Function]
def test_authorize_intent_no_checks(self, current_user):
from src.api.routes.assistant._llm_planner_intent import _authorize_intent
with patch("src.api.routes.assistant._llm_planner_intent.get_permission_checks") as mock_checks:
mock_checks.return_value = {}
_authorize_intent({"operation": "safe_op"}, current_user) # Should not raise
# #endregion test_authorize_intent_no_checks
# ── _dispatch.py ──────────────────────────────────────────────────────────────
class TestAssistantDispatch:
"""_get_git_service and _clarification_text_for_intent."""
# #region test_get_git_service [C:2] [TYPE Function]
def test_get_git_service(self):
from src.api.routes.assistant._dispatch import _get_git_service
import src.api.routes.assistant._dispatch as dispatch_mod
dispatch_mod._git_service_instance = None
svc = _get_git_service()
assert svc is not None
svc2 = _get_git_service()
assert svc2 is svc
# #endregion test_get_git_service
# #region test_clarification_text_for_intent [C:2] [TYPE Function]
def test_clarification_text_for_intent(self):
from src.api.routes.assistant._dispatch import _clarification_text_for_intent
# Known operation
text = _clarification_text_for_intent(
{"operation": "create_branch"}, "Missing params"
)
assert "Нужно уточнение" in text
assert "ветки" in text
# Unknown operation — falls back to detail_text
text = _clarification_text_for_intent(
{"operation": "unknown_op"}, "Custom error detail"
)
assert text == "Custom error detail"
# None intent
text = _clarification_text_for_intent(None, "No intent")
assert text == "No intent"
# #endregion test_clarification_text_for_intent
# #region test_dispatch_intent_wrapper [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_dispatch_intent_wrapper(self):
from src.api.routes.assistant._dispatch import _dispatch_intent
with patch("src.api.routes.assistant._dispatch._registry_dispatch") as mock_reg:
mock_reg.return_value = ("done", "task-1", [])
result = await _dispatch_intent(
{"operation": "test"}, None, None, None, None
)
assert result == ("done", "task-1", [])
mock_reg.assert_called_with("test", {"operation": "test"}, None, None, None, None)
# #endregion test_dispatch_intent_wrapper
# ── _resolvers.py ─────────────────────────────────────────────────────────────
class TestAssistantResolvers:
"""_resolve_env_id, _is_production_env, _resolve_provider_id, etc."""
# #region test_extract_id [C:2] [TYPE Function]
def test_extract_id(self):
from src.api.routes.assistant._resolvers import _extract_id
result = _extract_id("my ref is abc123", [r"ref is (\w+)"])
assert result == "abc123"
result = _extract_id("no match", [r"nothing"])
assert result is None
# #endregion test_extract_id
# #region test_resolve_env_id [C:2] [TYPE Function]
def test_resolve_env_id(self):
from src.api.routes.assistant._resolvers import _resolve_env_id
config_manager = MagicMock()
env1 = _make_env("env-dev", "Development")
env2 = _make_env("env-prod", "Production")
config_manager.get_environments = MagicMock(return_value=[env1, env2])
assert _resolve_env_id("env-dev", config_manager) == "env-dev"
assert _resolve_env_id("Development", config_manager) == "env-dev"
assert _resolve_env_id(None, config_manager) is None
assert _resolve_env_id("unknown", config_manager) is None
# #endregion test_resolve_env_id
# #region test_is_production_env [C:2] [TYPE Function]
def test_is_production_env(self):
from src.api.routes.assistant._resolvers import _is_production_env
config_manager = MagicMock()
env1 = _make_env("env-dev", "Development")
env2 = _make_env("env-prod", "Production")
config_manager.get_environments = MagicMock(return_value=[env1, env2])
assert _is_production_env("env-prod", config_manager) is True
assert _is_production_env("env-dev", config_manager) is False
assert _is_production_env("prod", config_manager) is True
assert _is_production_env("staging", config_manager) is False
assert _is_production_env(None, config_manager) is False
# #endregion test_is_production_env
# #region test_resolve_provider_id_by_token [C:2] [TYPE Function]
def test_resolve_provider_id_by_token(self):
from src.api.routes.assistant._resolvers import _resolve_provider_id
db = MagicMock()
config_manager = MagicMock()
p1 = MagicMock(id="p1", name="OpenAI")
p2 = MagicMock(id="p2", name="Anthropic")
with patch("src.api.routes.assistant._resolvers.LLMProviderService") as mock_svc_cls:
svc = MagicMock()
mock_svc_cls.return_value = svc
svc.get_all_providers.return_value = [p1, p2]
assert _resolve_provider_id("OpenAI", db) == "p1"
assert _resolve_provider_id("p2", db) == "p2"
assert _resolve_provider_id(None, db) == "p1" # returns first active
# #endregion test_resolve_provider_id_by_token
# #region test_resolve_provider_id_no_providers [C:2] [TYPE Function]
def test_resolve_provider_id_no_providers(self):
from src.api.routes.assistant._resolvers import _resolve_provider_id
db = MagicMock()
with patch("src.api.routes.assistant._resolvers.LLMProviderService") as mock_svc_cls:
svc = MagicMock()
mock_svc_cls.return_value = svc
svc.get_all_providers.return_value = []
assert _resolve_provider_id(None, db) is None
# #endregion test_resolve_provider_id_no_providers
# #region test_get_default_environment_id [C:2] [TYPE Function]
def test_get_default_environment_id(self):
from src.api.routes.assistant._resolvers import _get_default_environment_id
config_manager = MagicMock()
env1 = _make_env("env-dev", "Dev", is_default=True)
env2 = _make_env("env-prod", "Prod", is_default=False)
config_manager.get_environments = MagicMock(return_value=[env1, env2])
assert _get_default_environment_id(config_manager) == "env-dev"
config_manager.get_environments = MagicMock(return_value=[])
assert _get_default_environment_id(config_manager) is None
# #endregion test_get_default_environment_id
# #region test_get_environment_name_by_id [C:2] [TYPE Function]
def test_get_environment_name_by_id(self):
from src.api.routes.assistant._resolvers import _get_environment_name_by_id
config_manager = MagicMock()
env = _make_env("env-dev", "Development")
config_manager.get_environments = MagicMock(return_value=[env])
assert _get_environment_name_by_id("env-dev", config_manager) == "Development"
assert _get_environment_name_by_id(None, config_manager) == "unknown"
assert _get_environment_name_by_id("missing", config_manager) == "missing"
# #endregion test_get_environment_name_by_id
# #region test_resolve_dashboard_id_entity_int [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_resolve_dashboard_id_entity_int(self):
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
config_manager = MagicMock()
result = await _resolve_dashboard_id_entity(
{"dashboard_id": 42}, config_manager
)
assert result == 42
# #endregion test_resolve_dashboard_id_entity_int
# #region test_resolve_dashboard_id_entity_str_digit [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_resolve_dashboard_id_entity_str_digit(self):
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
config_manager = MagicMock()
result = await _resolve_dashboard_id_entity(
{"dashboard_id": "42"}, config_manager
)
assert result == 42
# #endregion test_resolve_dashboard_id_entity_str_digit
# #region test_resolve_dashboard_id_entity_ref_fallback [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_resolve_dashboard_id_entity_ref_fallback(self):
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
config_manager = MagicMock()
with patch("src.api.routes.assistant._resolvers._resolve_env_id", return_value="env-dev"):
with patch("src.api.routes.assistant._resolvers._get_default_environment_id", return_value="env-dev"):
with patch("src.api.routes.assistant._resolvers._resolve_dashboard_id_by_ref", return_value=99):
result = await _resolve_dashboard_id_entity(
{"dashboard_id": "my-slug"}, config_manager
)
assert result == 99
# #endregion test_resolve_dashboard_id_entity_ref_fallback
# #region test_resolve_dashboard_id_entity_no_match [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_resolve_dashboard_id_entity_no_match(self):
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
config_manager = MagicMock()
result = await _resolve_dashboard_id_entity({}, config_manager)
assert result is None
# #endregion test_resolve_dashboard_id_entity_no_match
# #region test_resolve_dashboard_id_by_ref [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_resolve_dashboard_id_by_ref(self):
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
config_manager = MagicMock()
env = _make_env("env-dev", "Development")
config_manager.get_environments = MagicMock(return_value=[env])
with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_client_cls:
client = MagicMock()
mock_client_cls.return_value = client
client.get_dashboards = AsyncMock(return_value=(None, [
{"id": 1, "dashboard_title": "Dashboard", "slug": "dash-one"},
]))
result = await _resolve_dashboard_id_by_ref("dash-one", "env-dev", config_manager)
assert result == 1
# #endregion test_resolve_dashboard_id_by_ref
# #region test_resolve_dashboard_id_by_ref_no_env [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_resolve_dashboard_id_by_ref_no_env(self):
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
config_manager = MagicMock()
config_manager.get_environments = MagicMock(return_value=[])
result = await _resolve_dashboard_id_by_ref("ref", "nonexistent", config_manager)
assert result is None
# #endregion test_resolve_dashboard_id_by_ref_no_env
# #region test_resolve_dashboard_id_by_ref_no_ref [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_resolve_dashboard_id_by_ref_no_ref(self):
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
config_manager = MagicMock()
result = await _resolve_dashboard_id_by_ref(None, None, config_manager)
assert result is None
# #endregion test_resolve_dashboard_id_by_ref_no_ref
# #region test_extract_result_deep_links_migration [C:2] [TYPE Function]
def test_extract_result_deep_links_migration(self):
from src.api.routes.assistant._resolvers import _extract_result_deep_links
config_manager = MagicMock()
env = _make_env("env-prod", "Production")
config_manager.get_environments = MagicMock(return_value=[env])
task = MagicMock(
plugin_id="superset-migration",
params={"selected_ids": [123], "target_env_id": "env-prod"},
result={"migrated_dashboards": [{"id": 123}]},
)
actions = _extract_result_deep_links(task, config_manager)
assert len(actions) >= 1
# #endregion test_extract_result_deep_links_migration
# #region test_extract_result_deep_links_backup [C:2] [TYPE Function]
def test_extract_result_deep_links_backup(self):
from src.api.routes.assistant._resolvers import _extract_result_deep_links
config_manager = MagicMock()
env = _make_env("env-dev", "Dev")
config_manager.get_environments = MagicMock(return_value=[env])
task = MagicMock(
plugin_id="superset-backup",
params={"dashboard_ids": [456], "environment_id": "env-dev"},
result={"dashboards": [{"id": 456}]},
)
actions = _extract_result_deep_links(task, config_manager)
assert len(actions) >= 1
# #endregion test_extract_result_deep_links_backup
# #region test_extract_result_deep_links_validation [C:2] [TYPE Function]
def test_extract_result_deep_links_validation(self):
from src.api.routes.assistant._resolvers import _extract_result_deep_links
config_manager = MagicMock()
env = _make_env("env-dev", "Dev")
config_manager.get_environments = MagicMock(return_value=[env])
task = MagicMock(
plugin_id="llm_dashboard_validation",
params={"dashboard_id": 789, "environment_id": "env-dev"},
result={},
)
actions = _extract_result_deep_links(task, config_manager)
assert len(actions) >= 1
# #endregion test_extract_result_deep_links_validation
# #region test_extract_result_deep_links_no_match [C:2] [TYPE Function]
def test_extract_result_deep_links_no_match(self):
from src.api.routes.assistant._resolvers import _extract_result_deep_links
config_manager = MagicMock()
task = MagicMock(
plugin_id="unknown_plugin",
params={},
result={},
)
actions = _extract_result_deep_links(task, config_manager)
assert actions == []
# #endregion test_extract_result_deep_links_no_match
# #region test_build_task_observability_summary_migration [C:2] [TYPE Function]
def test_build_task_observability_summary_migration(self):
from src.api.routes.assistant._resolvers import _build_task_observability_summary
config_manager = MagicMock()
env = _make_env("env-prod", "Production")
config_manager.get_environments = MagicMock(return_value=[env])
task = MagicMock(
plugin_id="superset-migration",
status="SUCCESS",
params={"target_env_id": "env-prod"},
result={
"migrated_dashboards": [{"id": 1}],
"selected_dashboards": 5,
"mapping_count": 10,
"failed_dashboards": [],
},
)
summary = _build_task_observability_summary(task, config_manager)
assert "Сводка миграции" in summary
assert "5" in summary
# #endregion test_build_task_observability_summary_migration
# #region test_build_task_observability_summary_migration_with_errors [C:2] [TYPE Function]
def test_build_task_observability_summary_migration_with_errors(self):
from src.api.routes.assistant._resolvers import _build_task_observability_summary
config_manager = MagicMock()
env = _make_env("env-prod", "Production")
config_manager.get_environments = MagicMock(return_value=[env])
task = MagicMock(
plugin_id="superset-migration",
status="FAILED",
params={"target_env_id": "env-prod"},
result={
"migrated_dashboards": [{"id": 1}],
"selected_dashboards": 3,
"mapping_count": 5,
"failed_dashboards": [
{"id": 2, "title": "BrokenDash", "error": "timeout"}
],
},
)
summary = _build_task_observability_summary(task, config_manager)
assert "с ошибками" in summary
assert "Внимание" in summary
# #endregion test_build_task_observability_summary_migration_with_errors
# #region test_build_task_observability_summary_backup [C:2] [TYPE Function]
def test_build_task_observability_summary_backup(self):
from src.api.routes.assistant._resolvers import _build_task_observability_summary
config_manager = MagicMock()
env = _make_env("env-dev", "Dev")
config_manager.get_environments = MagicMock(return_value=[env])
task = MagicMock(
plugin_id="superset-backup",
status="SUCCESS",
params={"environment_id": "env-dev"},
result={
"total_dashboards": 10,
"backed_up_dashboards": 8,
"failed_dashboards": 2,
"failures": [{"id": 3, "title": "Broken", "error": "err"}],
},
)
summary = _build_task_observability_summary(task, config_manager)
assert "Сводка бэкапа" in summary
assert "8" in summary
# #endregion test_build_task_observability_summary_backup
# #region test_build_task_observability_summary_validation [C:2] [TYPE Function]
def test_build_task_observability_summary_validation(self):
from src.api.routes.assistant._resolvers import _build_task_observability_summary
config_manager = MagicMock()
task = MagicMock(
plugin_id="llm_dashboard_validation",
status="SUCCESS",
params={},
result={
"status": "ALL_PASSED",
"summary": "All checks passed",
"issues": [],
},
)
summary = _build_task_observability_summary(task, config_manager)
assert "Сводка валидации" in summary
# #endregion test_build_task_observability_summary_validation
# #region test_build_task_observability_summary_fallback [C:2] [TYPE Function]
def test_build_task_observability_summary_fallback(self):
from src.api.routes.assistant._resolvers import _build_task_observability_summary
config_manager = MagicMock()
task = MagicMock(
plugin_id="unknown",
status="SUCCESS",
params={},
result={},
)
summary = _build_task_observability_summary(task, config_manager)
assert "Задача завершена" in summary
task = MagicMock(
plugin_id="unknown",
status="RUNNING",
params={},
result={},
)
summary = _build_task_observability_summary(task, config_manager)
assert summary == ""
# #endregion test_build_task_observability_summary_fallback
# ── _routes.py ────────────────────────────────────────────────────────────────
class TestAssistantRoutes:
"""send_message route — the main assistant entry point."""
# #region test_send_message_clarification [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_send_message_clarification(self):
from src.api.routes.assistant._routes import send_message
from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
CONFIRMATIONS.clear()
req = AssistantMessageRequest(message="do something vague")
with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"):
with patch("src.api.routes.assistant._routes._append_history"):
with patch("src.api.routes.assistant._routes._persist_message"):
with patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[]):
with patch("src.api.routes.assistant._routes._plan_intent_with_llm", return_value=None):
with patch("src.api.routes.assistant._routes._parse_command", return_value={"domain": "unknown", "operation": "clarify", "entities": {}, "confidence": 0.3}):
with patch("src.api.routes.assistant._routes._audit"):
with patch("src.api.routes.assistant._routes._persist_audit"):
with patch("src.api.routes.assistant._routes._load_dataset_review_context", return_value=None):
current_user = MagicMock(id="user-1")
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
resp = await send_message(
req, current_user, task_manager, config_manager, db
)
assert resp.state == "needs_clarification"
assert resp.intent is not None
assert len(resp.actions) > 0
# #endregion test_send_message_clarification
# #region test_send_message_safe_operation [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_send_message_safe_operation(self):
from src.api.routes.assistant._routes import send_message
from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
from src.api.routes.assistant._tool_registry import assistant_tool, _tools
CONFIRMATIONS.clear()
_tools.clear()
@assistant_tool(operation="show_capabilities", domain="assistant", description="Show capabilities", risk_level="safe")
async def caps(intent, user, tm, cm, db):
return ("Here are capabilities", None, [])
req = AssistantMessageRequest(message="what can you do")
with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"):
with patch("src.api.routes.assistant._routes._append_history"):
with patch("src.api.routes.assistant._routes._persist_message"):
with patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[{"operation": "show_capabilities", "domain": "assistant", "description": "Show caps"}]):
with patch("src.api.routes.assistant._routes._plan_intent_with_llm", return_value={"domain": "assistant", "operation": "show_capabilities", "entities": {}, "confidence": 0.9, "risk_level": "safe", "requires_confirmation": False}):
with patch("src.api.routes.assistant._routes._audit"):
with patch("src.api.routes.assistant._routes._persist_audit"):
with patch("src.api.routes.assistant._routes._load_dataset_review_context", return_value=None):
current_user = MagicMock(id="user-1")
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
resp = await send_message(
req, current_user, task_manager, config_manager, db
)
assert resp.state in ("success", "started")
assert "capabilities" in resp.text.lower()
# #endregion test_send_message_safe_operation
# #region test_send_message_guarded_needs_confirmation [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_send_message_guarded_needs_confirmation(self):
from src.api.routes.assistant._routes import send_message
from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
from src.api.routes.assistant._tool_registry import _tools
CONFIRMATIONS.clear()
_tools.clear()
req = AssistantMessageRequest(message="deploy dashboard 42 to prod")
with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"):
with patch("src.api.routes.assistant._routes._append_history"):
with patch("src.api.routes.assistant._routes._persist_message"):
with patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[{"operation": "deploy_dashboard", "domain": "git", "description": "Deploy"}]):
with patch("src.api.routes.assistant._routes._plan_intent_with_llm", return_value={"domain": "git", "operation": "deploy_dashboard", "entities": {"environment": "prod", "dashboard_id": 42}, "confidence": 0.9, "risk_level": "guarded", "requires_confirmation": True}):
with patch("src.api.routes.assistant._routes._async_confirmation_summary", return_value="Confirm deploy?"):
with patch("src.api.routes.assistant._routes._audit"):
with patch("src.api.routes.assistant._routes._persist_audit"):
with patch("src.api.routes.assistant._routes._load_dataset_review_context", return_value=None):
current_user = MagicMock(id="user-1")
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
resp = await send_message(
req, current_user, task_manager, config_manager, db
)
assert resp.state == "needs_confirmation"
assert resp.confirmation_id is not None
assert len(resp.actions) > 1
# #endregion test_send_message_guarded_needs_confirmation
# #region test_send_message_dataset_review_override [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_send_message_dataset_review_override(self):
from src.api.routes.assistant._routes import send_message
from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
from src.api.routes.assistant._tool_registry import _tools
CONFIRMATIONS.clear()
_tools.clear()
req = AssistantMessageRequest(message="review dataset", dataset_review_session_id="sess-1")
with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"):
with patch("src.api.routes.assistant._routes._append_history"):
with patch("src.api.routes.assistant._routes._persist_message"):
with patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[{"operation": "dataset_review_answer_context", "domain": "dataset_review"}]):
with patch("src.api.routes.assistant._routes._plan_intent_with_llm", return_value={"domain": "dataset_review", "operation": "dataset_review_answer_context", "entities": {}, "confidence": 0.9, "risk_level": "safe", "requires_confirmation": False}):
with patch("src.api.routes.assistant._routes._parse_command", return_value={"domain": "unknown", "operation": "clarify", "entities": {}, "confidence": 0.3}):
with patch("src.api.routes.assistant._routes._plan_dataset_review_intent", return_value=None):
with patch("src.api.routes.assistant._routes._authorize_intent"):
with patch("src.api.routes.assistant._routes.get_safe_ops", return_value={"dataset_review_answer_context"}):
with patch("src.api.routes.assistant._routes.dispatch",
return_value=("Review started", "task-1", [])):
with patch("src.api.routes.assistant._routes._audit"):
with patch("src.api.routes.assistant._routes._persist_audit"):
with patch("src.api.routes.assistant._routes._load_dataset_review_context", return_value={"session_id": "sess-1"}):
current_user = MagicMock(id="user-1")
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
resp = await send_message(
req, current_user, task_manager, config_manager, db
)
assert resp.state in ("success", "started")
# #endregion test_send_message_dataset_review_override
# #region test_send_message_http_exception_denied [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_send_message_http_exception_denied(self):
from src.api.routes.assistant._routes import send_message
from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
from src.api.routes.assistant._tool_registry import _tools
CONFIRMATIONS.clear()
_tools.clear()
req = AssistantMessageRequest(message="something")
with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"):
with patch("src.api.routes.assistant._routes._append_history"):
with patch("src.api.routes.assistant._routes._persist_message"):
with patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[{"operation": "guarded_op"}]):
with patch("src.api.routes.assistant._routes._plan_intent_with_llm", return_value={"domain": "d", "operation": "guarded_op", "entities": {}, "confidence": 0.9, "risk_level": "guarded", "requires_confirmation": False}):
with patch("src.api.routes.assistant._routes._authorize_intent", side_effect=HTTPException(status_code=403, detail="Forbidden")):
with patch("src.api.routes.assistant._routes._audit"):
with patch("src.api.routes.assistant._routes._persist_audit"):
with patch("src.api.routes.assistant._routes._load_dataset_review_context", return_value=None):
current_user = MagicMock(id="user-1")
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
resp = await send_message(
req, current_user, task_manager, config_manager, db
)
assert resp.state == "denied"
# #endregion test_send_message_http_exception_denied
# #region test_send_message_http_exception_clarification [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_send_message_http_exception_clarification(self):
from src.api.routes.assistant._routes import send_message
from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
from src.api.routes.assistant._tool_registry import _tools
CONFIRMATIONS.clear()
_tools.clear()
req = AssistantMessageRequest(message="do something")
with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"):
with patch("src.api.routes.assistant._routes._append_history"):
with patch("src.api.routes.assistant._routes._persist_message"):
with patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[]):
with patch("src.api.routes.assistant._routes._plan_intent_with_llm", return_value={"domain": "d", "operation": "commit_changes", "entities": {}, "confidence": 0.9, "risk_level": "guarded", "requires_confirmation": False}):
with patch("src.api.routes.assistant._routes._authorize_intent"):
with patch("src.api.routes.assistant._routes.get_safe_ops", return_value={"commit_changes"}):
with patch("src.api.routes.assistant._routes.dispatch",
side_effect=HTTPException(status_code=422, detail="Missing dashboard_id")):
with patch("src.api.routes.assistant._routes._audit"):
with patch("src.api.routes.assistant._routes._persist_audit"):
with patch("src.api.routes.assistant._routes._load_dataset_review_context", return_value=None):
current_user = MagicMock(id="user-1")
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
resp = await send_message(
req, current_user, task_manager, config_manager, db
)
assert resp.state == "needs_clarification"
# #endregion test_send_message_http_exception_clarification
# #region test_send_message_planner_fallback [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_send_message_planner_fallback(self):
from src.api.routes.assistant._routes import send_message
from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
from src.api.routes.assistant._tool_registry import _tools
CONFIRMATIONS.clear()
_tools.clear()
req = AssistantMessageRequest(message="show capabilities")
with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"):
with patch("src.api.routes.assistant._routes._append_history"):
with patch("src.api.routes.assistant._routes._persist_message"):
with patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[{"operation": "show_capabilities", "domain": "assistant"}]):
with patch("src.api.routes.assistant._routes._plan_intent_with_llm", side_effect=Exception("LLM down")):
with patch("src.api.routes.assistant._routes._parse_command", return_value={"domain": "assistant", "operation": "show_capabilities", "entities": {}, "confidence": 0.9, "risk_level": "safe", "requires_confirmation": False}):
with patch("src.api.routes.assistant._routes._authorize_intent"):
with patch("src.api.routes.assistant._routes.get_safe_ops", return_value={"show_capabilities"}):
with patch("src.api.routes.assistant._routes.dispatch",
return_value=("Capabilities list", None, [])):
with patch("src.api.routes.assistant._routes._audit"):
with patch("src.api.routes.assistant._routes._persist_audit"):
with patch("src.api.routes.assistant._routes._load_dataset_review_context", return_value=None):
current_user = MagicMock(id="user-1")
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
resp = await send_message(
req, current_user, task_manager, config_manager, db
)
# show_capabilities is safe — should dispatch without confirmation
assert resp.state in ("success", "started")
# #endregion test_send_message_planner_fallback
# #endregion Test.Assistant.Tools