Files
ss-tools/backend/tests/api/test_assistant_tools.py
root 632b730fff chore: migrate GRACE-Poly anchors to hierarchical dotted naming
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes
2026-07-22 11:48:15 +03:00

1820 lines
85 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

# #region Test.Assistant.Tools [C:2] [TYPE Module] [SEMANTICS test,assistant,tools,handlers]
# @BRIEF Tests for all assistant tool handlers, registry, resolvers, dispatch, and routes.
# @RELATION BINDS_TO -> [Api.ToolRegistry.AssistantToolRegistry]
# @RELATION BINDS_TO -> [Api.Resolvers.AssistantResolvers]
# @RELATION BINDS_TO -> [Api.Dispatch.AssistantDispatch]
# @RELATION BINDS_TO -> [Api.Routes.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.Assistant.TestShowCapabilitiesWithTools [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.Assistant.TestShowCapabilitiesWithTools
# #region Test.Assistant.TestShowCapabilitiesEmpty [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.Assistant.TestShowCapabilitiesEmpty
# ── _tool_commit.py ──────────────────────────────────────────────────────────
class TestToolCommit:
"""handle_commit_changes — commit dashboard changes."""
# #region Test.Assistant.TestCommitChangesSuccess [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.Assistant.TestCommitChangesSuccess
# #region Test.Assistant.TestCommitChangesMissingDashboard [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.Assistant.TestCommitChangesMissingDashboard
# ── _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.Assistant.TestCreateBranchSuccess [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.Assistant.TestCreateBranchSuccess
# #region Test.Assistant.TestCreateBranchMissingParams [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.Assistant.TestCreateBranchMissingParams
# ── _tool_deploy.py ───────────────────────────────────────────────────────────
class TestToolDeploy:
"""handle_deploy_dashboard — deploy dashboard to environment."""
# #region Test.Assistant.TestDeployDashboardSuccess [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.Assistant.TestDeployDashboardSuccess
# #region Test.Assistant.TestDeployDashboardMissingParams [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.Assistant.TestDeployDashboardMissingParams
# ── _tool_llm_documentation.py ────────────────────────────────────────────────
class TestToolLlmDocumentation:
"""handle_run_llm_documentation — generate LLM documentation."""
# #region Test.Assistant.TestLlmDocumentationSuccess [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.Assistant.TestLlmDocumentationSuccess
# #region Test.Assistant.TestLlmDocumentationMissingParams [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.Assistant.TestLlmDocumentationMissingParams
# ── _tool_list_environments.py ────────────────────────────────────────────────
class TestToolListEnvironments:
"""handle_list_environments — list configured environments."""
# #region Test.Assistant.TestListEnvironmentsWithEnvs [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.Assistant.TestListEnvironmentsWithEnvs
# #region Test.Assistant.TestListEnvironmentsEmpty [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.Assistant.TestListEnvironmentsEmpty
# ── _tool_health_summary.py ───────────────────────────────────────────────────
class TestToolHealthSummary:
"""handle_get_health_summary — dashboard health summary."""
# #region Test.Assistant.TestHealthSummarySuccess [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.Assistant.TestHealthSummarySuccess
# #region Test.Assistant.TestHealthSummaryWithFailures [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.Assistant.TestHealthSummaryWithFailures
# ── _tool_backup.py ───────────────────────────────────────────────────────────
class TestToolBackup:
"""handle_run_backup — run backup."""
# #region Test.Assistant.TestRunBackupEnvOnly [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.Assistant.TestRunBackupEnvOnly
# #region Test.Assistant.TestRunBackupWithDashboard [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.Assistant.TestRunBackupWithDashboard
# #region Test.Assistant.TestRunBackupMissingEnv [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.Assistant.TestRunBackupMissingEnv
# ── _tool_migration.py ─────────────────────────────────────────────────────────
class TestToolMigration:
"""handle_execute_migration — execute migration."""
# #region Test.Assistant.TestExecuteMigrationMissingEnvs [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.Assistant.TestExecuteMigrationMissingEnvs
# #region Test.Assistant.TestExecuteMigrationSuccess [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.Assistant.TestExecuteMigrationSuccess
# ── _tool_task_status.py ──────────────────────────────────────────────────────
class TestToolTaskStatus:
"""handle_get_task_status — get task status."""
# #region Test.Assistant.TestGetTaskStatusById [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.Assistant.TestGetTaskStatusById
# #region Test.Assistant.TestGetTaskStatusLatest [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.Assistant.TestGetTaskStatusLatest
# #region Test.Assistant.TestGetTaskStatusNoHistory [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.Assistant.TestGetTaskStatusNoHistory
# #region Test.Assistant.TestGetTaskStatusNotFound [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.Assistant.TestGetTaskStatusNotFound
# ── _tool_llm_validation.py ───────────────────────────────────────────────────
class TestToolLlmValidation:
"""handle_run_llm_validation — create and run LLM validation."""
# #region Test.Assistant.TestRunLlmValidationSuccess [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.Assistant.TestRunLlmValidationSuccess
# #region Test.Assistant.TestRunLlmValidationMissingParams [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.Assistant.TestRunLlmValidationMissingParams
# #region Test.Assistant.TestRunLlmValidationCreateTaskError [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.Assistant.TestRunLlmValidationCreateTaskError
# #region Test.Assistant.TestRunLlmValidationTriggerRunError [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.Assistant.TestRunLlmValidationTriggerRunError
# ── _tool_search_dashboards.py ────────────────────────────────────────────────
class TestToolSearchDashboards:
"""handle_search_dashboards — search and list dashboards."""
# #region Test.Assistant.TestSearchDashboardsSuccess [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.Assistant.TestSearchDashboardsSuccess
# #region Test.Assistant.TestSearchDashboardsWithSearch [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.Assistant.TestSearchDashboardsWithSearch
# #region Test.Assistant.TestSearchDashboardsNoEnv [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.Assistant.TestSearchDashboardsNoEnv
# #region Test.Assistant.TestSearchDashboardsNoEnvsAtAll [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.Assistant.TestSearchDashboardsNoEnvsAtAll
# #region Test.Assistant.TestSearchDashboardsEnvNotFound [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.Assistant.TestSearchDashboardsEnvNotFound
# ── _tool_llm.py ──────────────────────────────────────────────────────────────
class TestToolLlm:
"""handle_list_llm_providers and handle_get_llm_status."""
# #region Test.Assistant.TestListLlmProviders [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.Assistant.TestListLlmProviders
# #region Test.Assistant.TestListLlmProvidersEmpty [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.Assistant.TestListLlmProvidersEmpty
# #region Test.Assistant.TestGetLlmStatus [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.Assistant.TestGetLlmStatus
# #region Test.Assistant.TestGetLlmStatusNoProviders [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.Assistant.TestGetLlmStatusNoProviders
# ── _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.TestAssistantToolDecorator [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.TestAssistantToolDecorator
# #region Test.Assistant.TestDispatchSupportedOp [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.Assistant.TestDispatchSupportedOp
# #region Test.Assistant.TestDispatchUnsupportedOp [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.Assistant.TestDispatchUnsupportedOp
# #region Test.Assistant.TestGetSafeOps [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
# #endregion Test.Assistant.TestGetSafeOps
# #region Test.Assistant.TestGetCatalog [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.Assistant.TestGetCatalog
# #region Test.Assistant.TestGetPermissionChecks [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.Assistant.TestGetPermissionChecks
# #region Test.Assistant.TestCheckAnyPermissionAllowsValid [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.Assistant.TestCheckAnyPermissionAllowsValid
# #region Test.Assistant.TestCheckAnyPermissionDeniesAll [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.Assistant.TestCheckAnyPermissionDeniesAll
# #region Test.Assistant.TestHasAnyPermission [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.Assistant.TestHasAnyPermission
# ── _llm_planner.py ───────────────────────────────────────────────────────────
class TestLLMPlanner:
"""_build_tool_catalog and _coerce_intent_entities."""
# #region Test.Assistant.TestBuildToolCatalogNoReview [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.Assistant.TestBuildToolCatalogNoReview
# #region Test.Assistant.TestCoerceIntentEntities [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.Assistant.TestCoerceIntentEntities
# #region Test.Assistant.TestCoerceIntentEntitiesNonDict [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.Assistant.TestCoerceIntentEntitiesNonDict
# ── _llm_planner_intent.py ───────────────────────────────────────────────────
class TestLLMPlannerIntent:
"""_plan_intent_with_llm and _authorize_intent."""
# #region Test.Assistant.TestAuthorizeIntentPasses [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.Assistant.TestAuthorizeIntentPasses
# #region Test.Assistant.TestAuthorizeIntentNoChecks [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.Assistant.TestAuthorizeIntentNoChecks
# ── _dispatch.py ──────────────────────────────────────────────────────────────
class TestAssistantDispatch:
"""_get_git_service and _clarification_text_for_intent."""
# #region Test.Assistant.TestGetGitService [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.Assistant.TestGetGitService
# #region Test.Assistant.TestClarificationTextForIntent [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.Assistant.TestClarificationTextForIntent
# #region Test.Assistant.TestDispatchIntentWrapper [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.Assistant.TestDispatchIntentWrapper
# ── _resolvers.py ─────────────────────────────────────────────────────────────
class TestAssistantResolvers:
"""_resolve_env_id, _is_production_env, _resolve_provider_id, etc."""
# #region Test.Assistant.TestExtractId [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.Assistant.TestExtractId
# #region Test.Assistant.TestResolveEnvId [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.Assistant.TestResolveEnvId
# #region Test.Assistant.TestIsProductionEnv [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.Assistant.TestIsProductionEnv
# #region Test.Assistant.TestResolveProviderIdByToken [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.Assistant.TestResolveProviderIdByToken
# #region Test.Assistant.TestResolveProviderIdNoProviders [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.Assistant.TestResolveProviderIdNoProviders
# #region Test.Assistant.TestGetDefaultEnvironmentId [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.Assistant.TestGetDefaultEnvironmentId
# #region Test.Assistant.TestGetEnvironmentNameById [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.Assistant.TestGetEnvironmentNameById
# #region Test.Assistant.TestResolveDashboardIdEntityInt [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.Assistant.TestResolveDashboardIdEntityInt
# #region Test.Assistant.TestResolveDashboardIdEntityStrDigit [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.Assistant.TestResolveDashboardIdEntityStrDigit
# #region Test.Assistant.TestResolveDashboardIdEntityRefFallback [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.Assistant.TestResolveDashboardIdEntityRefFallback
# #region Test.Assistant.TestResolveDashboardIdEntityNoMatch [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.Assistant.TestResolveDashboardIdEntityNoMatch
# #region Test.Assistant.TestResolveDashboardIdByRef [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.Assistant.TestResolveDashboardIdByRef
# #region Test.Assistant.TestResolveDashboardIdByRefNoEnv [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.Assistant.TestResolveDashboardIdByRefNoEnv
# #region Test.Assistant.TestResolveDashboardIdByRefNoRef [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.Assistant.TestResolveDashboardIdByRefNoRef
# #region Test.Assistant.TestExtractResultDeepLinksMigration [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.Assistant.TestExtractResultDeepLinksMigration
# #region Test.Assistant.TestExtractResultDeepLinksBackup [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.Assistant.TestExtractResultDeepLinksBackup
# #region Test.Assistant.TestExtractResultDeepLinksValidation [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.Assistant.TestExtractResultDeepLinksValidation
# #region Test.Assistant.TestExtractResultDeepLinksNoMatch [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.Assistant.TestExtractResultDeepLinksNoMatch
# #region Test.Assistant.TestBuildTaskObservabilitySummaryMigration [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.Assistant.TestBuildTaskObservabilitySummaryMigration
# #region Test.Assistant.TestBuildTaskObservabilitySummaryMigrationWithErrors [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.Assistant.TestBuildTaskObservabilitySummaryMigrationWithErrors
# #region Test.Assistant.TestBuildTaskObservabilitySummaryBackup [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.Assistant.TestBuildTaskObservabilitySummaryBackup
# #region Test.Assistant.TestBuildTaskObservabilitySummaryValidation [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.Assistant.TestBuildTaskObservabilitySummaryValidation
# #region Test.Assistant.TestBuildTaskObservabilitySummaryFallback [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.Assistant.TestBuildTaskObservabilitySummaryFallback
# ── _routes.py ────────────────────────────────────────────────────────────────
class TestAssistantRoutes:
"""send_message route — the main assistant entry point."""
# #region Test.Assistant.TestSendMessageClarification [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"):
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.Assistant.TestSendMessageClarification
# #region Test.Assistant.TestSendMessageSafeOperation [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"):
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.Assistant.TestSendMessageSafeOperation
# #region Test.Assistant.TestSendMessageGuardedNeedsConfirmation [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"):
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.Assistant.TestSendMessageGuardedNeedsConfirmation
# #region Test.Assistant.TestSendMessageHttpExceptionDenied [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"):
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.Assistant.TestSendMessageHttpExceptionDenied
# #region Test.Assistant.TestSendMessageHttpExceptionClarification [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"):
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.Assistant.TestSendMessageHttpExceptionClarification
# #region Test.Assistant.TestSendMessagePlannerFallback [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"):
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.Assistant.TestSendMessagePlannerFallback
# #endregion Test.Assistant.Tools