Группы исправлений: - Группа 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
375 lines
14 KiB
Python
375 lines
14 KiB
Python
# #region Test.ValidationTaskService.Triggers [C:3] [TYPE Module] [SEMANTICS test,validation,trigger,run,detail]
|
|
# @BRIEF Tests for validation_service.py — trigger runs, list all runs, 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):
|
|
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):
|
|
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})
|
|
m.configure_mock(**{"filter.return_value.first.return_value": first_result})
|
|
m.configure_mock(**{"filter.return_value.count.return_value": len(policies)})
|
|
m.configure_mock(**{"filter.return_value.order_by.return_value.first.return_value": None})
|
|
m.configure_mock(**{"filter.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value": policies})
|
|
return m
|
|
|
|
|
|
class TestTriggerRun:
|
|
"""trigger_run — create run and spawn task."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_not_found(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"):
|
|
await svc.trigger_run("nonexistent", AsyncMock())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_existing_running_run(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
policy = make_policy(dashboard_ids=["42", "99"])
|
|
db = MagicMock()
|
|
mock_q = MagicMock()
|
|
mock_q.filter.return_value.first.side_effect = [policy, MagicMock()]
|
|
db.query.return_value = mock_q
|
|
svc = ValidationTaskService(db)
|
|
with pytest.raises(ValueError, match="already has a running run"):
|
|
await svc.trigger_run("pol-1", AsyncMock())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_worker_limit_reached(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
policy = make_policy(dashboard_ids=["42"], screenshot_enabled=True)
|
|
db = MagicMock()
|
|
call_log = {"count": 0}
|
|
|
|
def query_fn(model):
|
|
if model.__name__ == "ValidationPolicy":
|
|
m = MagicMock()
|
|
m.filter.return_value.first.return_value = policy
|
|
return m
|
|
elif model.__name__ == "ValidationRun":
|
|
m = MagicMock()
|
|
def run_filter(*a, **kw):
|
|
call_log["count"] += 1
|
|
fm = MagicMock()
|
|
if call_log["count"] == 1:
|
|
fm.first.return_value = None
|
|
else:
|
|
fm.count.return_value = 5
|
|
return fm
|
|
m.filter = run_filter
|
|
return m
|
|
else:
|
|
m = MagicMock()
|
|
m.filter.return_value.all.return_value = []
|
|
return m
|
|
|
|
db.query = query_fn
|
|
cm = MagicMock()
|
|
cm.configure_mock(**{"config.settings.global_validation_worker_limit": 3})
|
|
svc = ValidationTaskService(db, config_manager=cm)
|
|
with pytest.raises(ValueError, match="worker limit"):
|
|
await svc.trigger_run("pol-1", AsyncMock())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_dashboard_ids_raises(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
policy = make_policy(dashboard_ids=[], screenshot_enabled=True)
|
|
db = MagicMock()
|
|
|
|
def query_fn(model):
|
|
m = MagicMock()
|
|
if model.__name__ == "ValidationPolicy":
|
|
m.filter.return_value.first.return_value = policy
|
|
elif model.__name__ == "ValidationRun":
|
|
m.filter.return_value.first.return_value = None
|
|
m.filter.return_value.count.return_value = 0
|
|
elif model.__name__ == "ValidationSource":
|
|
m.filter.return_value.all.return_value = []
|
|
return m
|
|
|
|
db.query = query_fn
|
|
cm = MagicMock()
|
|
cm.configure_mock(**{"config.settings.global_validation_worker_limit": 5})
|
|
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
|
|
with pytest.raises(ValueError, match="no dashboard IDs"):
|
|
await svc.trigger_run("pol-1", AsyncMock())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_successful_trigger(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
policy = make_policy(
|
|
dashboard_ids=["42", "99"],
|
|
screenshot_enabled=True, logs_enabled=True,
|
|
execute_chart_data=False, llm_batch_size=1,
|
|
policy_dashboard_concurrency_limit=3, prompt_template=None,
|
|
)
|
|
db = MagicMock()
|
|
|
|
def query_fn(model):
|
|
m = MagicMock()
|
|
if model.__name__ == "ValidationSource":
|
|
m.filter.return_value.all.return_value = []
|
|
elif model.__name__ == "ValidationRun":
|
|
m.filter.return_value.first.return_value = None
|
|
m.filter.return_value.count.return_value = 0
|
|
elif model.__name__ == "ValidationPolicy":
|
|
m.filter.return_value.first.return_value = policy
|
|
return m
|
|
|
|
db.query = query_fn
|
|
cm = MagicMock()
|
|
cm.configure_mock(**{"config.settings.global_validation_worker_limit": 5})
|
|
svc = ValidationTaskService(db, config_manager=cm, username="tester")
|
|
|
|
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
|
|
|
|
task_manager = AsyncMock()
|
|
task = MagicMock()
|
|
task.id = "spawned-task-1"
|
|
task_manager.create_task.return_value = task
|
|
result = await svc.trigger_run("pol-1", task_manager)
|
|
|
|
assert result["task_id"] == "spawned-task-1"
|
|
assert "run_id" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trigger_with_sources_and_no_dashboard_ids(self):
|
|
"""Cover trigger_run line 641: sources present with resolved_dashboard_ids."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
policy = make_policy(
|
|
dashboard_ids=[],
|
|
screenshot_enabled=True, logs_enabled=True,
|
|
execute_chart_data=False, llm_batch_size=1,
|
|
policy_dashboard_concurrency_limit=3, prompt_template=None,
|
|
)
|
|
db = MagicMock()
|
|
|
|
# Create source mock with resolved_dashboard_id
|
|
source_mock = MagicMock()
|
|
source_mock.resolved_dashboard_id = "42"
|
|
source_mock.value = "http://superset/d/42"
|
|
|
|
def query_fn(model):
|
|
m = MagicMock()
|
|
if model.__name__ == "ValidationSource":
|
|
m.filter.return_value.all.return_value = [source_mock]
|
|
elif model.__name__ == "ValidationRun":
|
|
m.filter.return_value.first.return_value = None
|
|
m.filter.return_value.count.return_value = 0
|
|
elif model.__name__ == "ValidationPolicy":
|
|
m.filter.return_value.first.return_value = policy
|
|
return m
|
|
|
|
db.query = query_fn
|
|
cm = MagicMock()
|
|
cm.configure_mock(**{"config.settings.global_validation_worker_limit": 5})
|
|
svc = ValidationTaskService(db, config_manager=cm, username="tester")
|
|
|
|
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
|
|
|
|
task_manager = AsyncMock()
|
|
task = MagicMock()
|
|
task.id = "spawned-task-1"
|
|
task_manager.create_task.return_value = task
|
|
result = await svc.trigger_run("pol-1", task_manager)
|
|
|
|
assert result["task_id"] == "spawned-task-1"
|
|
assert "run_id" in result
|
|
|
|
|
|
class TestListAllRuns:
|
|
"""list_all_runs — cross-task filtering."""
|
|
|
|
def make_record(self, **overrides):
|
|
from src.models.llm import ValidationRecord as VR
|
|
r = MagicMock(spec=VR)
|
|
r.id = "rec-1"
|
|
r.run_id = "run-1"
|
|
r.policy_id = "pol-1"
|
|
r.dashboard_id = "42"
|
|
r.dashboard_title = "Sales"
|
|
r.environment_id = "env-1"
|
|
r.status = "PASS"
|
|
r.summary = "All good"
|
|
r.issues = []
|
|
r.raw_response = None
|
|
r.screenshot_path = None
|
|
r.screenshot_paths = None
|
|
r.execution_path = None
|
|
r.dataset_health = None
|
|
r.chart_data_results = []
|
|
r.tab_screenshots = []
|
|
r.logs_sent_to_llm = []
|
|
r.token_usage = None
|
|
r.timings = None
|
|
r.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
for k, v in overrides.items():
|
|
setattr(r, k, v)
|
|
return r
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_filters(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
record = self.make_record()
|
|
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": [record]})
|
|
m.configure_mock(**{"filter.return_value.count.return_value": 1})
|
|
m.configure_mock(**{"filter.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value": [record]})
|
|
db.query.return_value = m
|
|
svc = ValidationTaskService(db)
|
|
total, records = await svc.list_all_runs()
|
|
assert total == 1
|
|
assert len(records) == 1
|
|
assert records[0]["status"] == "PASS"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_by_status(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
svc = ValidationTaskService(db)
|
|
await svc.list_all_runs(status="FAIL")
|
|
db.query.return_value.filter.assert_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_by_environment(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
svc = ValidationTaskService(db)
|
|
await svc.list_all_runs(environment_id="env-1")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_by_dashboard_id(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
svc = ValidationTaskService(db)
|
|
await svc.list_all_runs(dashboard_id="42")
|
|
|
|
|
|
class TestGetRunDetail:
|
|
"""get_run_detail — run + records."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_not_found(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"):
|
|
await svc.get_run_detail("nonexistent")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_success(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 = 0
|
|
run.pass_count = 0
|
|
run.warn_count = 0
|
|
run.fail_count = 0
|
|
run.unknown_count = 0
|
|
run.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
|
|
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.dashboard_title = "Sales"
|
|
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)
|
|
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = run
|
|
db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [record]
|
|
svc = ValidationTaskService(db)
|
|
result = await svc.get_run_detail("run-1")
|
|
assert result["run"]["status"] == "completed"
|
|
assert len(result["records"]) == 1
|
|
assert result["records"][0]["status"] == "PASS"
|
|
# #endregion Test.ValidationTaskService.Triggers
|