Группы исправлений: - Группа 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
1894 lines
89 KiB
Python
1894 lines
89 KiB
Python
# #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
|