# #region TestRunToDict [C:3] [TYPE Module] [SEMANTICS test,validation,run,dict,duration] # @BRIEF Verify ValidationTaskService._run_to_dict computes duration_ms correctly, # including timezone-naive datetime handling and edge cases. # @RELATION BINDS_TO -> [ValidationTaskService] # @TEST_EDGE: no_finished_at -> duration_ms is None # @TEST_EDGE: no_started_at -> duration_ms is None # @TEST_EDGE: naive_datetime -> Handled correctly (treated as UTC) # @TEST_EDGE: aware_datetime -> Handled correctly # @TEST_INVARIANT: duration_ms_computed -> VERIFIED_BY: test_duration_ms_with_aware_datetimes, test_duration_ms_with_naive_datetimes import sys from datetime import UTC, datetime, timezone, timedelta from pathlib import Path from unittest.mock import MagicMock import pytest sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) class MockRun: """Minimal mock matching ValidationRun ORM attributes.""" def __init__(self, **kwargs): self.id = kwargs.get("id", "run-1") self.policy_id = kwargs.get("policy_id", "policy-1") self.task_id = kwargs.get("task_id", "task-1") self.status = kwargs.get("status", "completed") self.trigger = kwargs.get("trigger", "manual") self.started_at = kwargs.get("started_at") self.finished_at = kwargs.get("finished_at") self.dashboard_count = kwargs.get("dashboard_count", 3) self.pass_count = kwargs.get("pass_count", 2) self.warn_count = kwargs.get("warn_count", 1) self.fail_count = kwargs.get("fail_count", 0) self.unknown_count = kwargs.get("unknown_count", 0) self.created_at = kwargs.get("created_at", datetime(2025, 1, 15, tzinfo=UTC)) # #region TestRunToDictDuration [C:2] [TYPE Class] # @BRIEF Verify _run_to_dict duration_ms computation. class TestRunToDictDuration: """Verify _run_to_dict duration_ms field.""" # #region setup [C:1] [TYPE Function] @pytest.fixture(autouse=True) def setup_service(self): from src.services.validation_service import ValidationTaskService self.mock_db = MagicMock() self.service = ValidationTaskService(db=self.mock_db) self.run_to_dict = self.service._run_to_dict # #endregion setup # #region test_duration_ms_with_aware_datetimes [C:2] [TYPE Function] # @BRIEF UTC-aware started_at/finished_at → correct duration_ms. def test_duration_ms_with_aware_datetimes(self): started = datetime(2025, 1, 15, 10, 0, 0, tzinfo=UTC) finished = datetime(2025, 1, 15, 10, 0, 5, tzinfo=UTC) # 5 seconds run = MockRun(started_at=started, finished_at=finished) result = self.run_to_dict(run) assert result["duration_ms"] == 5000 # #endregion test_duration_ms_with_aware_datetimes # #region test_duration_ms_with_naive_datetimes [C:2] [TYPE Function] # @BRIEF Naive (no timezone) datetimes → treated as UTC, correct duration. def test_duration_ms_with_naive_datetimes(self): started = datetime(2025, 1, 15, 10, 0, 0) # naive finished = datetime(2025, 1, 15, 10, 2, 30) # naive, 2.5 minutes run = MockRun(started_at=started, finished_at=finished) result = self.run_to_dict(run) assert result["duration_ms"] == 150000 # 2.5 * 60 * 1000 # #endregion test_duration_ms_with_naive_datetimes # #region test_duration_ms_no_finished_at [C:2] [TYPE Function] # @BRIEF When finished_at is None, duration_ms is None. def test_duration_ms_no_finished_at(self): started = datetime(2025, 1, 15, 10, 0, 0, tzinfo=UTC) run = MockRun(started_at=started, finished_at=None) result = self.run_to_dict(run) assert result["duration_ms"] is None # #endregion test_duration_ms_no_finished_at # #region test_duration_ms_no_started_at [C:2] [TYPE Function] # @BRIEF When started_at is None, duration_ms is None. def test_duration_ms_no_started_at(self): finished = datetime(2025, 1, 15, 10, 0, 0, tzinfo=UTC) run = MockRun(started_at=None, finished_at=finished) result = self.run_to_dict(run) assert result["duration_ms"] is None # #endregion test_duration_ms_no_started_at # #region test_duration_ms_both_none [C:2] [TYPE Function] # @BRIEF When both are None, duration_ms is None. def test_duration_ms_both_none(self): run = MockRun(started_at=None, finished_at=None) result = self.run_to_dict(run) assert result["duration_ms"] is None # #endregion test_duration_ms_both_none # #region test_duration_ms_mixed_aware_naive [C:2] [TYPE Function] # @BRIEF One aware, one naive → still computes correctly (both treated as UTC). def test_duration_ms_mixed_aware_naive(self): started = datetime(2025, 1, 15, 10, 0, 0) # naive finished = datetime(2025, 1, 15, 10, 0, 10, tzinfo=UTC) # aware run = MockRun(started_at=started, finished_at=finished) result = self.run_to_dict(run) assert result["duration_ms"] == 10000 # #endregion test_duration_ms_mixed_aware_naive # #region test_other_fields_present [C:2] [TYPE Function] # @BRIEF _run_to_dict returns all expected fields. def test_other_fields_present(self): run = MockRun() result = self.run_to_dict(run) assert result["id"] == "run-1" assert result["policy_id"] == "policy-1" assert result["task_id"] == "task-1" assert result["status"] == "completed" assert result["trigger"] == "manual" assert result["dashboard_count"] == 3 assert result["pass_count"] == 2 assert result["warn_count"] == 1 assert result["fail_count"] == 0 assert result["unknown_count"] == 0 # #endregion test_other_fields_present # #region test_isoformat_on_dates [C:2] [TYPE Function] # @BRIEF started_at/finished_at are ISO-formatted strings in output. def test_isoformat_on_dates(self): started = datetime(2025, 1, 15, 10, 0, 0, tzinfo=UTC) run = MockRun(started_at=started, finished_at=started) result = self.run_to_dict(run) assert isinstance(result["started_at"], str) assert isinstance(result["finished_at"], str) assert "2025-01-15" in result["started_at"] # #endregion test_isoformat_on_dates # #endregion TestRunToDictDuration # #endregion TestRunToDict