# #region Test.Assistant.LlmPlannerIntent [C:2] [TYPE Module] [SEMANTICS test,assistant,llm,planner,intent,authorization] # @BRIEF Tests for _llm_planner_intent.py — LLM-based intent planning and authorization. # @RELATION BINDS_TO -> [AssistantLlmPlannerIntent] # @TEST_EDGE: plan_intent_empty_tools -> Returns None when tools list empty # @TEST_EDGE: plan_intent_no_provider -> Returns None when no LLM provider # @TEST_EDGE: plan_intent_llm_failure -> Returns None on LLM error # @TEST_EDGE: plan_intent_clarify -> Returns clarify intent for ambiguous input # @TEST_EDGE: plan_intent_valid_operation -> Valid operation mapped correctly # @TEST_EDGE: plan_intent_production_env -> Production env sets dangerous risk # @TEST_EDGE: authorize_intent -> Permission checks pass or raise 403 from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import HTTPException class TestPlanIntentWithLlm: """_plan_intent_with_llm — LLM-based intent planning.""" @pytest.fixture def db(self): return MagicMock() @pytest.fixture def config_manager(self): cm = MagicMock() cm.get_config.return_value.settings.llm = { "assistant_planner_provider": "openai", "assistant_planner_model": "gpt-4", } cm.get_environments.return_value = [] return cm def test_empty_tools_returns_none(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm result = __import__("asyncio").run(_plan_intent_with_llm("test", [], db, config_manager)) assert result is None def test_no_provider_returns_none(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm tools = [{"operation": "run_backup", "domain": "backup"}] with patch("src.api.routes.assistant._llm_planner_intent.LLMProviderService") as mock_svc: mock_svc.return_value.get_all_providers.return_value = [] result = __import__("asyncio").run( _plan_intent_with_llm("test", tools, db, config_manager) ) assert result is None def test_no_api_key_returns_none(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm tools = [{"operation": "run_backup", "domain": "backup"}] provider_mock = MagicMock() provider_mock.id = "p-1" provider_mock.provider_type = "openai" provider_mock.base_url = None provider_mock.default_model = "gpt-4" with patch("src.api.routes.assistant._llm_planner_intent.LLMProviderService") as mock_svc_cls: mock_svc = MagicMock() mock_svc_cls.return_value = mock_svc mock_svc.get_all_providers.return_value = [provider_mock] mock_svc.get_decrypted_api_key.return_value = None with patch("src.api.routes.assistant._llm_planner_intent._resolve_provider_id") as mock_resolve: mock_resolve.return_value = "p-1" result = __import__("asyncio").run( _plan_intent_with_llm("test", tools, db, config_manager) ) assert result is None def test_llm_failure_returns_none(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm tools = [{"operation": "run_backup", "domain": "backup"}] provider_mock = MagicMock() provider_mock.id = "p-1" provider_mock.provider_type = "openai" provider_mock.base_url = None provider_mock.default_model = "gpt-4" with patch("src.api.routes.assistant._llm_planner_intent.LLMProviderService") as mock_svc_cls, \ patch("src.api.routes.assistant._llm_planner_intent.LLMClient") as mock_client_cls: mock_svc = MagicMock() mock_svc_cls.return_value = mock_svc mock_svc.get_all_providers.return_value = [provider_mock] mock_svc.get_decrypted_api_key.return_value = "sk-test" mock_client = AsyncMock() mock_client_cls.return_value = mock_client mock_client.get_json_completion.side_effect = Exception("LLM unavailable") with patch("src.api.routes.assistant._llm_planner_intent._resolve_provider_id") as mock_resolve: mock_resolve.return_value = "p-1" result = __import__("asyncio").run( _plan_intent_with_llm("test", tools, db, config_manager) ) assert result is None def test_clarify_response(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm tools = [{"operation": "run_backup", "domain": "backup"}] provider_mock = MagicMock() provider_mock.id = "p-1" provider_mock.provider_type = "openai" provider_mock.base_url = None provider_mock.default_model = "gpt-4" with patch("src.api.routes.assistant._llm_planner_intent.LLMProviderService") as mock_svc_cls, \ patch("src.api.routes.assistant._llm_planner_intent.LLMClient") as mock_client_cls: mock_svc = MagicMock() mock_svc_cls.return_value = mock_svc mock_svc.get_all_providers.return_value = [provider_mock] mock_svc.get_decrypted_api_key.return_value = "sk-test" mock_client = AsyncMock() mock_client_cls.return_value = mock_client mock_client.get_json_completion.return_value = { "operation": "clarify", "domain": "unknown", "confidence": 0.3, "clarification": "Что именно вы хотите сделать?", "entities": {}, } with patch("src.api.routes.assistant._llm_planner_intent._resolve_provider_id") as mock_resolve: mock_resolve.return_value = "p-1" result = __import__("asyncio").run( _plan_intent_with_llm("test", tools, db, config_manager) ) assert result is not None assert result["operation"] == "clarify" assert "Что именно" in result.get("clarification", "") def test_invalid_operation_returns_none(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm tools = [{"operation": "run_backup", "domain": "backup"}] provider_mock = MagicMock() provider_mock.id = "p-1" provider_mock.provider_type = "openai" provider_mock.base_url = None provider_mock.default_model = "gpt-4" with patch("src.api.routes.assistant._llm_planner_intent.LLMProviderService") as mock_svc_cls, \ patch("src.api.routes.assistant._llm_planner_intent.LLMClient") as mock_client_cls: mock_svc = MagicMock() mock_svc_cls.return_value = mock_svc mock_svc.get_all_providers.return_value = [provider_mock] mock_svc.get_decrypted_api_key.return_value = "sk-test" mock_client = AsyncMock() mock_client_cls.return_value = mock_client mock_client.get_json_completion.return_value = { "operation": "nonexistent_op", "domain": "backup", "confidence": 0.9, "entities": {}, } with patch("src.api.routes.assistant._llm_planner_intent._resolve_provider_id") as mock_resolve: mock_resolve.return_value = "p-1" result = __import__("asyncio").run( _plan_intent_with_llm("test", tools, db, config_manager) ) assert result is None def test_non_dict_response_returns_none(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm tools = [{"operation": "run_backup", "domain": "backup"}] provider_mock = MagicMock() provider_mock.id = "p-1" provider_mock.provider_type = "openai" provider_mock.base_url = None provider_mock.default_model = "gpt-4" with patch("src.api.routes.assistant._llm_planner_intent.LLMProviderService") as mock_svc_cls, \ patch("src.api.routes.assistant._llm_planner_intent.LLMClient") as mock_client_cls: mock_svc = MagicMock() mock_svc_cls.return_value = mock_svc mock_svc.get_all_providers.return_value = [provider_mock] mock_svc.get_decrypted_api_key.return_value = "sk-test" mock_client = AsyncMock() mock_client_cls.return_value = mock_client mock_client.get_json_completion.return_value = "not a dict" with patch("src.api.routes.assistant._llm_planner_intent._resolve_provider_id") as mock_resolve: mock_resolve.return_value = "p-1" result = __import__("asyncio").run( _plan_intent_with_llm("test", tools, db, config_manager) ) assert result is None def test_valid_operation_with_defaults(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm tools = [{ "operation": "run_backup", "domain": "backup", "risk_level": "guarded", "requires_confirmation": False, "defaults": {"environment": "dev"}, }] provider_mock = MagicMock() provider_mock.id = "p-1" provider_mock.provider_type = "openai" provider_mock.base_url = None provider_mock.default_model = "gpt-4" with patch("src.api.routes.assistant._llm_planner_intent.LLMProviderService") as mock_svc_cls, \ patch("src.api.routes.assistant._llm_planner_intent.LLMClient") as mock_client_cls: mock_svc = MagicMock() mock_svc_cls.return_value = mock_svc mock_svc.get_all_providers.return_value = [provider_mock] mock_svc.get_decrypted_api_key.return_value = "sk-test" mock_client = AsyncMock() mock_client_cls.return_value = mock_client mock_client.get_json_completion.return_value = { "operation": "run_backup", "domain": "backup", "confidence": 0.9, "entities": {}, "risk_level": "guarded", "requires_confirmation": False, } with patch("src.api.routes.assistant._llm_planner_intent._resolve_provider_id") as mock_resolve, \ patch("src.api.routes.assistant._llm_planner_intent._coerce_intent_entities") as mock_coerce, \ patch("src.api.routes.assistant._llm_planner_intent.normalize_llm_settings") as mock_norm: mock_resolve.return_value = "p-1" mock_coerce.return_value = { "domain": "backup", "operation": "run_backup", "entities": {}, "confidence": 0.9, "risk_level": "guarded", "requires_confirmation": False, } mock_norm.return_value = { "assistant_planner_provider": "openai", "assistant_planner_model": "gpt-4", } result = __import__("asyncio").run( _plan_intent_with_llm("test", tools, db, config_manager) ) assert result is not None assert result["operation"] == "run_backup" # Default should be applied assert result["entities"].get("environment") == "dev" def test_production_deploy_sets_dangerous(self, db, config_manager): from src.api.routes.assistant._llm_planner_intent import _plan_intent_with_llm # Environments must be JSON-serializable dicts config_manager.get_environments.return_value = [ type('Env', (), {'id': 'prod', 'name': 'PROD'})(), ] tools = [{ "operation": "deploy_dashboard", "domain": "git", "risk_level": "guarded", "requires_confirmation": False, }] provider_mock = MagicMock() provider_mock.id = "p-1" provider_mock.provider_type = "openai" provider_mock.base_url = None provider_mock.default_model = "gpt-4" with patch("src.api.routes.assistant._llm_planner_intent.LLMProviderService") as mock_svc_cls, \ patch("src.api.routes.assistant._llm_planner_intent.LLMClient") as mock_client_cls: mock_svc = MagicMock() mock_svc_cls.return_value = mock_svc mock_svc.get_all_providers.return_value = [provider_mock] mock_svc.get_decrypted_api_key.return_value = "sk-test" mock_client = AsyncMock() mock_client_cls.return_value = mock_client mock_client.get_json_completion.return_value = { "operation": "deploy_dashboard", "domain": "git", "confidence": 0.9, "entities": {"environment": "prod"}, "risk_level": "guarded", "requires_confirmation": False, } with patch("src.api.routes.assistant._llm_planner_intent._resolve_provider_id") as mock_resolve, \ patch("src.api.routes.assistant._llm_planner_intent._coerce_intent_entities") as mock_coerce, \ patch("src.api.routes.assistant._llm_planner_intent._is_production_env") as mock_is_prod, \ patch("src.api.routes.assistant._llm_planner_intent.normalize_llm_settings") as mock_norm: mock_resolve.return_value = "p-1" mock_coerce.return_value = { "domain": "git", "operation": "deploy_dashboard", "entities": {"environment": "prod"}, "confidence": 0.9, "risk_level": "guarded", "requires_confirmation": False, } mock_is_prod.return_value = True mock_norm.return_value = { "assistant_planner_provider": "openai", "assistant_planner_model": "gpt-4", } result = __import__("asyncio").run( _plan_intent_with_llm("test", tools, db, config_manager) ) assert result is not None assert result["risk_level"] == "dangerous" assert result["requires_confirmation"] is True class TestAuthorizeIntent: """_authorize_intent — permission checks.""" def test_authorized(self): from src.api.routes.assistant._llm_planner_intent import _authorize_intent from src.schemas.auth import User from datetime import UTC, datetime user = User(id="user-1", username="test", email="test@x.com", is_active=True, auth_source="internal", created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[]) intent = {"operation": "run_backup"} with patch("src.api.routes.assistant._llm_planner_intent.get_permission_checks") as mock_checks, \ patch("src.api.routes.assistant._llm_planner_intent._check_any_permission") as mock_check: mock_checks.return_value = {"run_backup": [("backup", "WRITE")]} _authorize_intent(intent, user) mock_check.assert_called_once_with(user, [("backup", "WRITE")]) def test_no_checks_skips(self): from src.api.routes.assistant._llm_planner_intent import _authorize_intent from src.schemas.auth import User from datetime import UTC, datetime user = User(id="user-1", username="test", email="test@x.com", is_active=True, auth_source="internal", created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[]) intent = {"operation": "show_capabilities"} with patch("src.api.routes.assistant._llm_planner_intent.get_permission_checks") as mock_checks: mock_checks.return_value = {} # no checks for this op # Should not raise _authorize_intent(intent, user) # #endregion Test.Assistant.LlmPlannerIntent