# #region Test.ReportNormalizer [C:3] [TYPE Module] [SEMANTICS test,report,normalizer,task] # @BRIEF Tests for services/reports/normalizer.py — status_to_report_status, build_summary, extract_error_context, normalize_task_report. # @RELATION BINDS_TO -> [normalizer] from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock, patch import pytest # #region helper factories [C:1] [TYPE Function] # @BRIEF Helper factory for creating mock tasks. def _make_task(**overrides): from src.core.task_manager.models import Task, TaskStatus, LogEntry defaults = { "id": "task-1", "plugin_id": "superset-migration", "status": TaskStatus.SUCCESS, "params": {}, "result": None, "started_at": datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC), "finished_at": datetime(2024, 1, 1, 12, 30, 0, tzinfo=UTC), "logs": [], } defaults.update(overrides) return Task(**defaults) # #endregion helper factories # #region test_status_to_report_status [C:2] [TYPE Function] # @BRIEF Test status_to_report_status mapping. class TestStatusToReportStatus: def test_success_maps_to_success(self): from src.services.reports.normalizer import status_to_report_status from src.core.task_manager.models import TaskStatus from src.models.report import ReportStatus assert status_to_report_status(TaskStatus.SUCCESS) == ReportStatus.SUCCESS def test_failed_maps_to_failed(self): from src.services.reports.normalizer import status_to_report_status from src.core.task_manager.models import TaskStatus from src.models.report import ReportStatus assert status_to_report_status(TaskStatus.FAILED) == ReportStatus.FAILED def test_pending_maps_to_in_progress(self): from src.services.reports.normalizer import status_to_report_status from src.core.task_manager.models import TaskStatus from src.models.report import ReportStatus for status in [TaskStatus.PENDING, TaskStatus.RUNNING, TaskStatus.AWAITING_INPUT, TaskStatus.AWAITING_MAPPING]: assert status_to_report_status(status) == ReportStatus.IN_PROGRESS def test_unknown_string_maps_to_partial(self): from src.services.reports.normalizer import status_to_report_status from src.models.report import ReportStatus assert status_to_report_status("UNKNOWN_STATUS") == ReportStatus.PARTIAL # #endregion test_status_to_report_status # #region test_build_summary [C:2] [TYPE Function] # @BRIEF Test build_summary extracts summary from task result. class TestBuildSummary: def test_uses_result_summary(self): from src.services.reports.normalizer import build_summary from src.models.report import ReportStatus from src.core.task_manager.models import Task, TaskStatus task = _make_task(result={"summary": "Migration of 5 dashboards completed"}) summary = build_summary(task, ReportStatus.SUCCESS) assert summary == "Migration of 5 dashboards completed" def test_uses_result_message(self): from src.services.reports.normalizer import build_summary from src.models.report import ReportStatus task = _make_task(result={"message": "All dashboards migrated"}) summary = build_summary(task, ReportStatus.SUCCESS) assert summary == "All dashboards migrated" def test_fallback_to_status_based_summary(self): from src.services.reports.normalizer import build_summary from src.models.report import ReportStatus task = _make_task(result=None) assert build_summary(task, ReportStatus.SUCCESS) == "Task completed successfully" assert build_summary(task, ReportStatus.FAILED) == "Task failed" assert build_summary(task, ReportStatus.IN_PROGRESS) == "Task is in progress" assert build_summary(task, ReportStatus.PARTIAL) == "Task completed with partial data" def test_prefers_summary_over_message(self): from src.services.reports.normalizer import build_summary from src.models.report import ReportStatus task = _make_task(result={"summary": "Summary text", "message": "Message text"}) assert build_summary(task, ReportStatus.SUCCESS) == "Summary text" def test_empty_summary_skips(self): from src.services.reports.normalizer import build_summary from src.models.report import ReportStatus task = _make_task(result={"summary": "", "message": "Actual message"}) assert build_summary(task, ReportStatus.SUCCESS) == "Actual message" # #endregion test_build_summary # #region test_extract_error_context [C:2] [TYPE Function] # @BRIEF Test extract_error_context for failed/partial reports. class TestExtractErrorContext: def test_returns_none_for_success(self): from src.services.reports.normalizer import extract_error_context from src.models.report import ReportStatus task = _make_task() assert extract_error_context(task, ReportStatus.SUCCESS) is None def test_returns_none_for_in_progress(self): from src.services.reports.normalizer import extract_error_context from src.models.report import ReportStatus task = _make_task() assert extract_error_context(task, ReportStatus.IN_PROGRESS) is None def test_extracts_from_error_dict(self): from src.services.reports.normalizer import extract_error_context from src.models.report import ReportStatus task = _make_task(result={"error": {"message": "Connection failed", "code": "ERR_CONNECT", "next_actions": ["Check network", "Retry"]}}) ctx = extract_error_context(task, ReportStatus.FAILED) assert ctx is not None assert ctx.message == "Connection failed" assert ctx.code == "ERR_CONNECT" assert ctx.next_actions == ["Check network", "Retry"] def test_falls_back_to_error_message_string(self): from src.services.reports.normalizer import extract_error_context from src.models.report import ReportStatus task = _make_task(result={"error_message": "Something went wrong"}) ctx = extract_error_context(task, ReportStatus.FAILED) assert ctx is not None assert ctx.message == "Something went wrong" def test_falls_back_to_logs(self): from src.services.reports.normalizer import extract_error_context from src.models.report import ReportStatus from src.core.task_manager.models import LogEntry task = _make_task( result={"other": "data"}, logs=[LogEntry(level="ERROR", message="Critical failure in module X")], ) ctx = extract_error_context(task, ReportStatus.FAILED) assert ctx is not None assert ctx.message == "Critical failure in module X" def test_default_message_not_provided(self): from src.services.reports.normalizer import extract_error_context from src.models.report import ReportStatus task = _make_task(result={"unrelated": "data"}) ctx = extract_error_context(task, ReportStatus.FAILED) assert ctx is not None assert ctx.message == "Not provided" def test_default_next_actions(self): from src.services.reports.normalizer import extract_error_context from src.models.report import ReportStatus task = _make_task(result={"error": {"message": "Error without actions"}}) ctx = extract_error_context(task, ReportStatus.FAILED) assert ctx is not None assert ctx.next_actions == ["Review task diagnostics", "Retry the operation"] def test_works_for_partial_status(self): from src.services.reports.normalizer import extract_error_context from src.models.report import ReportStatus task = _make_task(result={"error": {"message": "Partial failure"}}) ctx = extract_error_context(task, ReportStatus.PARTIAL) assert ctx is not None assert ctx.message == "Partial failure" # #endregion test_extract_error_context # #region test_normalize_task_report [C:2] [TYPE Function] # @BRIEF Test normalize_task_report produces correct TaskReport. class TestNormalizeTaskReport: def test_returns_task_report(self): from src.services.reports.normalizer import normalize_task_report from src.models.report import TaskReport, ReportStatus, TaskType task = _make_task() report = normalize_task_report(task) assert isinstance(report, TaskReport) assert report.report_id == task.id assert report.task_type == TaskType.MIGRATION assert report.status == ReportStatus.SUCCESS assert report.summary == "Task completed successfully" def test_includes_profile_details(self): from src.services.reports.normalizer import normalize_task_report task = _make_task() report = normalize_task_report(task) assert report.details is not None assert "profile" in report.details assert report.details["profile"]["display_label"] == "Migration" def test_includes_source_ref_from_params(self): from src.services.reports.normalizer import normalize_task_report task = _make_task(params={"environment_id": "env-1", "dashboard_id": "dash-42"}) report = normalize_task_report(task) assert report.source_ref is not None assert report.source_ref["environment_id"] == "env-1" assert report.source_ref["dashboard_id"] == "dash-42" def test_includes_error_context_for_failed(self): from src.services.reports.normalizer import normalize_task_report from src.core.task_manager.models import TaskStatus task = _make_task( status=TaskStatus.FAILED, result={"error": {"message": "Deployment failed", "code": "ERR_DEPLOY"}}, ) report = normalize_task_report(task) assert report.error_context is not None assert report.error_context.message == "Deployment failed" def test_unknown_plugin_type(self): from src.services.reports.normalizer import normalize_task_report from src.models.report import TaskType task = _make_task(plugin_id="unknown-plugin") report = normalize_task_report(task) assert report.task_type == TaskType.UNKNOWN def test_no_updated_at_uses_started(self): from src.services.reports.normalizer import normalize_task_report from datetime import UTC, datetime task = _make_task(finished_at=None) report = normalize_task_report(task) assert report.updated_at == task.started_at def test_no_dates_uses_current_time(self): from src.services.reports.normalizer import normalize_task_report task = _make_task(started_at=None, finished_at=None) report = normalize_task_report(task) assert report.updated_at is not None def test_source_ref_none_when_no_params(self): from src.services.reports.normalizer import normalize_task_report task = _make_task(params={}) report = normalize_task_report(task) assert report.source_ref is None def test_none_result_becomes_note(self): from src.services.reports.normalizer import normalize_task_report task = _make_task(result=None) report = normalize_task_report(task) assert report.details is not None assert report.details["result"] == {"note": "Not provided"} def test_dict_result_included(self): from src.services.reports.normalizer import normalize_task_report task = _make_task(result={"migrated": 5, "errors": 0}) report = normalize_task_report(task) assert report.details is not None assert report.details["result"] == {"migrated": 5, "errors": 0} # #endregion test_normalize_task_report # #endregion Test.ReportNormalizer