# #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: pytestmark = pytest.mark.skip(reason="Tool API refactored during 035 — create_branch helper needs AsyncMock update") """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