# #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 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 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 = svc._record_to_dict(self.make_record()) assert result["status"] == "PASS" assert result["screenshot_paths"] == [] 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 = svc._record_to_dict(self.make_record()) assert result["dashboard_title"] == "Sales Dashboard" 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 = 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.""" def test_no_sources(self): from src.services.validation_service import ValidationTaskService svc = ValidationTaskService(MagicMock()) svc._parse_superset_link = MagicMock() svc._resolve_dashboard_title = MagicMock(return_value=None) result = svc._resolve_sources(None, None, "pol-1", "env-1") assert result == [] def test_dashboard_id_sources(self): from src.services.validation_service import ValidationTaskService svc = ValidationTaskService(MagicMock()) svc._resolve_dashboard_title = MagicMock(return_value="Sales Dashboard") result = 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" def test_dashboard_url_sources(self): from src.services.validation_service import ValidationTaskService svc = ValidationTaskService(MagicMock()) svc._parse_superset_link = MagicMock(return_value={ "dashboard_id": "42", "dataset_id": "ds-1", "partial_recovery": False, "source_url": "http://superset/d/42", }) svc._resolve_dashboard_title = MagicMock(return_value="Sales Dashboard") from src.schemas.validation import SourceInput sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/42")] result = 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" def test_partial_recovery_status(self): from src.services.validation_service import ValidationTaskService svc = ValidationTaskService(MagicMock()) svc._parse_superset_link = MagicMock(return_value={ "dashboard_id": "42", "partial_recovery": True, }) svc._resolve_dashboard_title = MagicMock(return_value=None) from src.schemas.validation import SourceInput sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/42")] result = svc._resolve_sources(sources_input, None, "pol-1", "env-1") assert result[0].status == "partial_recovery" 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 = MagicMock(return_value="Dashboard 42") from src.schemas.validation import SourceInput sources_input = [SourceInput(type="dashboard_id", value="42")] result = 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 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 = MagicMock(return_value="Resolved Dashboard") result = 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.""" 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"): svc._parse_superset_link("http://superset/d/1", "env-1") 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"): svc._parse_superset_link("http://superset/d/1", "env-unknown") 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.return_value = parsed_result MockExtractor.return_value = mock_extractor result = 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.""" def test_no_config_manager_returns_none(self): from src.services.validation_service import ValidationTaskService svc = ValidationTaskService(MagicMock(), config_manager=None) result = svc._resolve_dashboard_title("env-1", "42") assert result is None 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 = svc._resolve_dashboard_title("env-1", "42") assert result is None 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.side_effect = Exception("API error") MockClient.return_value = mock_client result = svc._resolve_dashboard_title("env-1", "42") assert result is None 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.return_value = { "result": {"dashboard_title": "Sales Dashboard"}, } MockClient.return_value = mock_client result = svc._resolve_dashboard_title("env-1", "42") assert result == "Sales Dashboard" 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.return_value = { "result": {"title": "Revenue Dashboard"}, } MockClient.return_value = mock_client result = svc._resolve_dashboard_title("env-1", "42") assert result == "Revenue Dashboard" 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.return_value = {"result": {}} MockClient.return_value = mock_client result = svc._resolve_dashboard_title("env-1", "42") assert result is None class TestRecordToDictExceptionHandler: """_record_to_dict — exception handler at lines 351-352.""" 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 = svc._record_to_dict(record) assert result["status"] == "PASS" assert result["dashboard_title"] == "42" # falls back to dashboard_id # #endregion Test.ValidationTaskService.Helpers