Files
ss-tools/backend/tests/services/test_validation_service_branches.py
busya 488a8f349b test(backend): raise coverage to 95%+ statements and branches (97.8%/95.0%)
- ~60 new/extended test files across api, core, plugins, services, schemas:
  routes, superset clients, task_manager, lineage, git, translate,
  dashboard-testing, load-testing, migration, llm_analysis, scheduler, ssl
- .coveragerc: enable branch coverage; exclude src/__tests__ (test files)
  and src/scripts (CLI/ops tools) from the denominator
- bug fixes found while testing:
  * settings: PUT /settings/reports registered under duplicated prefix
  * schemas/lineage: FleetReportDTO missing run_status (route always 500)
  * dashboard_testing/baseline_inheritance: visual entry read wrong field
  * superset_client/_databases: logger extra name shadowed LogRecord attr
  * routes/datasets: _yaml_string_paths recursion without yield from
  * translate/sql_generator: restore explicit-type timestamp contract
  * baseline_catalog: remove unreachable dashboard_id fallback
- conftest fixes: pytest_plugins to rootdir conftest (pytest 9), test
  filename collision, TMPDIR-safe integration fixtures
2026-08-19 17:14:32 +03:00

284 lines
12 KiB
Python

# #region Test.ValidationTaskService.Branches [C:3] [TYPE Module] [SEMANTICS test,validation,branches,coverage]
# @BRIEF Coverage tests for validation_service.py remaining branches — non-list screenshot_paths,
# source resolution fallbacks, record title fallback, time-field parse failure, update
# unknown-key skip, delete-with-runs without run IDs, trigger without provider.
# @RELATION BINDS_TO -> [Services.ValidationService]
# @TEST_EDGE: screenshot_paths_not_list -> non-list screenshot_paths yields empty list
# @TEST_EDGE: dashboard_url_without_id -> no resolved id and no title fetch
# @TEST_EDGE: unknown_source_type -> unsupported source type yields no resolved id
# @TEST_EDGE: no_sources_no_dashboards -> empty source list returned
# @TEST_EDGE: title_resolve_returns_none -> dashboard_title falls back to dashboard_id
# @TEST_EDGE: invalid_time_field -> no setattr when time parse fails
# @TEST_EDGE: unknown_update_key -> skipped when policy lacks the attribute
# @TEST_EDGE: delete_runs_without_runs -> record deletion skipped when no runs exist
# @TEST_EDGE: trigger_without_provider -> provider validation skipped when provider_id empty
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import UTC, datetime
import pytest
class _PolicyStub:
"""Bare policy stub — unknown attributes must NOT auto-exist (unlike MagicMock)."""
def __init__(self, **attrs):
for k, v in attrs.items():
setattr(self, k, v)
def _make_record(**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
class TestExtractScreenshotPaths:
"""_extract_screenshot_paths — non-list screenshot_paths value."""
def test_screenshot_paths_not_a_list(self):
from src.services.validation_service import _extract_screenshot_paths
result = _extract_screenshot_paths('{"screenshot_paths": "single-path.png"}')
assert result == []
def test_screenshot_paths_null_value(self):
from src.services.validation_service import _extract_screenshot_paths
result = _extract_screenshot_paths('{"screenshot_paths": null}')
assert result == []
class TestResolveSourcesBranches:
"""_resolve_sources — remaining fallback branches."""
async def test_dashboard_url_without_dashboard_id(self):
"""dashboard_url parsed without dashboard_id -> no resolved id, no title fetch."""
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import SourceInput
svc = ValidationTaskService(MagicMock())
svc._parse_superset_link = AsyncMock(return_value={
"dashboard_id": None,
"partial_recovery": False,
})
svc._resolve_dashboard_title = AsyncMock(return_value="must-not-be-called")
sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/abc")]
result = await svc._resolve_sources(sources_input, None, "pol-1", "env-1")
assert len(result) == 1
assert result[0].resolved_dashboard_id is None
assert result[0].status == "valid"
svc._resolve_dashboard_title.assert_not_called()
async def test_unknown_source_type(self):
"""Source type neither dashboard_url nor dashboard_id -> no resolved id."""
from src.services.validation_service import ValidationTaskService
svc = ValidationTaskService(MagicMock())
svc._parse_superset_link = AsyncMock()
svc._resolve_dashboard_title = AsyncMock(return_value=None)
source_input = SimpleNamespace(type="dataset", value="ds-1")
result = await svc._resolve_sources([source_input], None, "pol-1", "env-1")
assert len(result) == 1
assert result[0].type == "dataset"
assert result[0].resolved_dashboard_id is None
svc._parse_superset_link.assert_not_called()
async def test_no_sources_no_dashboards(self):
"""Neither sources_input nor dashboard_ids -> empty list."""
from src.services.validation_service import ValidationTaskService
svc = ValidationTaskService(MagicMock())
result = await svc._resolve_sources(None, None, "pol-1", "env-1")
assert result == []
class TestRecordToDictTitleFallbackNone:
"""_record_to_dict — title resolution returns None -> dashboard_id fallback."""
async def test_title_resolve_returns_none(self):
from src.services.validation_service import ValidationTaskService
record = _make_record()
source = MagicMock()
source.title = None
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = source
svc = ValidationTaskService(db)
svc._resolve_dashboard_title = AsyncMock(return_value=None)
result = await svc._record_to_dict(record)
assert result["dashboard_title"] == "42"
svc._resolve_dashboard_title.assert_called_once_with("env-1", "42")
assert source.title is None # fallback path must not write a resolved title
async def test_title_resolve_returns_none_no_env(self):
"""record without environment_id -> no Superset fallback call."""
from src.services.validation_service import ValidationTaskService
record = _make_record(environment_id=None)
source = MagicMock()
source.title = None
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = source
svc = ValidationTaskService(db)
svc._resolve_dashboard_title = AsyncMock(return_value="must-not-be-called")
result = await svc._record_to_dict(record)
assert result["dashboard_title"] == "42"
svc._resolve_dashboard_title.assert_not_called()
class TestApplyTimeFieldParseFailure:
"""_apply_time_field — field present but unparseable -> no setattr."""
def test_invalid_time_does_not_set(self):
from src.services.validation_service import ValidationTaskService
svc = ValidationTaskService(MagicMock())
policy = MagicMock()
policy.window_start = None
svc._apply_time_field({"window_start": "not-a-time"}, policy, "window_start")
assert policy.window_start is None
def test_invalid_time_does_not_set_with_partial_minutes(self):
"""'09' (missing minutes) also fails parsing -> no setattr."""
from src.services.validation_service import ValidationTaskService
svc = ValidationTaskService(MagicMock())
policy = MagicMock()
policy.window_start = None
svc._apply_time_field({"window_start": "09"}, policy, "window_start")
assert policy.window_start is None
class TestUpdateTaskUnknownKey:
"""update_task — update data key not present on policy is skipped."""
async def test_update_skips_unknown_key(self):
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import ValidationTaskUpdate
policy = _PolicyStub(id="pol-1", name="Test Task")
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = policy
svc = ValidationTaskService(db, config_manager=MagicMock())
svc._policy_to_response = MagicMock(return_value=MagicMock())
payload = MagicMock(spec=ValidationTaskUpdate)
payload.model_dump.return_value = {"unknown_field": "x"}
await svc.update_task("pol-1", payload)
assert not hasattr(policy, "unknown_field")
db.commit.assert_called_once()
class TestDeleteTaskRuns:
"""delete_task — delete_runs=True but no run IDs found."""
def test_delete_with_runs_but_no_run_ids(self):
from src.services.validation_service import ValidationTaskService
policy = MagicMock()
policy.id = "pol-1"
policy.name = "Test Task"
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = policy
db.query.return_value.filter.return_value.all.return_value = []
svc = ValidationTaskService(db)
svc.delete_task("pol-1", delete_runs=True)
db.delete.assert_called_once_with(policy)
db.commit.assert_called_once()
class TestTriggerRunNoProvider:
"""trigger_run — policy without provider_id skips provider validation."""
async def test_trigger_without_provider_id(self):
from src.services.validation_service import ValidationTaskService
policy = MagicMock()
policy.id = "pol-1"
policy.name = "Test Task"
policy.environment_id = "env-1"
policy.provider_id = None
policy.dashboard_ids = ["42"]
policy.screenshot_enabled = True
policy.logs_enabled = True
policy.execute_chart_data = False
policy.llm_batch_size = None
policy.policy_dashboard_concurrency_limit = None
policy.prompt_template = None
policy.sources = []
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)
task_manager = AsyncMock()
task = MagicMock()
task.id = "spawned-task-1"
task_manager.create_task.return_value = task
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
result = await svc.trigger_run("pol-1", task_manager)
assert result["task_id"] == "spawned-task-1"
mock_cls.assert_not_called()
async def test_trigger_without_provider_and_no_dashboards(self):
"""provider_id empty + no sources/dashboard_ids -> ValueError."""
from src.services.validation_service import ValidationTaskService
policy = MagicMock()
policy.id = "pol-1"
policy.name = "Test Task"
policy.provider_id = None
policy.dashboard_ids = []
policy.sources = []
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)
with pytest.raises(ValueError, match="no dashboard IDs"):
await svc.trigger_run("pol-1", AsyncMock())
# #endregion Test.ValidationTaskService.Branches