Subagents delivered tests across all uncovered backend modules: Schemas (100%): agent, auth, health, profile, settings, validation Services (98-100%): auth, profile, health, llm, mapping, resource, security, git, superset_lookup, sql_table_extractor, rbac API routes (new): auth, admin, health, environments, plugins, dashboards (helpers, projection, actions, listing), git (config, deps, env, helpers) Clean Release (100%): DTO, facade, policy_engine, stages, repos, preparation, source_isolation, compliance Git services: base, remote_providers Agent module: app, run, middleware, langgraph_setup Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching Reports: normalizer, report_service, type_profiles Notifications: service, providers Also: - .gitignore: add .coverage, *.cover, coverage-* dirs - src/schemas/auth.py: fix AD group DN regex (comma in CN=...) - Remove co-located src/services/__tests__/ (caused pytest module collision)
257 lines
12 KiB
Python
257 lines
12 KiB
Python
# #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
|