Группы исправлений: - Группа 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
427 lines
17 KiB
Python
427 lines
17 KiB
Python
# #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 AsyncMock, 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."""
|
|
|
|
async 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 = AsyncMock(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 = await svc.create_task(payload)
|
|
|
|
assert db.commit.called
|
|
svc._resolve_sources.assert_called_once()
|
|
|
|
async 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 = AsyncMock(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 = await svc.create_task(payload)
|
|
|
|
assert db.commit.called
|
|
|
|
|
|
class TestUpdateTask:
|
|
"""update_task — partial update with re-validation."""
|
|
|
|
async 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"):
|
|
await svc.update_task("nonexistent", payload)
|
|
|
|
async 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 = await svc.update_task("pol-1", payload)
|
|
assert db.commit.called
|
|
|
|
async 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 = await svc.update_task("pol-1", payload)
|
|
assert db.commit.called
|
|
|
|
async 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 = await svc.update_task("pol-1", payload)
|
|
assert db.commit.called
|
|
|
|
async 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 = AsyncMock(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 = await 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
|