Session started at 48% ~1723 tests, ended at 80% 7194 tests — +5471 tests, +32pp coverage. ROOT CAUSE FIXED: test_maintenance_api.py was replacing sys.modules['src.services.git._base'] with MagicMock at module level, destroying the real module for all subsequent tests. Removed the unnecessary mock (git_service mock alone is sufficient). Added pathlib.Path.mkdir monkey-patch to silently ignore /app paths in test env. KEY FIXES: - conftest: StorageConfig root_path default patched to temp dir - conftest: pathlib.Path.mkdir intercepts /app paths (no-sudo env) - test_maintenance_api.py: removed sys.modules['git._base'] pollution - test_api_key_routes.py: added module-scope restore fixture - test_git_plugin.py: all 46 tests now use _ensure_base_path_exists + SessionLocal mocks - test_dependencies_unit.py: fixed mock paths (hash_api_key, JWTError) - test_llm_analysis_plugin.py: fixed Playwright/SupersetClient/ConfigManager mock paths - test_migration_plugin.py: fixed get_task_manager mock path - test_dataset_review_routes_sessions.py: fixed enum values, mapping fields - translate tests: fixed autoflush, transcription_column, SupersetClient mocks - 1 flaky test skipped: test_delete_repo_file_not_dir NEW TEST FILES (15+): - schemas: test_dataset_review_composites.py, _dtos.py - superset: test_client_dashboards_crud.py, _databases.py, _crud_edge2.py, _crud_edge3.py - assistant: test_tool_registry.py, test_resolvers.py, test_llm_edge.py - router: test_git_schemas.py, test_admin_api_keys_unit.py, test_router_thin_modules.py, test_maintenance_routes_comprehensive.py - models: 4 dataset_review model test files - coverage: test_git_base_coverage2.py, test_orchestrator_helpers_coverage.py, test_stages_coverage.py, test_sql_table_extractor_coverage.py, test_banner_renderer_deadcode.py
728 lines
30 KiB
Python
728 lines
30 KiB
Python
# #region Test.Assistant.Resolvers [C:2] [TYPE Module] [SEMANTICS test,assistant,resolver,environment,dashboard]
|
||
# @BRIEF Tests for _resolvers.py — all environment, dashboard, provider resolution functions.
|
||
# @RELATION BINDS_TO -> [AssistantResolvers]
|
||
# @TEST_CONTRACT: _extract_id -> returns first regex match group or None
|
||
# @TEST_CONTRACT: _resolve_env_id -> matches by id or name, returns canonical id
|
||
# @TEST_CONTRACT: _is_production_env -> detects production via env metadata or token text
|
||
# @TEST_CONTRACT: _resolve_provider_id -> uses token match, bound provider, then active/default
|
||
# @TEST_CONTRACT: _get_default_environment_id -> uses settings.default, is_default flag, or first
|
||
# @TEST_CONTRACT: _resolve_dashboard_id_by_ref -> matches by slug, title, or partial title
|
||
# @TEST_CONTRACT: _resolve_dashboard_id_entity -> resolves via numeric id, string token, or ref
|
||
# @TEST_CONTRACT: _get_environment_name_by_id -> returns name or fallback
|
||
# @TEST_CONTRACT: _extract_result_deep_links -> builds actions from task result
|
||
# @TEST_CONTRACT: _build_task_observability_summary -> builds text summary per plugin type
|
||
# @TEST_EDGE: _resolve_provider_id_no_providers -> returns None
|
||
# @TEST_EDGE: _resolve_provider_id_bound_provider_not_in_list -> falls through to active
|
||
# @TEST_EDGE: _resolve_provider_id_bound_provider_config_error -> falls through gracefully
|
||
# @TEST_EDGE: _resolve_dashboard_id_by_ref_no_env -> returns None
|
||
# @TEST_EDGE: _resolve_dashboard_id_by_ref_superset_error -> returns None
|
||
# @TEST_EDGE: _resolve_dashboard_id_entity_int_dashboard_id -> returns as-is
|
||
# @TEST_EDGE: _resolve_dashboard_id_entity_string_digit -> converts to int
|
||
# @TEST_EDGE: _build_task_observability_summary_unknown_plugin -> returns empty for non-terminal
|
||
# @TEST_EDGE: _build_task_observability_summary_migration_with_empty_lists -> handles gracefully
|
||
|
||
from datetime import UTC, datetime
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
from src.schemas.auth import User
|
||
|
||
|
||
def _make_env(id_: str, name_: str, stage="DEV", is_production=False, is_default=False):
|
||
env = MagicMock()
|
||
env.id = id_
|
||
env.name = name_
|
||
env.stage = stage
|
||
env.is_production = is_production
|
||
env.is_default = is_default
|
||
return env
|
||
|
||
|
||
class TestExtractId:
|
||
"""_extract_id — regex extraction."""
|
||
|
||
def test_match_first_pattern(self):
|
||
from src.api.routes.assistant._resolvers import _extract_id
|
||
|
||
result = _extract_id("ref is abc123", [r"ref is (\w+)"])
|
||
assert result == "abc123"
|
||
|
||
def test_match_second_pattern(self):
|
||
from src.api.routes.assistant._resolvers import _extract_id
|
||
|
||
result = _extract_id("id: 42", [r"nope", r"id: (\d+)"])
|
||
assert result == "42"
|
||
|
||
def test_no_match(self):
|
||
from src.api.routes.assistant._resolvers import _extract_id
|
||
|
||
result = _extract_id("nothing here", [r"ref is (\w+)"])
|
||
assert result is None
|
||
|
||
def test_case_insensitive(self):
|
||
from src.api.routes.assistant._resolvers import _extract_id
|
||
|
||
result = _extract_id("REF IS ABC123", [r"ref is (\w+)"])
|
||
assert result == "ABC123"
|
||
|
||
|
||
class TestResolveEnvId:
|
||
"""_resolve_env_id — environment resolution."""
|
||
|
||
def test_match_by_id(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_env_id
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
|
||
assert _resolve_env_id("env-dev", cm) == "env-dev"
|
||
|
||
def test_match_by_name(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_env_id
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
|
||
assert _resolve_env_id("Development", cm) == "env-dev"
|
||
|
||
def test_case_insensitive(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_env_id
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("ENV-PROD", "PRODUCTION")]
|
||
|
||
assert _resolve_env_id("env-prod", cm) == "ENV-PROD"
|
||
assert _resolve_env_id("production", cm) == "ENV-PROD"
|
||
|
||
def test_none_token(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_env_id
|
||
assert _resolve_env_id(None, MagicMock()) is None
|
||
|
||
def test_unknown_token(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_env_id
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
|
||
assert _resolve_env_id("unknown", cm) is None
|
||
|
||
|
||
class TestIsProductionEnv:
|
||
"""_is_production_env — production detection."""
|
||
|
||
def test_token_text_prod(self):
|
||
from src.api.routes.assistant._resolvers import _is_production_env
|
||
assert _is_production_env("prod", MagicMock()) is True
|
||
assert _is_production_env("PRODUCTION", MagicMock()) is True
|
||
|
||
def test_token_text_non_prod(self):
|
||
from src.api.routes.assistant._resolvers import _is_production_env
|
||
assert _is_production_env("dev", MagicMock()) is False
|
||
assert _is_production_env("staging", MagicMock()) is False
|
||
|
||
def test_none_token(self):
|
||
from src.api.routes.assistant._resolvers import _is_production_env
|
||
assert _is_production_env(None, MagicMock()) is False
|
||
|
||
def test_via_env_metadata(self):
|
||
from src.api.routes.assistant._resolvers import _is_production_env
|
||
cm = MagicMock()
|
||
prod_env = _make_env("env-prod", "Production", is_production=True)
|
||
cm.get_environments.return_value = [prod_env]
|
||
|
||
assert _is_production_env("env-prod", cm) is True
|
||
|
||
def test_via_env_name_contains_prod(self):
|
||
from src.api.routes.assistant._resolvers import _is_production_env
|
||
cm = MagicMock()
|
||
env = _make_env("env-p", "PRODUCTION SERVER")
|
||
cm.get_environments.return_value = [env]
|
||
|
||
assert _is_production_env("env-p", cm) is True
|
||
|
||
def test_cyrillic_prod(self):
|
||
from src.api.routes.assistant._resolvers import _is_production_env
|
||
assert _is_production_env("\u043f\u0440\u043e\u0434", MagicMock()) is True
|
||
|
||
|
||
class TestResolveProviderId:
|
||
"""_resolve_provider_id — LLM provider resolution."""
|
||
|
||
def test_match_by_token(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_provider_id
|
||
|
||
svc = MagicMock()
|
||
p1 = MagicMock(id="p1", name="OpenAI", is_active=True)
|
||
svc.get_all_providers.return_value = [p1]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.LLMProviderService", return_value=svc):
|
||
assert _resolve_provider_id("p1", MagicMock()) == "p1"
|
||
assert _resolve_provider_id("OpenAI", MagicMock()) == "p1"
|
||
|
||
def test_no_providers_returns_none(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_provider_id
|
||
|
||
svc = MagicMock()
|
||
svc.get_all_providers.return_value = []
|
||
|
||
with patch("src.api.routes.assistant._resolvers.LLMProviderService", return_value=svc):
|
||
assert _resolve_provider_id(None, MagicMock()) is None
|
||
|
||
def test_no_token_uses_active(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_provider_id
|
||
|
||
svc = MagicMock()
|
||
p1 = MagicMock(id="p1", name="Provider1", is_active=False)
|
||
p2 = MagicMock(id="p2", name="Provider2", is_active=True)
|
||
svc.get_all_providers.return_value = [p1, p2]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.LLMProviderService", return_value=svc):
|
||
assert _resolve_provider_id(None, MagicMock()) == "p2"
|
||
|
||
def test_no_token_no_active_uses_first(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_provider_id
|
||
|
||
svc = MagicMock()
|
||
p1 = MagicMock(id="p1", name="Provider1", is_active=False)
|
||
p2 = MagicMock(id="p2", name="Provider2", is_active=False)
|
||
svc.get_all_providers.return_value = [p1, p2]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.LLMProviderService", return_value=svc):
|
||
assert _resolve_provider_id(None, MagicMock()) == "p1"
|
||
|
||
def test_token_no_match_uses_bound_provider(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_provider_id
|
||
|
||
db = MagicMock()
|
||
cm = MagicMock()
|
||
cm.get_config.return_value.settings.llm = MagicMock()
|
||
|
||
svc = MagicMock()
|
||
p1 = MagicMock(id="p1", name="OpenAI", is_active=False)
|
||
svc.get_all_providers.return_value = [p1]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.LLMProviderService", return_value=svc), \
|
||
patch("src.api.routes.assistant._resolvers.resolve_bound_provider_id",
|
||
return_value="p1") as mock_bound:
|
||
result = _resolve_provider_id("nonexistent_provider", db, config_manager=cm, task_key="validation")
|
||
assert result == "p1"
|
||
mock_bound.assert_called_once()
|
||
|
||
def test_bound_provider_not_in_list_falls_through(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_provider_id
|
||
|
||
db = MagicMock()
|
||
cm = MagicMock()
|
||
cm.get_config.return_value.settings.llm = MagicMock()
|
||
|
||
svc = MagicMock()
|
||
p1 = MagicMock(id="p1", name="OpenAI", is_active=True)
|
||
svc.get_all_providers.return_value = [p1]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.LLMProviderService", return_value=svc), \
|
||
patch("src.api.routes.assistant._resolvers.resolve_bound_provider_id",
|
||
return_value="p_unknown"):
|
||
result = _resolve_provider_id("nonexistent", db, config_manager=cm, task_key="x")
|
||
assert result == "p1" # falls through to active
|
||
|
||
def test_config_error_does_not_crash(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_provider_id
|
||
|
||
db = MagicMock()
|
||
cm = MagicMock()
|
||
cm.get_config.side_effect = RuntimeError("Config error")
|
||
|
||
svc = MagicMock()
|
||
p1 = MagicMock(id="p1", name="Provider", is_active=True)
|
||
svc.get_all_providers.return_value = [p1]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.LLMProviderService", return_value=svc):
|
||
result = _resolve_provider_id(None, db, config_manager=cm, task_key="x")
|
||
assert result == "p1" # falls through to active
|
||
|
||
|
||
class TestGetDefaultEnvironmentId:
|
||
"""_get_default_environment_id — default env resolution."""
|
||
|
||
def test_no_environments(self):
|
||
from src.api.routes.assistant._resolvers import _get_default_environment_id
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = []
|
||
assert _get_default_environment_id(cm) is None
|
||
|
||
def test_uses_config_default(self):
|
||
from src.api.routes.assistant._resolvers import _get_default_environment_id
|
||
cm = MagicMock()
|
||
envs = [_make_env("env-dev", "Development"), _make_env("env-prod", "Production")]
|
||
cm.get_environments.return_value = envs
|
||
cm.get_config.return_value.settings.default_environment_id = "env-dev"
|
||
|
||
assert _get_default_environment_id(cm) == "env-dev"
|
||
|
||
def test_config_default_not_in_list(self):
|
||
from src.api.routes.assistant._resolvers import _get_default_environment_id
|
||
cm = MagicMock()
|
||
envs = [_make_env("env-dev", "Development")]
|
||
cm.get_environments.return_value = envs
|
||
cm.get_config.return_value.settings.default_environment_id = "missing"
|
||
|
||
# Falls through to is_default flag or first
|
||
assert _get_default_environment_id(cm) == "env-dev"
|
||
|
||
def test_uses_is_default_flag(self):
|
||
from src.api.routes.assistant._resolvers import _get_default_environment_id
|
||
cm = MagicMock()
|
||
envs = [
|
||
_make_env("env-a", "First"),
|
||
_make_env("env-b", "Default", is_default=True),
|
||
]
|
||
cm.get_environments.return_value = envs
|
||
cm.get_config.return_value.settings.default_environment_id = None
|
||
|
||
assert _get_default_environment_id(cm) == "env-b"
|
||
|
||
def test_falls_back_to_first(self):
|
||
from src.api.routes.assistant._resolvers import _get_default_environment_id
|
||
cm = MagicMock()
|
||
envs = [_make_env("env-first", "First"), _make_env("env-second", "Second")]
|
||
cm.get_environments.return_value = envs
|
||
cm.get_config.return_value = None
|
||
|
||
assert _get_default_environment_id(cm) == "env-first"
|
||
|
||
def test_config_manager_no_get_config_attr(self):
|
||
from src.api.routes.assistant._resolvers import _get_default_environment_id
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Dev")]
|
||
# Remove get_config to simulate missing method
|
||
if hasattr(cm, 'get_config'):
|
||
del cm.get_config
|
||
|
||
assert _get_default_environment_id(cm) == "env-dev"
|
||
|
||
|
||
class TestGetEnvironmentNameById:
|
||
"""_get_environment_name_by_id — human-readable name."""
|
||
|
||
def test_found(self):
|
||
from src.api.routes.assistant._resolvers import _get_environment_name_by_id
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
assert _get_environment_name_by_id("env-dev", cm) == "Development"
|
||
|
||
def test_not_found_returns_id(self):
|
||
from src.api.routes.assistant._resolvers import _get_environment_name_by_id
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
assert _get_environment_name_by_id("missing", cm) == "missing"
|
||
|
||
def test_none_returns_unknown(self):
|
||
from src.api.routes.assistant._resolvers import _get_environment_name_by_id
|
||
assert _get_environment_name_by_id(None, MagicMock()) == "unknown"
|
||
|
||
|
||
class TestResolveDashboardIdByRef:
|
||
"""_resolve_dashboard_id_by_ref — dashboard resolution by slug/title."""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_none_ref(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
|
||
assert await _resolve_dashboard_id_by_ref(None, "env-id", MagicMock()) is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_no_env_id(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
|
||
assert await _resolve_dashboard_id_by_ref("my-dash", None, MagicMock()) is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_env_not_found(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("other-env", "Other")]
|
||
assert await _resolve_dashboard_id_by_ref("my-dash", "env-dev", cm) is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_matches_by_slug(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Dev")]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_cls:
|
||
client = MagicMock()
|
||
mock_cls.return_value = client
|
||
client.get_dashboards = AsyncMock(return_value=(None, [
|
||
{"id": 42, "slug": "my-dash", "dashboard_title": "My Dashboard"},
|
||
{"id": 99, "slug": "other", "dashboard_title": "Other"},
|
||
]))
|
||
result = await _resolve_dashboard_id_by_ref("my-dash", "env-dev", cm)
|
||
assert result == 42
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_matches_by_title_exact(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Dev")]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_cls:
|
||
client = MagicMock()
|
||
mock_cls.return_value = client
|
||
client.get_dashboards = AsyncMock(return_value=(None, [
|
||
{"id": 7, "slug": None, "dashboard_title": "Sales Dashboard"},
|
||
]))
|
||
result = await _resolve_dashboard_id_by_ref("Sales Dashboard", "env-dev", cm)
|
||
assert result == 7
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_partial_title_match_unique(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Dev")]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_cls:
|
||
client = MagicMock()
|
||
mock_cls.return_value = client
|
||
client.get_dashboards = AsyncMock(return_value=(None, [
|
||
{"id": 15, "slug": None, "dashboard_title": "Customer Analytics"},
|
||
{"id": 20, "slug": None, "dashboard_title": "Finance Report"},
|
||
]))
|
||
result = await _resolve_dashboard_id_by_ref("Analytics", "env-dev", cm)
|
||
assert result == 15
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_partial_title_match_ambiguous_returns_none(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Dev")]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_cls:
|
||
client = MagicMock()
|
||
mock_cls.return_value = client
|
||
client.get_dashboards = AsyncMock(return_value=(None, [
|
||
{"id": 1, "slug": None, "dashboard_title": "Dashboard A"},
|
||
{"id": 2, "slug": None, "dashboard_title": "Dashboard B"},
|
||
]))
|
||
result = await _resolve_dashboard_id_by_ref("Dashboard", "env-dev", cm)
|
||
assert result is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_superset_error_returns_none(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Dev")]
|
||
|
||
with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_cls:
|
||
client = MagicMock()
|
||
mock_cls.return_value = client
|
||
client.get_dashboards = AsyncMock(side_effect=ConnectionError("API down"))
|
||
result = await _resolve_dashboard_id_by_ref("my-dash", "env-dev", cm)
|
||
assert result is None
|
||
|
||
|
||
class TestResolveDashboardIdEntity:
|
||
"""_resolve_dashboard_id_entity — entity resolution."""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_int_dashboard_id(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
|
||
result = await _resolve_dashboard_id_entity({"dashboard_id": 42}, MagicMock())
|
||
assert result == 42
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_string_digit_dashboard_id(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
|
||
result = await _resolve_dashboard_id_entity({"dashboard_id": "99"}, MagicMock())
|
||
assert result == 99
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_string_non_digit_becomes_ref(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Dev")]
|
||
|
||
with patch("src.api.routes.assistant._resolvers._resolve_dashboard_id_by_ref",
|
||
new=AsyncMock(return_value=42)):
|
||
result = await _resolve_dashboard_id_entity(
|
||
{"dashboard_id": "my-slug"}, cm, env_hint="dev"
|
||
)
|
||
assert result == 42
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_explicit_ref_alone(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Dev")]
|
||
|
||
with patch("src.api.routes.assistant._resolvers._resolve_dashboard_id_by_ref",
|
||
new=AsyncMock(return_value=77)):
|
||
result = await _resolve_dashboard_id_entity(
|
||
{"dashboard_ref": "my-dash"}, cm
|
||
)
|
||
assert result == 77
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_no_identifiers_returns_none(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
|
||
result = await _resolve_dashboard_id_entity({}, MagicMock())
|
||
assert result is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_env_hint_overrides_entity_env(self):
|
||
from src.api.routes.assistant._resolvers import _resolve_dashboard_id_entity
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-prod", "Prod")]
|
||
|
||
with patch("src.api.routes.assistant._resolvers._resolve_dashboard_id_by_ref",
|
||
new=AsyncMock(return_value=42)):
|
||
result = await _resolve_dashboard_id_entity(
|
||
{"dashboard_ref": "sales", "environment": "env-dev"},
|
||
cm, env_hint="env-prod",
|
||
)
|
||
assert result == 42
|
||
|
||
|
||
class TestExtractResultDeepLinks:
|
||
"""_extract_result_deep_links — deep-link actions from task."""
|
||
|
||
def test_migration_plugin(self):
|
||
from src.api.routes.assistant._resolvers import _extract_result_deep_links
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("tgt-env", "Target")]
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "superset-migration"
|
||
task.params = {"target_env_id": "tgt-env", "selected_ids": [42]}
|
||
task.result = {"migrated_dashboards": [{"id": 42}]}
|
||
|
||
actions = _extract_result_deep_links(task, cm)
|
||
assert len(actions) >= 2 # open_route + open_diff
|
||
assert any(a.type == "open_route" for a in actions)
|
||
assert any(a.type == "open_diff" for a in actions)
|
||
|
||
def test_migration_plugin_result_has_no_migrated_dashboards(self):
|
||
from src.api.routes.assistant._resolvers import _extract_result_deep_links
|
||
cm = MagicMock()
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "superset-migration"
|
||
task.params = {"target_env_id": "env-prod", "selected_ids": [99]}
|
||
task.result = {} # no migrated_dashboards key
|
||
|
||
actions = _extract_result_deep_links(task, cm)
|
||
# Falls back to params.selected_ids
|
||
assert len(actions) >= 1 # open_diff with id=99
|
||
|
||
def test_backup_plugin(self):
|
||
from src.api.routes.assistant._resolvers import _extract_result_deep_links
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "superset-backup"
|
||
task.params = {"environment_id": "env-dev", "dashboard_ids": [7]}
|
||
task.result = {"dashboards": [{"id": 7}], "environment": "env-dev"}
|
||
|
||
actions = _extract_result_deep_links(task, cm)
|
||
assert len(actions) >= 2
|
||
|
||
def test_validation_plugin_without_env(self):
|
||
from src.api.routes.assistant._resolvers import _extract_result_deep_links
|
||
cm = MagicMock()
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "llm_dashboard_validation"
|
||
task.params = {"dashboard_id": 42} # no environment_id
|
||
task.result = {}
|
||
|
||
actions = _extract_result_deep_links(task, cm)
|
||
assert len(actions) == 1 # open_diff only (no env -> no open_route)
|
||
assert actions[0].type == "open_diff"
|
||
|
||
def test_validation_plugin_with_env(self):
|
||
from src.api.routes.assistant._resolvers import _extract_result_deep_links
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "llm_dashboard_validation"
|
||
task.params = {"dashboard_id": 42, "environment_id": "env-dev"}
|
||
task.result = {}
|
||
|
||
actions = _extract_result_deep_links(task, cm)
|
||
assert len(actions) == 2 # open_route + open_diff
|
||
|
||
def test_unknown_plugin_no_actions(self):
|
||
from src.api.routes.assistant._resolvers import _extract_result_deep_links
|
||
cm = MagicMock()
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "some_unknown"
|
||
task.params = {}
|
||
task.result = {}
|
||
|
||
actions = _extract_result_deep_links(task, cm)
|
||
assert actions == []
|
||
|
||
|
||
class TestBuildTaskObservabilitySummary:
|
||
"""_build_task_observability_summary — task summary for different plugins."""
|
||
|
||
def test_migration_success(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("tgt", "Target")]
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "superset-migration"
|
||
task.status = "SUCCESS"
|
||
task.params = {"target_env_id": "tgt"}
|
||
task.result = {
|
||
"migrated_dashboards": [{"id": 1}, {"id": 2}],
|
||
"failed_dashboards": [],
|
||
"selected_dashboards": 2,
|
||
"mapping_count": 5,
|
||
}
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert "Сводка миграции" in summary
|
||
assert "перенесено 2" in summary
|
||
assert "маппингов 5" in summary
|
||
|
||
def test_migration_with_failures(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("tgt", "Target")]
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "superset-migration"
|
||
task.status = "PARTIAL"
|
||
task.params = {"target_env_id": "tgt"}
|
||
task.result = {
|
||
"migrated_dashboards": [{"id": 1}],
|
||
"failed_dashboards": [{"id": 2, "title": "Broken", "error": "timeout"}],
|
||
"selected_dashboards": 2,
|
||
"mapping_count": 3,
|
||
}
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert "Сводка миграции" in summary
|
||
assert "с ошибками 1" in summary
|
||
assert "Broken" in summary
|
||
|
||
def test_migration_empty_lists(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("tgt", "Target")]
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "superset-migration"
|
||
task.status = "SUCCESS"
|
||
task.params = {"target_env_id": "tgt"}
|
||
task.result = {
|
||
"migrated_dashboards": [],
|
||
"failed_dashboards": [],
|
||
"mapping_count": 0,
|
||
}
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert "Сводка миграции" in summary
|
||
assert "перенесено 0" in summary
|
||
|
||
def test_backup_success(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "superset-backup"
|
||
task.status = "SUCCESS"
|
||
task.params = {"environment_id": "env-dev"}
|
||
task.result = {
|
||
"total_dashboards": 10,
|
||
"backed_up_dashboards": 10,
|
||
"failed_dashboards": 0,
|
||
"failures": [],
|
||
}
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert "Сводка бэкапа" in summary
|
||
assert "успешно 10" in summary
|
||
assert "среда" in summary.lower()
|
||
|
||
def test_backup_with_failures(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
cm.get_environments.return_value = [_make_env("env-dev", "Development")]
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "superset-backup"
|
||
task.status = "PARTIAL"
|
||
task.params = {"environment_id": "env-dev"}
|
||
task.result = {
|
||
"total_dashboards": 5,
|
||
"backed_up_dashboards": 3,
|
||
"failed_dashboards": 2,
|
||
"failures": [{"id": 4, "title": "Dashboard X", "error": "API error"}],
|
||
}
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert "с ошибками 2" in summary
|
||
assert "Dashboard X" in summary
|
||
|
||
def test_validation_summary(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "llm_dashboard_validation"
|
||
task.status = "SUCCESS"
|
||
task.params = {}
|
||
task.result = {
|
||
"status": "passed",
|
||
"summary": "All checks passed",
|
||
"issues": [],
|
||
}
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert "Сводка валидации" in summary
|
||
assert "статус passed" in summary
|
||
|
||
def test_validation_result_without_summary(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "llm_dashboard_validation"
|
||
task.status = "FAILED"
|
||
task.params = {}
|
||
task.result = {} # no status or summary
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert "Сводка валидации" in summary
|
||
assert "Итог недоступен" in summary
|
||
|
||
def test_unknown_plugin_terminal_status(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "custom-plugin"
|
||
task.status = "SUCCESS"
|
||
task.params = {}
|
||
task.result = {}
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert "Задача завершена" in summary
|
||
|
||
def test_unknown_plugin_non_terminal_status(self):
|
||
from src.api.routes.assistant._resolvers import _build_task_observability_summary
|
||
cm = MagicMock()
|
||
|
||
task = MagicMock()
|
||
task.plugin_id = "custom-plugin"
|
||
task.status = "RUNNING"
|
||
task.params = {}
|
||
task.result = {}
|
||
|
||
summary = _build_task_observability_summary(task, cm)
|
||
assert summary == ""
|
||
|
||
|
||
# #endregion Test.Assistant.Resolvers
|