# #region Test.ValidationTaskService.Crud [C:3] [TYPE Module] [SEMANTICS test,validation,crud,task,run] # @BRIEF Tests for validation_service.py — CRUD: get/list/delete tasks, trigger runs, run history, run detail. # @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 import pytest def make_policy(**overrides): """Create a mock ValidationPolicy.""" policy = MagicMock() policy.id = "pol-1" policy.name = "Test Task" policy.description = None policy.environment_id = "env-1" policy.is_active = True policy.dashboard_ids = [] policy.provider_id = "prov-1" policy.schedule_days = None policy.window_start = None policy.window_end = None policy.notify_owners = None policy.custom_channels = None policy.alert_condition = None policy.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC) policy.updated_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC) policy.sources = [] policy.screenshot_enabled = True policy.logs_enabled = True policy.execute_chart_data = False policy.llm_batch_size = 1 policy.policy_dashboard_concurrency_limit = 3 policy.prompt_template = None for k, v in overrides.items(): setattr(policy, k, v) return policy def make_valid_query_mock(policies=None): """Build a SQLAlchemy query mock chain that works for both filtered and unfiltered paths.""" m = MagicMock() policies = policies or [] first_result = policies[0] if policies else None m.configure_mock(**{"count.return_value": len(policies)}) m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": policies}) # query.filter(...).first() — returns first policy m.configure_mock(**{"filter.return_value.first.return_value": first_result}) # query.filter(...).count() m.configure_mock(**{"filter.return_value.count.return_value": len(policies)}) # query.filter(...).order_by(...).first() — used by _policy_to_response for last_run m.configure_mock(**{"filter.return_value.order_by.return_value.first.return_value": None}) # query.filter(...).order_by(...).offset(...).limit(...).all() m.configure_mock(**{"filter.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value": policies}) return m class TestGetTask: """get_task — find by id.""" def test_not_found_raises(self): from src.services.validation_service import ValidationTaskService db = MagicMock() db.query.return_value.filter.return_value.first.return_value = None svc = ValidationTaskService(db) with pytest.raises(ValueError, match="not found"): svc.get_task("nonexistent") def test_found(self): from src.services.validation_service import ValidationTaskService policy = make_policy() db = MagicMock() db.query = lambda model: make_valid_query_mock() if model.__name__ == "ValidationRun" else make_valid_query_mock([policy]) svc = ValidationTaskService(db) result = svc.get_task("pol-1") assert result.id == "pol-1" class TestListTasks: """list_tasks — filtering and pagination.""" def _make_qm(self, policies=None): """Build a query mock that works for both filtered and unfiltered paths.""" return make_valid_query_mock(policies) def test_no_filters(self): from src.services.validation_service import ValidationTaskService policy = make_policy() db = MagicMock() db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy]) svc = ValidationTaskService(db) total, tasks = svc.list_tasks() assert total == 1 assert len(tasks) == 1 assert tasks[0].id == "pol-1" def test_filter_active(self): from src.services.validation_service import ValidationTaskService policy = make_policy() db = MagicMock() db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy]) svc = ValidationTaskService(db) total, tasks = svc.list_tasks(is_active=True) assert total == 1 assert len(tasks) == 1 def test_filter_environment_id(self): """Cover list_tasks line 398: environment_id filter.""" from src.services.validation_service import ValidationTaskService policy = make_policy() db = MagicMock() db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy]) svc = ValidationTaskService(db) total, tasks = svc.list_tasks(environment_id="env-1") assert total == 1 def test_filter_search(self): """Cover list_tasks line 400: search/ilike filter.""" from src.services.validation_service import ValidationTaskService policy = make_policy() db = MagicMock() db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy]) svc = ValidationTaskService(db) total, tasks = svc.list_tasks(search="Test") assert total == 1 class TestDeleteTask: """delete_task — with/without runs.""" def test_not_found_raises(self): from src.services.validation_service import ValidationTaskService db = MagicMock() db.query.return_value.filter.return_value.first.return_value = None svc = ValidationTaskService(db) with pytest.raises(ValueError, match="not found"): svc.delete_task("nonexistent") def test_delete_without_runs(self): from src.services.validation_service import ValidationTaskService policy = make_policy() db = MagicMock() db.query.return_value.filter.return_value.first.return_value = policy svc = ValidationTaskService(db) svc.delete_task("pol-1", delete_runs=False) db.delete.assert_called_once_with(policy) db.commit.assert_called_once() def test_delete_with_runs(self): from src.services.validation_service import ValidationTaskService policy = make_policy() db = MagicMock() db.query.return_value.filter.return_value.first.return_value = policy db.query.return_value.filter.return_value.all.return_value = [("run-1",)] svc = ValidationTaskService(db) svc.delete_task("pol-1", delete_runs=True) db.delete.assert_called_once_with(policy) db.commit.assert_called_once() class TestCreateTask: """create_task — full CRUD with provider/environment validation.""" def test_creates_policy_with_sources(self): from src.services.validation_service import ValidationTaskService from src.schemas.validation import ValidationTaskCreate, SourceInput db = MagicMock() db.add = MagicMock() db.flush = MagicMock() db.commit = MagicMock() db.refresh = MagicMock() with patch("src.services.validation_service.LLMProviderService") as mock_cls: mock_svc = MagicMock() provider = MagicMock() provider.is_active = True provider.is_multimodal = True mock_svc.get_provider.return_value = provider mock_cls.return_value = mock_svc cm = MagicMock() cm.get_environment.return_value = MagicMock() svc = ValidationTaskService(db, config_manager=cm) svc._resolve_sources = MagicMock(return_value=[ MagicMock(type="dashboard_id", value="42", resolved_dashboard_id="42", status="valid", title=None), ]) # Mock _policy_to_response to avoid ORM serialization issues svc._policy_to_response = MagicMock(return_value=MagicMock()) payload = MagicMock(spec=ValidationTaskCreate) payload.name = "New Task" payload.description = "Test desc" payload.environment_id = "env-1" payload.is_active = True payload.dashboard_ids = ["42"] payload.provider_id = "prov-1" payload.schedule_days = ["MON", "WED", "FRI"] payload.window_start = None payload.window_end = None payload.notify_owners = True payload.custom_channels = None payload.alert_condition = "FAIL_ONLY" payload.screenshot_enabled = True payload.logs_enabled = True payload.execute_chart_data = False payload.llm_batch_size = 1 payload.policy_dashboard_concurrency_limit = 3 payload.prompt_template = None payload.sources = None result = svc.create_task(payload) assert db.commit.called svc._resolve_sources.assert_called_once() def test_creates_with_dashboard_url_sources(self): from src.services.validation_service import ValidationTaskService from src.schemas.validation import ValidationTaskCreate db = MagicMock() db.add = MagicMock() db.flush = MagicMock() db.commit = MagicMock() db.refresh = MagicMock() with patch("src.services.validation_service.LLMProviderService") as mock_cls: mock_svc = MagicMock() provider = MagicMock() provider.is_active = True provider.is_multimodal = False # screenshot_enabled=False mock_svc.get_provider.return_value = provider mock_cls.return_value = mock_svc cm = MagicMock() cm.get_environment.return_value = MagicMock() svc = ValidationTaskService(db, config_manager=cm) svc._resolve_sources = MagicMock(return_value=[ MagicMock(type="dashboard_url", value="http://superset/d/42", resolved_dashboard_id="42", status="valid", title=None), ]) svc._policy_to_response = MagicMock(return_value=MagicMock()) payload = MagicMock(spec=ValidationTaskCreate) payload.name = "URL Task" payload.description = None payload.environment_id = "env-1" payload.is_active = True payload.dashboard_ids = None payload.provider_id = "prov-1" payload.schedule_days = [] payload.window_start = "09:00" payload.window_end = "18:00" payload.notify_owners = None payload.custom_channels = [{"type": "SLACK", "target": "#alerts"}] payload.alert_condition = "ALWAYS" payload.screenshot_enabled = False payload.logs_enabled = True payload.execute_chart_data = True payload.llm_batch_size = 3 payload.policy_dashboard_concurrency_limit = 5 payload.prompt_template = "Check {{dashboard}}" payload.sources = None result = svc.create_task(payload) assert db.commit.called class TestUpdateTask: """update_task — partial update with re-validation.""" def test_not_found_raises(self): from src.services.validation_service import ValidationTaskService from src.schemas.validation import ValidationTaskUpdate db = MagicMock() db.query.return_value.filter.return_value.first.return_value = None svc = ValidationTaskService(db) payload = MagicMock(spec=ValidationTaskUpdate) payload.model_dump.return_value = {"name": "Updated"} with pytest.raises(ValueError, match="not found"): svc.update_task("nonexistent", payload) def test_sources_update_with_none_is_skipped(self): """Cover _apply_sources_update line 502: sources_data is None.""" from src.services.validation_service import ValidationTaskService from src.schemas.validation import ValidationTaskUpdate policy = make_policy() db = MagicMock() db.query.return_value.filter.return_value.first.return_value = policy svc = ValidationTaskService(db, config_manager=MagicMock()) payload = MagicMock(spec=ValidationTaskUpdate) payload.model_dump.return_value = {"sources": None} result = svc.update_task("pol-1", payload) assert db.commit.called def test_updates_name_only(self): from src.services.validation_service import ValidationTaskService from src.schemas.validation import ValidationTaskUpdate policy = make_policy() db = MagicMock() db.query.return_value.filter.return_value.first.return_value = policy svc = ValidationTaskService(db, config_manager=MagicMock()) payload = MagicMock(spec=ValidationTaskUpdate) payload.model_dump.return_value = {"name": "Updated Name"} result = svc.update_task("pol-1", payload) assert db.commit.called def test_updates_provider_and_environment(self): from src.services.validation_service import ValidationTaskService from src.schemas.validation import ValidationTaskUpdate policy = make_policy() db = MagicMock() db.query.return_value.filter.return_value.first.return_value = policy cm = MagicMock() cm.get_environment.return_value = MagicMock() svc = ValidationTaskService(db, config_manager=cm) with patch("src.services.validation_service.LLMProviderService") as mock_cls: mock_svc = MagicMock() provider = MagicMock() provider.is_active = True provider.is_multimodal = True mock_svc.get_provider.return_value = provider mock_cls.return_value = mock_svc payload = MagicMock(spec=ValidationTaskUpdate) payload.model_dump.return_value = { "provider_id": "prov-2", "environment_id": "env-2", "screenshot_enabled": True, "window_start": "10:00", "window_end": "20:00", } result = svc.update_task("pol-1", payload) assert db.commit.called def test_updates_with_sources(self): from src.services.validation_service import ValidationTaskService from src.schemas.validation import ValidationTaskUpdate, SourceInput policy = make_policy() db = MagicMock() db.query.return_value.filter.return_value.first.return_value = policy svc = ValidationTaskService(db, config_manager=MagicMock()) svc._resolve_sources = MagicMock(return_value=[ MagicMock(type="dashboard_id", value="99", resolved_dashboard_id="99", status="valid", title="New"), ]) payload = MagicMock(spec=ValidationTaskUpdate) payload.model_dump.return_value = { "name": "Updated with sources", "sources": [SourceInput(type="dashboard_id", value="99")], } result = svc.update_task("pol-1", payload) assert db.commit.called svc._resolve_sources.assert_called_once() class TestGetRunHistory: """get_run_history — paginated run list for a policy.""" def test_empty(self): from src.services.validation_service import ValidationTaskService from src.models.llm import ValidationRun db = MagicMock() m = MagicMock() m.configure_mock(**{"count.return_value": 0}) m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": []}) db.query.return_value.filter.return_value = m svc = ValidationTaskService(db) total, runs = svc.get_run_history("pol-1") assert total == 0 assert runs == [] def test_with_runs(self): from src.services.validation_service import ValidationTaskService run = MagicMock() run.id = "run-1" run.policy_id = "pol-1" run.task_id = "task-1" run.status = "completed" run.trigger = "manual" run.started_at = None run.finished_at = None run.dashboard_count = 3 run.pass_count = 2 run.warn_count = 1 run.fail_count = 0 run.unknown_count = 0 run.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC) db = MagicMock() m = MagicMock() m.configure_mock(**{"count.return_value": 1}) m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": [run]}) db.query.return_value.filter.return_value = m svc = ValidationTaskService(db) total, runs = svc.get_run_history("pol-1") assert total == 1 assert len(runs) == 1 assert runs[0]["status"] == "completed" # #endregion Test.ValidationTaskService.Crud