Files
ss-tools/backend/tests/plugins/test_llm_analysis_plugin.py
busya 005ef0f5c7 🎉 FINAL: 7194 tests passing, 0 failures, 80% raw / ~90% real coverage.
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
2026-06-16 00:12:49 +03:00

1460 lines
64 KiB
Python

# #region Test.LLMAnalysisPlugin [C:3] [TYPE Module] [SEMANTICS test, llm, plugin, validation, documentation]
# @BRIEF Comprehensive tests for LLMAnalysisPlugin module — helpers, DashboardValidationPlugin, DocumentationPlugin.
# @RELATION BINDS_TO -> [LLMAnalysisPlugin]
# @TEST_EDGE: masked_api_key -> Detected as invalid
# @TEST_EDGE: json_safe_datetime -> datetime converted to ISO string
# @TEST_EDGE: ensure_json_prompt -> Appends JSON format when missing
# @TEST_EDGE: update_run_status -> Aggregates record statuses correctly
from datetime import UTC, datetime, timedelta
import json
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
import pytest
# ── Module-level Function Tests ──
class TestIsMaskedOrInvalidApiKey:
"""Verify _is_masked_or_invalid_api_key."""
def test_none_is_invalid(self):
from src.plugins.llm_analysis.plugin import _is_masked_or_invalid_api_key
assert _is_masked_or_invalid_api_key(None) is True
def test_empty_is_invalid(self):
from src.plugins.llm_analysis.plugin import _is_masked_or_invalid_api_key
assert _is_masked_or_invalid_api_key("") is True
def test_masked_is_invalid(self):
from src.plugins.llm_analysis.plugin import _is_masked_or_invalid_api_key
assert _is_masked_or_invalid_api_key("********") is True
assert _is_masked_or_invalid_api_key("EMPTY_OR_NONE") is True
def test_short_is_invalid(self):
from src.plugins.llm_analysis.plugin import _is_masked_or_invalid_api_key
assert _is_masked_or_invalid_api_key("short") is True # len < 16
def test_valid_key(self):
from src.plugins.llm_analysis.plugin import _is_masked_or_invalid_api_key
assert _is_masked_or_invalid_api_key("sk-valid-key-that-is-long-enough") is False
def test_exactly_16_chars(self):
from src.plugins.llm_analysis.plugin import _is_masked_or_invalid_api_key
assert _is_masked_or_invalid_api_key("1234567890123456") is False # len == 16, valid
class TestJsonSafeValue:
"""Verify _json_safe_value."""
def test_datetime_converted(self):
from src.plugins.llm_analysis.plugin import _json_safe_value
dt = datetime(2024, 1, 15, 10, 30, 0, tzinfo=UTC)
result = _json_safe_value(dt)
assert result == "2024-01-15T10:30:00+00:00"
def test_nested_dict(self):
from src.plugins.llm_analysis.plugin import _json_safe_value
dt = datetime(2024, 6, 1, 12, 0, 0, tzinfo=UTC)
data = {"name": "test", "created": dt, "items": [{"updated": dt}]}
result = _json_safe_value(data)
assert result["created"] == "2024-06-01T12:00:00+00:00"
assert result["items"][0]["updated"] == "2024-06-01T12:00:00+00:00"
def test_plain_value(self):
from src.plugins.llm_analysis.plugin import _json_safe_value
assert _json_safe_value("hello") == "hello"
assert _json_safe_value(42) == 42
assert _json_safe_value(None) is None
def test_empty_structures(self):
from src.plugins.llm_analysis.plugin import _json_safe_value
assert _json_safe_value({}) == {}
assert _json_safe_value([]) == []
class TestEnsureJsonPrompt:
"""Verify _ensure_json_prompt."""
def test_none_returns_default(self):
from src.plugins.llm_analysis.plugin import _ensure_json_prompt
result = _ensure_json_prompt(None)
assert result.startswith("Analyze the dashboard")
assert '"status"' in result
def test_empty_returns_default(self):
from src.plugins.llm_analysis.plugin import _ensure_json_prompt
result = _ensure_json_prompt("")
assert result.startswith("Analyze the dashboard") # empty string is falsy
assert '"status"' in result
def test_already_has_json_format(self):
from src.plugins.llm_analysis.plugin import _ensure_json_prompt
prompt = 'Analyze this dashboard and return JSON with "status" and "summary"'
result = _ensure_json_prompt(prompt)
assert result == prompt # unchanged
def test_missing_json_appended(self):
from src.plugins.llm_analysis.plugin import _ensure_json_prompt
prompt = "Tell me about this dashboard"
result = _ensure_json_prompt(prompt)
assert 'Respond ONLY with valid JSON' in result
def test_has_json_keyword_but_no_structure(self):
"""Has 'json' keyword but no '"status"' or '"summary"'. JSON instruction appended."""
from src.plugins.llm_analysis.plugin import _ensure_json_prompt
prompt = "Return the data in json format"
result = _ensure_json_prompt(prompt)
assert 'Respond ONLY with valid JSON' in result
def test_has_curly_braces_json(self):
"""Prompt already has { and " suggesting JSON structure."""
from src.plugins.llm_analysis.plugin import _ensure_json_prompt
prompt = '{ "result": "ok" }'
result = _ensure_json_prompt(prompt)
assert result == prompt
class TestUpdateRunStatus:
"""Verify _update_run_status."""
def test_no_run_id_returns_early(self):
from src.plugins.llm_analysis.plugin import _update_run_status
db = MagicMock()
_update_run_status(db, None) # no crash
db.query.assert_not_called()
def test_empty_run_id_returns_early(self):
from src.plugins.llm_analysis.plugin import _update_run_status
db = MagicMock()
_update_run_status(db, "") # no crash
db.query.assert_not_called()
def test_no_records_found(self):
from src.plugins.llm_analysis.plugin import _update_run_status
db = MagicMock()
db.query.return_value.filter.return_value.all.return_value = []
db.query.return_value.filter.return_value.first.return_value = MagicMock()
_update_run_status(db, "run-1")
# Should log and return without commit
db.commit.assert_not_called()
def test_no_run_found(self):
from src.plugins.llm_analysis.plugin import _update_run_status
db = MagicMock()
db.query.return_value.filter.return_value.all.return_value = [MagicMock()]
db.query.return_value.filter.return_value.first.return_value = None
_update_run_status(db, "run-1")
db.commit.assert_not_called()
def test_partial_records_not_complete(self):
from src.plugins.llm_analysis.plugin import _update_run_status
db = MagicMock()
records = [MagicMock(status="PASS"), MagicMock(status="WARN")]
run = MagicMock()
run.dashboard_count = 5 # expect 5, got 2
db.query.return_value.filter.return_value.all.return_value = records
db.query.return_value.filter.return_value.first.return_value = run
_update_run_status(db, "run-1")
db.commit.assert_not_called()
def test_all_records_complete(self):
from src.plugins.llm_analysis.plugin import _update_run_status
db = MagicMock()
records = [
MagicMock(status="PASS"),
MagicMock(status="WARN"),
MagicMock(status="FAIL"),
MagicMock(status="UNKNOWN"),
]
run = MagicMock()
run.dashboard_count = 4
db.query.return_value.filter.return_value.all.return_value = records
db.query.return_value.filter.return_value.first.return_value = run
_update_run_status(db, "run-1")
assert run.status == 'completed'
assert run.pass_count == 1
assert run.warn_count == 1
assert run.fail_count == 1
assert run.unknown_count == 1
db.commit.assert_called_once()
def test_all_pass_updates_correctly(self):
from src.plugins.llm_analysis.plugin import _update_run_status
db = MagicMock()
records = [MagicMock(status="PASS"), MagicMock(status="PASS")]
run = MagicMock()
run.dashboard_count = 2
db.query.return_value.filter.return_value.all.return_value = records
db.query.return_value.filter.return_value.first.return_value = run
_update_run_status(db, "run-1")
assert run.pass_count == 2
assert run.fail_count == 0
db.commit.assert_called_once()
def test_exception_handled(self):
"""Exception during update does not propagate."""
from src.plugins.llm_analysis.plugin import _update_run_status
db = MagicMock()
db.query.side_effect = Exception("DB error")
_update_run_status(db, "run-1") # should not raise
# ── DashboardValidationPlugin Tests ──
class TestDashboardValidationPluginProperties:
"""Verify DashboardValidationPlugin static properties."""
def test_properties(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
assert plugin.id == "llm_dashboard_validation"
assert plugin.name == "Dashboard LLM Validation"
assert plugin.description == "Automated dashboard health analysis using LLMs."
assert plugin.version == "1.0.0"
def test_get_schema(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
schema = plugin.get_schema()
assert schema["type"] == "object"
assert "dashboard_id" in schema["properties"]
assert "environment_id" in schema["properties"]
assert "provider_id" in schema["required"]
class TestDashboardValidationPluginExecute:
"""Verify DashboardValidationPlugin.execute — main dispatch."""
def _make_mock_db(self):
"""Create a mock DB session."""
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_db.close.return_value = None
return mock_db
def _make_mock_provider(self, is_multimodal=True):
"""Create a mock DB provider."""
provider = MagicMock()
provider.id = "prov-1"
provider.name = "Test Provider"
provider.provider_type = "openai"
provider.base_url = "https://api.openai.com"
provider.default_model = "gpt-4o"
provider.is_active = True
provider.is_multimodal = is_multimodal
provider.max_images = 10
return provider
async def _run_execute(self, plugin, params, mock_db, mock_env, mock_provider, api_key="sk-real-key-that-is-valid", context=None, patch_path_a=None, patch_path_b=None):
"""Helper to run execute with all patching."""
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_env_obj = mock_env if mock_env else MagicMock()
mock_cm.get_environment.return_value = mock_env_obj
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = mock_provider
MockProviderService.return_value.get_decrypted_api_key.return_value = api_key
if patch_path_a is not None:
with patch.object(plugin, '_execute_path_a', new=AsyncMock(return_value=patch_path_a)):
return await plugin.execute(params, context=context)
elif patch_path_b is not None:
with patch.object(plugin, '_execute_path_b', new=AsyncMock(return_value=patch_path_b)):
return await plugin.execute(params, context=context)
else:
return await plugin.execute(params, context=context)
@pytest.mark.asyncio
async def test_execute_path_a(self):
"""Happy: Path A (screenshot) succeeds."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider(is_multimodal=True)
result = await self._run_execute(
plugin,
{"dashboard_id": "42", "environment_id": "env-1", "provider_id": "prov-1", "screenshot_enabled": True},
mock_db, MagicMock(), provider,
patch_path_a={"dashboard_id": "42", "execution_path": "multimodal", "status": "PASS"},
)
assert result["total"] == 1
@pytest.mark.asyncio
async def test_execute_path_b(self):
"""Happy: Path B (text-only) succeeds."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider(is_multimodal=False)
result = await self._run_execute(
plugin,
{"dashboard_id": "42", "environment_id": "env-1", "provider_id": "prov-1", "screenshot_enabled": False},
mock_db, MagicMock(), provider,
patch_path_b={"dashboard_id": "42", "execution_path": "text_only", "status": "WARN"},
)
assert result["total"] == 1
@pytest.mark.asyncio
async def test_execute_env_not_found(self):
"""Negative: env not found raises."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = None
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = provider
MockProviderService.return_value.get_decrypted_api_key.return_value = "sk-real-key-that-is-valid"
with pytest.raises(ValueError, match="env-1 not found"):
await plugin.execute({
"dashboard_id": "42",
"environment_id": "env-1",
"provider_id": "prov-1",
})
@pytest.mark.asyncio
async def test_execute_provider_not_found(self):
"""Negative: provider not found raises."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
mock_env = MagicMock()
mock_env.id = "env-1"
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = mock_env
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = None
with pytest.raises(ValueError, match="prov-1 not found"):
await plugin.execute({
"dashboard_id": "42",
"environment_id": "env-1",
"provider_id": "prov-1",
})
@pytest.mark.asyncio
async def test_execute_invalid_api_key(self):
"""Negative: masked/invalid API key raises."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
mock_env = MagicMock()
provider = self._make_mock_provider()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = mock_env
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = provider
MockProviderService.return_value.get_decrypted_api_key.return_value = "********"
with pytest.raises(ValueError, match="Invalid API key"):
await plugin.execute({
"dashboard_id": "42",
"environment_id": "env-1",
"provider_id": "prov-1",
})
@pytest.mark.asyncio
async def test_execute_no_dashboard_id(self):
"""Negative: no dashboard_id raises."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = MagicMock()
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = provider
MockProviderService.return_value.get_decrypted_api_key.return_value = "sk-real-key-that-is-valid"
with pytest.raises(ValueError, match="No dashboard_id"):
await plugin.execute({
"environment_id": "env-1",
"provider_id": "prov-1",
})
@pytest.mark.asyncio
async def test_execute_screenshot_without_multimodal_raises(self):
"""Negative: screenshot_enabled but provider not multimodal."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
mock_env = MagicMock()
provider = self._make_mock_provider(is_multimodal=False)
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = mock_env
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = provider
MockProviderService.return_value.get_decrypted_api_key.return_value = "sk-real-key-that-is-valid"
with pytest.raises(ValueError, match="multimodal"):
await plugin.execute({
"dashboard_id": "42",
"environment_id": "env-1",
"provider_id": "prov-1",
"screenshot_enabled": True,
})
@pytest.mark.asyncio
async def test_execute_with_dashboard_ids_list(self):
"""Happy: v2 dashboard_ids list processed."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider()
mock_env = MagicMock()
result = await self._run_execute(
plugin,
{"dashboard_ids": ["42", "43"], "environment_id": "env-1", "provider_id": "prov-1", "screenshot_enabled": False},
mock_db, mock_env, provider,
patch_path_b={"dashboard_id": "42", "status": "PASS"},
)
assert result["total"] == 2
@pytest.mark.asyncio
async def test_execute_continues_on_dashboard_failure(self):
"""Negative: one dashboard fails, loop continues to next."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider()
mock_env = MagicMock()
call_count = [0]
async def path_b_side(params, ctx, db, prov, key, env, cm, log):
call_count[0] += 1
if call_count[0] == 1:
raise ValueError("First dashboard failed")
return {"dashboard_id": params.get("dashboard_id"), "status": "PASS"}
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = mock_env
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = provider
MockProviderService.return_value.get_decrypted_api_key.return_value = "sk-real-key-that-is-valid"
with patch.object(plugin, '_execute_path_b', new=AsyncMock(side_effect=path_b_side)):
with pytest.raises(ValueError, match="First dashboard failed"):
await plugin.execute({
"dashboard_ids": ["42", "43"],
"environment_id": "env-1",
"provider_id": "prov-1",
"screenshot_enabled": False,
})
assert call_count[0] == 2
@pytest.mark.asyncio
async def test_execute_with_context(self):
"""Edge: TaskContext provided."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider()
mock_ctx = MagicMock()
mock_ctx.logger = MagicMock()
mock_ctx.logger.with_source.return_value = MagicMock()
mock_ctx.task_id = "task-1"
mock_env = MagicMock()
result = await self._run_execute(
plugin,
{"dashboard_ids": ["42"], "environment_id": "env-1", "provider_id": "prov-1", "screenshot_enabled": False},
mock_db, mock_env, provider,
context=mock_ctx,
patch_path_b={"dashboard_id": "42", "status": "PASS"},
)
assert result["total"] == 1
@pytest.mark.asyncio
async def test_execute_url_reparse(self):
"""Edge: dashboard URL provided triggers URL re-parsing."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = MagicMock()
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = provider
MockProviderService.return_value.get_decrypted_api_key.return_value = "sk-real-key-that-is-valid"
with patch('src.core.utils.superset_context_extractor.SupersetContextExtractor') as MockExtractor:
mock_ext = MagicMock()
MockExtractor.return_value = mock_ext
mock_parsed = MagicMock()
mock_parsed.dashboard_id = "99"
mock_ext.parse_superset_link.return_value = mock_parsed
with patch.object(plugin, '_execute_path_b', new=AsyncMock(return_value={
"dashboard_id": "99", "status": "PASS",
})):
result = await plugin.execute({
"dashboard_id": "42",
"dashboard_url": "http://example.com/superset/dashboard/99/",
"environment_id": "env-1",
"provider_id": "prov-1",
"screenshot_enabled": False,
})
assert result["total"] == 1
@pytest.mark.asyncio
async def test_execute_url_reparse_failure_sets_source_invalid(self):
"""Edge: URL re-parse failure sets source_invalid flag."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = self._make_mock_db()
provider = self._make_mock_provider()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = MagicMock()
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = provider
MockProviderService.return_value.get_decrypted_api_key.return_value = "sk-real-key-that-is-valid"
with patch('src.core.utils.superset_context_extractor.SupersetContextExtractor') as MockExtractor:
mock_ext = MagicMock()
MockExtractor.return_value = mock_ext
mock_ext.parse_superset_link.side_effect = ValueError("Bad URL")
with patch.object(plugin, '_execute_path_b', new=AsyncMock(return_value={
"dashboard_id": "42", "status": "PASS",
})):
result = await plugin.execute({
"dashboard_id": "42",
"dashboard_url": "bad-url",
"environment_id": "env-1",
"provider_id": "prov-1",
"screenshot_enabled": False,
})
assert result["total"] == 1
class TestExecutePathA:
"""Verify _execute_path_a — screenshot + multimodal LLM flow."""
@pytest.mark.asyncio
async def test_full_flow(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.name = "Test"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.max_images = 10
mock_cm = MagicMock()
mock_cfg = MagicMock()
mock_cfg.settings.storage.root_path = "/tmp/storage"
mock_cm.get_config.return_value = mock_cfg
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
params = {
"dashboard_id": "42",
"environment_id": "env-1",
"run_id": "run-42",
}
# Mock ScreenshotService
with patch('src.plugins.llm_analysis.plugin.ScreenshotService') as MockSS:
mock_ss = MagicMock()
MockSS.return_value = mock_ss
mock_ss.capture_dashboard = AsyncMock(return_value=(["/tmp/jpeg.jpg"], [{"webp_path": "/tmp/archive.webp"}]))
# Mock LLMClient
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
mock_llm = MagicMock()
MockLLM.return_value = mock_llm
mock_llm.analyze_dashboard_multimodal = AsyncMock(return_value={
"status": "PASS",
"summary": "All good",
"issues": [],
"chunk_count": 1,
})
# Mock _fetch_dashboard_logs
with patch.object(plugin, '_fetch_dashboard_logs', new=AsyncMock(return_value=(
["log entry"], {"logs_fetch_duration_ms": 10}
))):
# Mock cleanup
with patch('src.plugins.llm_analysis.plugin.ScreenshotService._cleanup_temp_files'):
# Mock NotificationService
with patch('src.plugins.llm_analysis.plugin.NotificationService') as MockNotif:
mock_notif = MagicMock()
MockNotif.return_value = mock_notif
mock_notif.dispatch_report = AsyncMock()
result = await plugin._execute_path_a(
params=params,
context=None,
db=mock_db,
db_provider=mock_provider,
api_key="sk-real-key",
env=mock_env,
config_mgr=mock_cm,
log=mock_log,
)
assert result["execution_path"] == "multimodal"
assert result["status"] == "PASS"
assert "timings" in result
mock_db.add.assert_called_once()
mock_db.commit.assert_called()
@pytest.mark.asyncio
async def test_no_screenshots_falls_back_to_text(self):
"""When capture_dashboard returns no screenshots, fallback to text-only."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.name = "Test"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.max_images = 10
mock_cm = MagicMock()
mock_cfg = MagicMock()
mock_cfg.settings.storage.root_path = "/tmp/storage"
mock_cm.get_config.return_value = mock_cfg
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
params = {"dashboard_id": "42", "environment_id": "env-1"}
with patch('src.plugins.llm_analysis.plugin.ScreenshotService') as MockSS:
mock_ss = MagicMock()
MockSS.return_value = mock_ss
# No screenshots captured
mock_ss.capture_dashboard = AsyncMock(return_value=([], []))
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
mock_llm = MagicMock()
MockLLM.return_value = mock_llm
mock_llm.analyze_dashboard_text_batch = AsyncMock(return_value={
"dashboards": [{"status": "WARN", "summary": "No screenshots", "issues": []}]
})
with patch.object(plugin, '_fetch_dashboard_logs', new=AsyncMock(return_value=(
["log"], {"logs_fetch_duration_ms": 5}
))):
with patch('src.plugins.llm_analysis.plugin.ScreenshotService._cleanup_temp_files'):
with patch('src.plugins.llm_analysis.plugin.NotificationService') as MockNotif:
MockNotif.return_value.dispatch_report = AsyncMock()
result = await plugin._execute_path_a(
params, None, mock_db, mock_provider,
"sk-key", mock_env, mock_cm, mock_log,
)
assert result["status"] == "WARN"
@pytest.mark.asyncio
async def test_notification_failure_does_not_block(self):
"""Edge: notification failure is logged but does not block return."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.name = "Test"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.max_images = 10
mock_cm = MagicMock()
mock_cfg = MagicMock()
mock_cfg.settings.storage.root_path = "/tmp/storage"
mock_cm.get_config.return_value = mock_cfg
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
params = {"dashboard_id": "42", "environment_id": "env-1"}
with patch('src.plugins.llm_analysis.plugin.ScreenshotService') as MockSS:
MockSS.return_value.capture_dashboard = AsyncMock(return_value=(["/tmp/jpg"], [{"webp_path": "/tmp/webp"}]))
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
MockLLM.return_value.analyze_dashboard_multimodal = AsyncMock(return_value={
"status": "PASS", "summary": "OK", "issues": [], "chunk_count": 1,
})
with patch.object(plugin, '_fetch_dashboard_logs', new=AsyncMock(return_value=(
["log"], {"logs_fetch_duration_ms": 5}
))):
with patch('src.plugins.llm_analysis.plugin.ScreenshotService._cleanup_temp_files'):
with patch('src.plugins.llm_analysis.plugin.NotificationService') as MockNotif:
MockNotif.return_value.dispatch_report = AsyncMock(side_effect=RuntimeError("Notif failed"))
result = await plugin._execute_path_a(
params, None, mock_db, mock_provider,
"sk-key", mock_env, mock_cm, mock_log,
)
assert result["status"] == "PASS" # notification failure is non-blocking
class TestExecutePathB:
"""Verify _execute_path_b — text-only LLM flow."""
@pytest.mark.asyncio
async def test_full_flow(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.name = "Test"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.max_images = 10
mock_cm = MagicMock()
mock_cfg = MagicMock()
mock_cfg.settings.llm = {"prompts": {"dashboard_validation_prompt_text": "Analyze: {topology}"}}
mock_cm.get_config.return_value = mock_cfg
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
params = {"dashboard_id": "42", "environment_id": "env-1", "run_id": "run-99"}
# Mock SupersetClient
with patch('src.plugins.llm_analysis.plugin.SupersetClient') as MockSC:
mock_sc = MagicMock()
MockSC.return_value = mock_sc
mock_sc.get_dashboard = AsyncMock(return_value={
"result": {
"slices": [{"id": 1, "slice_id": 1}],
}
})
mock_sc.get_chart = AsyncMock(return_value={
"result": {
"id": 1,
"slice_name": "Chart 1",
"datasource_id": 101,
"datasource_type": "table",
"viz_type": "table",
"params": '{"metrics": ["count"]}',
}
})
# Mock DatasetHealthChecker
with patch('src.plugins.llm_analysis.plugin.DatasetHealthChecker') as MockDHC:
mock_dhc = MagicMock()
MockDHC.return_value = mock_dhc
mock_dhc.check_dashboard_datasets = AsyncMock(return_value={
"datasets": [{"dataset_id": 101, "metadata_accessible": True}],
"chart_data": [],
})
# Mock LLMClient
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
mock_llm = MagicMock()
MockLLM.return_value = mock_llm
mock_llm.get_json_completion = AsyncMock(return_value={
"status": "PASS",
"summary": "Text analysis OK",
"issues": [],
})
# Mock _fetch_dashboard_logs
with patch.object(plugin, '_fetch_dashboard_logs', new=AsyncMock(return_value=(
["log entry"], {"logs_fetch_duration_ms": 10}
))):
with patch('src.plugins.llm_analysis.plugin.build_dashboard_topology', return_value="Tab1 > Chart1"):
with patch('src.plugins.llm_analysis.plugin.NotificationService') as MockNotif:
MockNotif.return_value.dispatch_report = AsyncMock()
result = await plugin._execute_path_b(
params=params,
context=None,
db=mock_db,
db_provider=mock_provider,
api_key="sk-real-key",
env=mock_env,
config_mgr=mock_cm,
log=mock_log,
)
assert result["execution_path"] == "text_only"
assert result["status"] == "PASS"
mock_db.add.assert_called_once()
mock_db.commit.assert_called()
class TestFetchDashboardLogs:
"""Verify _execute_path_a — screenshot + multimodal LLM flow."""
@pytest.mark.asyncio
async def test_full_flow(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.name = "Test"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.max_images = 10
mock_cm = MagicMock()
mock_cfg = MagicMock()
mock_cfg.settings.storage.root_path = "/tmp/storage"
mock_cm.get_config.return_value = mock_cfg
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
params = {
"dashboard_id": "42",
"environment_id": "env-1",
"run_id": "run-42",
}
# Mock ScreenshotService
with patch('src.plugins.llm_analysis.plugin.ScreenshotService') as MockSS:
mock_ss = MagicMock()
MockSS.return_value = mock_ss
mock_ss.capture_dashboard = AsyncMock(return_value=(["/tmp/jpeg.jpg"], [{"webp_path": "/tmp/archive.webp"}]))
# Mock LLMClient
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
mock_llm = MagicMock()
MockLLM.return_value = mock_llm
mock_llm.analyze_dashboard_multimodal = AsyncMock(return_value={
"status": "PASS",
"summary": "All good",
"issues": [],
"chunk_count": 1,
})
# Mock _fetch_dashboard_logs
with patch.object(plugin, '_fetch_dashboard_logs', new=AsyncMock(return_value=(
["log entry"], {"logs_fetch_duration_ms": 10}
))):
# Mock cleanup
with patch('src.plugins.llm_analysis.plugin.ScreenshotService._cleanup_temp_files'):
# Mock NotificationService
with patch('src.plugins.llm_analysis.plugin.NotificationService') as MockNotif:
mock_notif = MagicMock()
MockNotif.return_value = mock_notif
mock_notif.dispatch_report = AsyncMock()
result = await plugin._execute_path_a(
params=params,
context=None,
db=mock_db,
db_provider=mock_provider,
api_key="sk-real-key",
env=mock_env,
config_mgr=mock_cm,
log=mock_log,
)
assert result["execution_path"] == "multimodal"
assert result["status"] == "PASS"
assert "timings" in result
mock_db.add.assert_called_once()
mock_db.commit.assert_called()
@pytest.mark.asyncio
async def test_no_screenshots_falls_back_to_text(self):
"""When capture_dashboard returns no screenshots, fallback to text-only."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.name = "Test"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.max_images = 10
mock_cm = MagicMock()
mock_cfg = MagicMock()
mock_cfg.settings.storage.root_path = "/tmp/storage"
mock_cm.get_config.return_value = mock_cfg
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
params = {"dashboard_id": "42", "environment_id": "env-1"}
with patch('src.plugins.llm_analysis.plugin.ScreenshotService') as MockSS:
mock_ss = MagicMock()
MockSS.return_value = mock_ss
# No screenshots captured
mock_ss.capture_dashboard = AsyncMock(return_value=([], []))
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
mock_llm = MagicMock()
MockLLM.return_value = mock_llm
mock_llm.analyze_dashboard_text_batch = AsyncMock(return_value={
"dashboards": [{"status": "WARN", "summary": "No screenshots", "issues": []}]
})
with patch.object(plugin, '_fetch_dashboard_logs', new=AsyncMock(return_value=(
["log"], {"logs_fetch_duration_ms": 5}
))):
with patch('src.plugins.llm_analysis.plugin.ScreenshotService._cleanup_temp_files'):
with patch('src.plugins.llm_analysis.plugin.NotificationService') as MockNotif:
MockNotif.return_value.dispatch_report = AsyncMock()
result = await plugin._execute_path_a(
params, None, mock_db, mock_provider,
"sk-key", mock_env, mock_cm, mock_log,
)
assert result["status"] == "WARN"
@pytest.mark.asyncio
async def test_notification_failure_does_not_block(self):
"""Edge: notification failure is logged but does not block return."""
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.name = "Test"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.max_images = 10
mock_cm = MagicMock()
mock_cfg = MagicMock()
mock_cfg.settings.storage.root_path = "/tmp/storage"
mock_cm.get_config.return_value = mock_cfg
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
params = {"dashboard_id": "42", "environment_id": "env-1"}
with patch('src.plugins.llm_analysis.plugin.ScreenshotService') as MockSS:
MockSS.return_value.capture_dashboard = AsyncMock(return_value=(["/tmp/jpg"], [{"webp_path": "/tmp/webp"}]))
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
MockLLM.return_value.analyze_dashboard_multimodal = AsyncMock(return_value={
"status": "PASS", "summary": "OK", "issues": [], "chunk_count": 1,
})
with patch.object(plugin, '_fetch_dashboard_logs', new=AsyncMock(return_value=(
["log"], {"logs_fetch_duration_ms": 5}
))):
with patch('src.plugins.llm_analysis.plugin.ScreenshotService._cleanup_temp_files'):
with patch('src.plugins.llm_analysis.plugin.NotificationService') as MockNotif:
MockNotif.return_value.dispatch_report = AsyncMock(side_effect=RuntimeError("Notif failed"))
result = await plugin._execute_path_a(
params, None, mock_db, mock_provider,
"sk-key", mock_env, mock_cm, mock_log,
)
assert result["status"] == "PASS" # notification failure is non-blocking
class TestExecutePathB:
"""Verify _execute_path_b — text-only LLM flow."""
@pytest.mark.asyncio
async def test_full_flow(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_db = MagicMock()
mock_db.add.return_value = None
mock_db.commit.return_value = None
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.name = "Test"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.max_images = 10
mock_cm = MagicMock()
mock_cfg = MagicMock()
mock_cfg.settings.llm = {"prompts": {"dashboard_validation_prompt_text": "Analyze: {topology}"}}
mock_cm.get_config.return_value = mock_cfg
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
params = {"dashboard_id": "42", "environment_id": "env-1", "run_id": "run-99"}
# Mock SupersetClient
with patch('src.plugins.llm_analysis.plugin.SupersetClient') as MockSC:
mock_sc = MagicMock()
MockSC.return_value = mock_sc
mock_sc.get_dashboard = AsyncMock(return_value={
"result": {
"slices": [{"id": 1, "slice_id": 1}],
}
})
mock_sc.get_chart = AsyncMock(return_value={
"result": {
"id": 1,
"slice_name": "Chart 1",
"datasource_id": 101,
"datasource_type": "table",
"viz_type": "table",
"params": '{"metrics": ["count"]}',
}
})
# Mock DatasetHealthChecker
with patch('src.plugins.llm_analysis.plugin.DatasetHealthChecker') as MockDHC:
mock_dhc = MagicMock()
MockDHC.return_value = mock_dhc
mock_dhc.check_dashboard_datasets = AsyncMock(return_value={
"datasets": [{"dataset_id": 101, "metadata_accessible": True}],
"chart_data": [],
})
# Mock LLMClient
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
mock_llm = MagicMock()
MockLLM.return_value = mock_llm
mock_llm.get_json_completion = AsyncMock(return_value={
"status": "PASS",
"summary": "Text analysis OK",
"issues": [],
})
# Mock _fetch_dashboard_logs
with patch.object(plugin, '_fetch_dashboard_logs', new=AsyncMock(return_value=(
["log entry"], {"logs_fetch_duration_ms": 10}
))):
with patch('src.plugins.llm_analysis.plugin.build_dashboard_topology', return_value="Tab1 > Chart1"):
with patch('src.plugins.llm_analysis.plugin.NotificationService') as MockNotif:
MockNotif.return_value.dispatch_report = AsyncMock()
result = await plugin._execute_path_b(
params=params,
context=None,
db=mock_db,
db_provider=mock_provider,
api_key="sk-real-key",
env=mock_env,
config_mgr=mock_cm,
log=mock_log,
)
assert result["execution_path"] == "text_only"
assert result["status"] == "PASS"
mock_db.add.assert_called_once()
mock_db.commit.assert_called()
class TestFetchDashboardLogs:
"""Verify _fetch_dashboard_logs."""
@pytest.mark.asyncio
async def test_success(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
with patch('src.plugins.llm_analysis.plugin.SupersetClient') as MockSC:
mock_sc = MagicMock()
MockSC.return_value = mock_sc
mock_sc.network.request = AsyncMock(return_value={
"result": [
{"action": "view", "dttm": "2024-01-01T00:00:00", "json": "{}"},
{"action": "save", "dttm": "2024-01-01T01:00:00", "json": '{"foo": "bar"}'},
]
})
logs, timings = await plugin._fetch_dashboard_logs(mock_env, "42", mock_log)
assert len(logs) == 2
assert "[2024-01-01T00:00:00] view: {}" in logs
assert "logs_fetch_duration_ms" in timings
@pytest.mark.asyncio
async def test_no_logs_found(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
with patch('src.plugins.llm_analysis.plugin.SupersetClient') as MockSC:
mock_sc = MagicMock()
MockSC.return_value = mock_sc
mock_sc.network.request = AsyncMock(return_value={"result": []})
logs, timings = await plugin._fetch_dashboard_logs(mock_env, "42", mock_log)
assert len(logs) == 1
assert "No recent logs" in logs[0]
@pytest.mark.asyncio
async def test_network_error(self):
from src.plugins.llm_analysis.plugin import DashboardValidationPlugin
plugin = DashboardValidationPlugin()
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.url = "https://superset.example.com"
mock_log = MagicMock()
mock_log.with_source.return_value = mock_log
with patch('src.plugins.llm_analysis.plugin.SupersetClient') as MockSC:
mock_sc = MagicMock()
MockSC.return_value = mock_sc
mock_sc.network.request.side_effect = ConnectionError("API unreachable")
logs, timings = await plugin._fetch_dashboard_logs(mock_env, "42", mock_log)
assert len(logs) == 1
assert "Error fetching" in logs[0]
# ── DocumentationPlugin Tests ──
class TestDocumentationPluginProperties:
"""Verify DocumentationPlugin static properties."""
def test_properties(self):
from src.plugins.llm_analysis.plugin import DocumentationPlugin
plugin = DocumentationPlugin()
assert plugin.id == "llm_documentation"
assert plugin.name == "Dataset LLM Documentation"
assert plugin.description == "Automated dataset and column documentation using LLMs."
assert plugin.version == "1.0.0"
def test_get_schema(self):
from src.plugins.llm_analysis.plugin import DocumentationPlugin
plugin = DocumentationPlugin()
schema = plugin.get_schema()
assert schema["type"] == "object"
assert "dataset_id" in schema["properties"]
assert "environment_id" in schema["properties"]
assert "provider_id" in schema["properties"]
assert "dataset_id" in schema["required"]
class TestDocumentationPluginExecute:
"""Verify DocumentationPlugin.execute."""
@pytest.mark.asyncio
async def test_full_flow(self):
from src.plugins.llm_analysis.plugin import DocumentationPlugin
plugin = DocumentationPlugin()
mock_db = MagicMock()
mock_db.close.return_value = None
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_env = MagicMock()
mock_env.id = "env-1"
mock_env.name = "Test Env"
mock_cm.get_environment.return_value = mock_env
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
mock_svc = MagicMock()
MockProviderService.return_value = mock_svc
mock_provider = MagicMock()
mock_provider.id = "prov-1"
mock_provider.provider_type = "openai"
mock_provider.base_url = "https://api.openai.com"
mock_provider.default_model = "gpt-4o"
mock_provider.is_multimodal = True
mock_svc.get_provider.return_value = mock_provider
mock_svc.get_decrypted_api_key.return_value = "sk-real-key-that-is-long-enough"
with patch('src.core.superset_client.SupersetClient') as MockSC:
mock_sc = MagicMock()
MockSC.return_value = mock_sc
mock_sc.get_dataset = AsyncMock(return_value={
"columns": [
{"id": 1, "column_name": "id", "type": "INTEGER", "description": None},
{"id": 2, "column_name": "name", "type": "VARCHAR", "description": None},
],
"table_name": "users",
})
mock_sc.update_dataset = AsyncMock(return_value={"result": "ok"})
with patch('src.plugins.llm_analysis.plugin.LLMClient') as MockLLM:
mock_llm = MagicMock()
MockLLM.return_value = mock_llm
mock_llm.get_json_completion = AsyncMock(return_value={
"dataset_description": "Users table with IDs and names",
"column_descriptions": [
{"name": "id", "description": "Primary key"},
{"name": "name", "description": "User display name"},
],
})
mock_cfg = MagicMock()
mock_cfg.settings.llm = {"prompts": {"documentation_prompt": "Doc: {dataset_name} {columns_json}"}}
mock_cm.get_config.return_value = mock_cfg
result = await plugin.execute({
"dataset_id": "1",
"environment_id": "env-1",
"provider_id": "prov-1",
})
assert "dataset_description" in result
assert result["dataset_description"] == "Users table with IDs and names"
mock_sc.update_dataset.assert_called_once()
@pytest.mark.asyncio
async def test_env_not_found(self):
from src.plugins.llm_analysis.plugin import DocumentationPlugin
plugin = DocumentationPlugin()
mock_db = MagicMock()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = None
with pytest.raises(ValueError, match="env-1 not found"):
await plugin.execute({
"dataset_id": "1",
"environment_id": "env-1",
"provider_id": "prov-1",
})
@pytest.mark.asyncio
async def test_provider_not_found(self):
from src.plugins.llm_analysis.plugin import DocumentationPlugin
plugin = DocumentationPlugin()
mock_db = MagicMock()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = MagicMock()
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = None
with pytest.raises(ValueError, match="prov-1 not found"):
await plugin.execute({
"dataset_id": "1",
"environment_id": "env-1",
"provider_id": "prov-1",
})
@pytest.mark.asyncio
async def test_invalid_api_key(self):
from src.plugins.llm_analysis.plugin import DocumentationPlugin
plugin = DocumentationPlugin()
mock_db = MagicMock()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = MagicMock()
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
MockProviderService.return_value.get_provider.return_value = MagicMock()
MockProviderService.return_value.get_decrypted_api_key.return_value = "********"
with pytest.raises(ValueError, match="Invalid API key"):
await plugin.execute({
"dataset_id": "1",
"environment_id": "env-1",
"provider_id": "prov-1",
})
@pytest.mark.asyncio
async def test_provider_not_found(self):
from src.plugins.llm_analysis.plugin import DocumentationPlugin
plugin = DocumentationPlugin()
mock_db = MagicMock()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = MagicMock()
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
mock_svc = MagicMock()
MockProviderService.return_value = mock_svc
mock_svc.get_provider.return_value = None
with pytest.raises(ValueError, match="prov-1 not found"):
await plugin.execute({
"dataset_id": "1",
"environment_id": "env-1",
"provider_id": "prov-1",
})
@pytest.mark.asyncio
async def test_invalid_api_key(self):
from src.plugins.llm_analysis.plugin import DocumentationPlugin
plugin = DocumentationPlugin()
mock_db = MagicMock()
with patch('src.plugins.llm_analysis.plugin.SessionLocal', return_value=mock_db):
with patch('src.dependencies.get_config_manager') as mock_get_cm:
mock_cm = MagicMock()
mock_get_cm.return_value = mock_cm
mock_cm.get_environment.return_value = MagicMock()
with patch('src.plugins.llm_analysis.plugin.LLMProviderService') as MockProviderService:
mock_svc = MagicMock()
MockProviderService.return_value = mock_svc
mock_svc.get_provider.return_value = MagicMock()
mock_svc.get_decrypted_api_key.return_value = "********"
with pytest.raises(ValueError, match="Invalid API key"):
await plugin.execute({
"dataset_id": "1",
"environment_id": "env-1",
"provider_id": "prov-1",
})
# #endregion Test.LLMAnalysisPlugin