Группы исправлений: - Группа 1 (async/await misuse): MagicMock → AsyncMock для get_dashboards, export_dashboard, import_dashboard, sync_environment, get_run_detail, list_all_runs, create_task и др. — 23 теста - Группа 2 (runner.run deadlock): добавлены моки get_async_job_runner + IdMappingService/AsyncSupersetClient в migration plugin + API tests для предотвращения вечной блокировки future.result() — 16 тестов - Группа 3 (SyntaxError): исправлены 7 незакрытых скобок ')' в test_validation_tasks_comprehensive.py (QA-агент оставил AsyncMock без закрывающих скобок) - Группа 4 (mock verification): logger mock error→explore, scheduler tests skipped (удалён из production), dataset mapper — 11 тестов - Группа 5 (search/assistant): MagicMock → AsyncMock — 9 тестов - Группа 6 (extractor parsing): AsyncMock для async методов — 9 тестов Итого: 61 ранее FAILED → 274 passed, 4 skipped, 0 failed
578 lines
24 KiB
Python
578 lines
24 KiB
Python
# #region Test.ValidationTaskService.Helpers [C:3] [TYPE Module] [SEMANTICS test,validation,helpers,utilities]
|
|
# @BRIEF Tests for validation_service.py — helper functions, provider/environment validation, run/record conversion, source resolution, time fields.
|
|
# @RELATION BINDS_TO -> [ValidationService]
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from datetime import UTC, datetime, time
|
|
import pytest
|
|
|
|
|
|
class TestHelperFunctions:
|
|
"""_extract_screenshot_paths, _parse_time, _format_time."""
|
|
|
|
def test_extract_screenshot_paths_none(self):
|
|
from src.services.validation_service import _extract_screenshot_paths
|
|
assert _extract_screenshot_paths(None) == []
|
|
|
|
def test_extract_screenshot_paths_valid(self):
|
|
from src.services.validation_service import _extract_screenshot_paths
|
|
result = _extract_screenshot_paths('{"screenshot_paths": ["/path/1.png", "/path/2.png"]}')
|
|
assert result == ["/path/1.png", "/path/2.png"]
|
|
|
|
def test_extract_screenshot_paths_empty_json(self):
|
|
from src.services.validation_service import _extract_screenshot_paths
|
|
result = _extract_screenshot_paths('{"other": "data"}')
|
|
assert result == []
|
|
|
|
def test_extract_screenshot_paths_invalid_json(self):
|
|
from src.services.validation_service import _extract_screenshot_paths
|
|
result = _extract_screenshot_paths("not-json")
|
|
assert result == []
|
|
|
|
def test_parse_time_none(self):
|
|
from src.services.validation_service import _parse_time
|
|
assert _parse_time(None) is None
|
|
|
|
def test_parse_time_valid(self):
|
|
from src.services.validation_service import _parse_time
|
|
result = _parse_time("09:30")
|
|
assert result == time(9, 30)
|
|
|
|
def test_parse_time_invalid(self):
|
|
from src.services.validation_service import _parse_time
|
|
assert _parse_time("abc") is None
|
|
|
|
def test_format_time_none(self):
|
|
from src.services.validation_service import _format_time
|
|
assert _format_time(None) is None
|
|
|
|
def test_format_time_valid(self):
|
|
from src.services.validation_service import _format_time
|
|
assert _format_time(time(14, 5)) == "14:05"
|
|
|
|
|
|
class TestValidateProvider:
|
|
"""_validate_provider — existence, active, multimodal checks."""
|
|
|
|
@pytest.fixture
|
|
def service(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
return ValidationTaskService(db, config_manager=None)
|
|
|
|
def test_empty_provider_id_raises(self, service):
|
|
with pytest.raises(ValueError, match="provider_id is required"):
|
|
service._validate_provider("")
|
|
|
|
def test_none_provider_id_raises(self, service):
|
|
with pytest.raises(ValueError, match="provider_id is required"):
|
|
service._validate_provider(" ")
|
|
|
|
def test_not_found_raises(self, service):
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = None
|
|
mock_cls.return_value = mock_svc
|
|
with pytest.raises(ValueError, match="not found"):
|
|
service._validate_provider("prov-1")
|
|
|
|
def test_inactive_raises(self, service):
|
|
provider = MagicMock()
|
|
provider.is_active = False
|
|
provider.name = "GPT-4"
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = provider
|
|
mock_cls.return_value = mock_svc
|
|
with pytest.raises(ValueError, match="not active"):
|
|
service._validate_provider("prov-1")
|
|
|
|
def test_non_multimodal_raises(self, service):
|
|
provider = MagicMock()
|
|
provider.is_active = True
|
|
provider.is_multimodal = False
|
|
provider.name = "GPT-4"
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = provider
|
|
mock_cls.return_value = mock_svc
|
|
with pytest.raises(ValueError, match="not multimodal"):
|
|
service._validate_provider("prov-1", require_multimodal=True)
|
|
|
|
def test_multimodal_not_required(self, service):
|
|
provider = MagicMock()
|
|
provider.is_active = True
|
|
provider.is_multimodal = False
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = provider
|
|
mock_cls.return_value = mock_svc
|
|
result = service._validate_provider("prov-1", require_multimodal=False)
|
|
assert result is provider
|
|
|
|
def test_valid_multimodal(self, service):
|
|
provider = MagicMock()
|
|
provider.is_active = True
|
|
provider.is_multimodal = True
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = provider
|
|
mock_cls.return_value = mock_svc
|
|
result = service._validate_provider("prov-1")
|
|
assert result is provider
|
|
|
|
|
|
class TestValidateEnvironment:
|
|
"""_validate_environment — existence check."""
|
|
|
|
def test_no_config_manager_skips(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock(), config_manager=None)
|
|
svc._validate_environment("env-1")
|
|
|
|
def test_environment_found(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = MagicMock()
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
svc._validate_environment("env-1")
|
|
|
|
def test_environment_not_found(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = None
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
with pytest.raises(ValueError, match="not found"):
|
|
svc._validate_environment("env-unknown")
|
|
|
|
|
|
class TestRunToDict:
|
|
"""_run_to_dict — conversion with duration_ms."""
|
|
|
|
def make_run(self, **overrides):
|
|
run = MagicMock()
|
|
run.id = "run-1"
|
|
run.policy_id = "pol-1"
|
|
run.task_id = "task-1"
|
|
run.status = "completed"
|
|
run.trigger = "manual"
|
|
run.dashboard_count = 5
|
|
run.pass_count = 3
|
|
run.warn_count = 1
|
|
run.fail_count = 1
|
|
run.unknown_count = 0
|
|
run.started_at = None
|
|
run.finished_at = None
|
|
run.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
for k, v in overrides.items():
|
|
setattr(run, k, v)
|
|
return run
|
|
|
|
def test_no_dates(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
result = ValidationTaskService(MagicMock())._run_to_dict(self.make_run())
|
|
assert result["duration_ms"] is None
|
|
assert result["status"] == "completed"
|
|
|
|
def test_with_dates_computes_duration(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
run = self.make_run(
|
|
started_at=datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC),
|
|
finished_at=datetime(2026, 1, 1, 12, 3, 30, tzinfo=UTC),
|
|
)
|
|
result = ValidationTaskService(MagicMock())._run_to_dict(run)
|
|
assert result["duration_ms"] == 210000
|
|
|
|
def test_naive_datetime_utc(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
run = self.make_run(
|
|
started_at=datetime(2026, 1, 1, 12, 0, 0),
|
|
finished_at=datetime(2026, 1, 1, 12, 1, 0),
|
|
)
|
|
result = ValidationTaskService(MagicMock())._run_to_dict(run)
|
|
assert result["duration_ms"] == 60000
|
|
|
|
|
|
class TestRecordToDict:
|
|
"""_record_to_dict — conversion with title resolution."""
|
|
|
|
def make_record(self, **overrides):
|
|
from src.models.llm import ValidationRecord as VR
|
|
record = MagicMock(spec=VR)
|
|
record.id = "rec-1"
|
|
record.run_id = "run-1"
|
|
record.policy_id = "pol-1"
|
|
record.dashboard_id = "42"
|
|
record.environment_id = "env-1"
|
|
record.status = "PASS"
|
|
record.summary = "OK"
|
|
record.issues = []
|
|
record.raw_response = None
|
|
record.screenshot_path = None
|
|
record.screenshot_paths = None
|
|
record.execution_path = None
|
|
record.dataset_health = None
|
|
record.chart_data_results = []
|
|
record.tab_screenshots = []
|
|
record.logs_sent_to_llm = []
|
|
record.token_usage = None
|
|
record.timings = None
|
|
record.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
for k, v in overrides.items():
|
|
setattr(record, k, v)
|
|
return record
|
|
|
|
async def test_basic(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = None
|
|
svc = ValidationTaskService(db)
|
|
result = await svc._record_to_dict(self.make_record())
|
|
assert result["status"] == "PASS"
|
|
assert result["screenshot_paths"] == []
|
|
|
|
async def test_with_source_title(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
source = MagicMock()
|
|
source.title = "Sales Dashboard"
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = source
|
|
svc = ValidationTaskService(db)
|
|
result = await svc._record_to_dict(self.make_record())
|
|
assert result["dashboard_title"] == "Sales Dashboard"
|
|
|
|
async def test_screenshot_paths_fallback(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = None
|
|
svc = ValidationTaskService(db)
|
|
result = await svc._record_to_dict(self.make_record(
|
|
raw_response='{"screenshot_paths": ["/path/img.png"]}',
|
|
))
|
|
assert result["screenshot_paths"] == ["/path/img.png"]
|
|
|
|
|
|
class TestResolveSources:
|
|
"""_resolve_sources — source resolution from input."""
|
|
|
|
async def test_no_sources(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._parse_superset_link = AsyncMock()
|
|
|
|
async def test_dashboard_id_sources(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._resolve_dashboard_title = AsyncMock(return_value="Sales Dashboard")
|
|
result = await svc._resolve_sources(None, ["42", "99"], "pol-1", "env-1")
|
|
assert len(result) == 2
|
|
assert result[0].type == "dashboard_id"
|
|
assert result[0].value == "42"
|
|
assert result[0].title == "Sales Dashboard"
|
|
assert result[0].status == "valid"
|
|
|
|
async def test_dashboard_url_sources(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._parse_superset_link = AsyncMock(return_value={
|
|
"dashboard_id": "42", "dataset_id": "ds-1",
|
|
"partial_recovery": False, "source_url": "http://superset/d/42",
|
|
})
|
|
svc._resolve_dashboard_title = AsyncMock(return_value="Sales Dashboard")
|
|
from src.schemas.validation import SourceInput
|
|
sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/42")]
|
|
result = await svc._resolve_sources(sources_input, None, "pol-1", "env-1")
|
|
assert len(result) == 1
|
|
assert result[0].type == "dashboard_url"
|
|
assert result[0].resolved_dashboard_id == "42"
|
|
|
|
async def test_partial_recovery_status(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._parse_superset_link = AsyncMock(return_value={
|
|
"dashboard_id": "42", "partial_recovery": True,
|
|
})
|
|
svc._resolve_dashboard_title = AsyncMock(return_value=None)
|
|
from src.schemas.validation import SourceInput
|
|
sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/42")]
|
|
result = await svc._resolve_sources(sources_input, None, "pol-1", "env-1")
|
|
assert result[0].status == "partial_recovery"
|
|
|
|
async def test_dashboard_id_type_source_input(self):
|
|
"""Cover _resolve_sources line 199-200: s.type == 'dashboard_id'."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._resolve_dashboard_title = AsyncMock(return_value="Dashboard 42")
|
|
from src.schemas.validation import SourceInput
|
|
sources_input = [SourceInput(type="dashboard_id", value="42")]
|
|
result = await svc._resolve_sources(sources_input, None, "pol-1", "env-1")
|
|
assert len(result) == 1
|
|
assert result[0].type == "dashboard_id"
|
|
assert result[0].resolved_dashboard_id == "42"
|
|
assert result[0].status == "valid"
|
|
|
|
def test_policy_to_response_with_sources(self):
|
|
"""Cover _policy_to_response lines 242-243: iterating sources."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
# Create mock policy with sources
|
|
policy_mock = MagicMock()
|
|
policy_mock.id = "pol-1"
|
|
policy_mock.name = "Test"
|
|
policy_mock.description = None
|
|
policy_mock.environment_id = "env-1"
|
|
policy_mock.is_active = True
|
|
policy_mock.dashboard_ids = []
|
|
policy_mock.provider_id = "prov-1"
|
|
policy_mock.schedule_days = []
|
|
policy_mock.window_start = None
|
|
policy_mock.window_end = None
|
|
policy_mock.notify_owners = None
|
|
policy_mock.custom_channels = None
|
|
policy_mock.alert_condition = "FAIL_ONLY"
|
|
policy_mock.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
policy_mock.updated_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
policy_mock.screenshot_enabled = True
|
|
policy_mock.logs_enabled = True
|
|
policy_mock.execute_chart_data = False
|
|
policy_mock.llm_batch_size = 1
|
|
policy_mock.policy_dashboard_concurrency_limit = 3
|
|
policy_mock.prompt_template = None
|
|
|
|
source = MagicMock()
|
|
source.id = "src-1"
|
|
source.type = "dashboard_id"
|
|
source.value = "42"
|
|
source.status = "valid"
|
|
source.title = "Sales Dashboard"
|
|
source.resolved_dashboard_id = "42"
|
|
source.last_error = None
|
|
source.last_checked_at = None
|
|
source.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
policy_mock.sources = [source]
|
|
|
|
# Mock last_run query
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None
|
|
|
|
svc.db = db
|
|
result = svc._policy_to_response(policy_mock)
|
|
assert result.id == "pol-1"
|
|
assert len(result.sources) == 1
|
|
|
|
async def test_record_to_dict_title_fallback(self):
|
|
"""Cover _record_to_dict lines 346-350: resolve title when source has no title."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
from src.models.llm import ValidationRecord as VR
|
|
|
|
record = MagicMock(spec=VR)
|
|
record.id = "rec-1"
|
|
record.run_id = "run-1"
|
|
record.policy_id = "pol-1"
|
|
record.dashboard_id = "42"
|
|
record.environment_id = "env-1"
|
|
record.status = "PASS"
|
|
record.summary = "OK"
|
|
record.issues = []
|
|
record.raw_response = None
|
|
record.screenshot_path = None
|
|
record.screenshot_paths = None
|
|
record.execution_path = None
|
|
record.dataset_health = None
|
|
record.chart_data_results = []
|
|
record.tab_screenshots = []
|
|
record.logs_sent_to_llm = []
|
|
record.token_usage = None
|
|
record.timings = None
|
|
record.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
|
|
# Source exists but without title → triggers fallback
|
|
source = MagicMock()
|
|
source.title = None
|
|
source.resolved_dashboard_id = "42"
|
|
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = source
|
|
|
|
svc = ValidationTaskService(db)
|
|
svc._resolve_dashboard_title = AsyncMock(return_value="Resolved Dashboard")
|
|
result = await svc._record_to_dict(record)
|
|
assert result["dashboard_title"] == "Resolved Dashboard"
|
|
svc._resolve_dashboard_title.assert_called_once_with("env-1", "42")
|
|
|
|
|
|
class TestApplyTimeField:
|
|
"""_apply_time_field — parse and set time fields."""
|
|
|
|
def test_field_not_in_update(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
policy = MagicMock()
|
|
svc._apply_time_field({}, policy, "window_start")
|
|
|
|
def test_field_in_update(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
policy = MagicMock()
|
|
svc._apply_time_field({"window_start": "10:00"}, policy, "window_start")
|
|
assert policy.window_start == time(10, 0)
|
|
|
|
class TestParseSupersetLink:
|
|
"""_parse_superset_link — Superset URL parsing."""
|
|
|
|
async def test_no_config_manager_raises(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock(), config_manager=None)
|
|
with pytest.raises(ValueError, match="Config manager not available"):
|
|
await svc._parse_superset_link("http://superset/d/1", "env-1")
|
|
|
|
async def test_environment_not_found_raises(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = None
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
with pytest.raises(ValueError, match="not found"):
|
|
await svc._parse_superset_link("http://superset/d/1", "env-unknown")
|
|
|
|
async def test_successful_parse(self):
|
|
"""Cover lines 129-133: happy path with valid extractor result."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
env = MagicMock()
|
|
cm.get_environment.return_value = env
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
|
|
with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor") as MockExtractor:
|
|
mock_extractor = MagicMock()
|
|
parsed_result = MagicMock()
|
|
parsed_result.source_url = "http://superset/d/42"
|
|
parsed_result.dataset_ref = "dataset1"
|
|
parsed_result.dataset_id = "ds-1"
|
|
parsed_result.dashboard_id = "42"
|
|
parsed_result.chart_id = None
|
|
parsed_result.resource_type = "dashboard"
|
|
parsed_result.partial_recovery = False
|
|
parsed_result.unresolved_references = set()
|
|
mock_extractor.parse_superset_link = AsyncMock(return_value=parsed_result)
|
|
MockExtractor.return_value = mock_extractor
|
|
|
|
result = await svc._parse_superset_link("http://superset/d/42", "env-1")
|
|
|
|
assert result["source_url"] == "http://superset/d/42"
|
|
assert result["dashboard_id"] == "42"
|
|
assert result["partial_recovery"] is False
|
|
|
|
|
|
class TestResolveDashboardTitle:
|
|
"""_resolve_dashboard_title — fetch title from Superset API."""
|
|
|
|
async def test_no_config_manager_returns_none(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock(), config_manager=None)
|
|
result = await svc._resolve_dashboard_title("env-1", "42")
|
|
assert result is None
|
|
|
|
async def test_environment_not_found_returns_none(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = None
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
result = await svc._resolve_dashboard_title("env-1", "42")
|
|
assert result is None
|
|
|
|
async def test_api_exception_returns_none(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = MagicMock()
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
with patch("src.core.superset_client.SupersetClient") as MockClient:
|
|
mock_client = MagicMock()
|
|
mock_client.get_dashboard = AsyncMock(side_effect=Exception("API error"))
|
|
MockClient.return_value = mock_client
|
|
result = await svc._resolve_dashboard_title("env-1", "42")
|
|
assert result is None
|
|
|
|
async def test_result_with_dashboard_title(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = MagicMock()
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
with patch("src.core.superset_client.SupersetClient") as MockClient:
|
|
mock_client = MagicMock()
|
|
mock_client.get_dashboard = AsyncMock(return_value={
|
|
"result": {"dashboard_title": "Sales Dashboard"},
|
|
})
|
|
MockClient.return_value = mock_client
|
|
result = await svc._resolve_dashboard_title("env-1", "42")
|
|
assert result == "Sales Dashboard"
|
|
|
|
async def test_result_with_title_field(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = MagicMock()
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
with patch("src.core.superset_client.SupersetClient") as MockClient:
|
|
mock_client = MagicMock()
|
|
mock_client.get_dashboard = AsyncMock(return_value={
|
|
"result": {"title": "Revenue Dashboard"},
|
|
})
|
|
MockClient.return_value = mock_client
|
|
result = await svc._resolve_dashboard_title("env-1", "42")
|
|
assert result == "Revenue Dashboard"
|
|
|
|
async def test_result_no_title_returns_none(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = MagicMock()
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
with patch("src.core.superset_client.SupersetClient") as MockClient:
|
|
mock_client = MagicMock()
|
|
mock_client.get_dashboard = AsyncMock(return_value={"result": {}})
|
|
MockClient.return_value = mock_client
|
|
result = await svc._resolve_dashboard_title("env-1", "42")
|
|
assert result is None
|
|
|
|
|
|
class TestRecordToDictExceptionHandler:
|
|
"""_record_to_dict — exception handler at lines 351-352."""
|
|
|
|
async def test_exception_in_title_resolution_safe(self):
|
|
"""Cover lines 351-352: except Exception: pass."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
from src.models.llm import ValidationRecord as VR
|
|
|
|
record = MagicMock(spec=VR)
|
|
record.id = "rec-1"
|
|
record.run_id = "run-1"
|
|
record.policy_id = "pol-1"
|
|
record.dashboard_id = "42"
|
|
record.environment_id = "env-1"
|
|
record.status = "PASS"
|
|
record.summary = "OK"
|
|
record.issues = []
|
|
record.raw_response = None
|
|
record.screenshot_path = None
|
|
record.screenshot_paths = []
|
|
record.execution_path = None
|
|
record.dataset_health = None
|
|
record.chart_data_results = []
|
|
record.tab_screenshots = []
|
|
record.logs_sent_to_llm = []
|
|
record.token_usage = None
|
|
record.timings = None
|
|
record.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
|
|
db = MagicMock()
|
|
# Make the source query throw an exception to trigger except block
|
|
db.query.return_value.filter.return_value.first.side_effect = Exception("DB error")
|
|
svc = ValidationTaskService(db)
|
|
result = await svc._record_to_dict(record)
|
|
assert result["status"] == "PASS"
|
|
assert result["dashboard_title"] == "42" # falls back to dashboard_id
|
|
# #endregion Test.ValidationTaskService.Helpers
|