fix(dashboards): normalize naive/aware datetimes in resource task ordering

This commit is contained in:
2026-03-10 09:29:40 +03:00
parent 3a8c82918a
commit 82435822eb
2 changed files with 90 additions and 6 deletions

View File

@@ -9,7 +9,7 @@
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
from datetime import datetime
from datetime import datetime, timezone
# [DEF:test_get_dashboards_with_status:Function]
@@ -269,4 +269,71 @@ def test_get_last_task_for_resource_no_match():
# [/DEF:test_get_last_task_for_resource_no_match:Function]
# [DEF:test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes:Function]
# @TEST: get_dashboards_with_status handles mixed naive/aware datetimes without comparison errors.
# @PRE: Task list includes both timezone-aware and timezone-naive timestamps.
# @POST: Latest task is selected deterministically and no exception is raised.
@pytest.mark.asyncio
async def test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes():
with patch("src.services.resource_service.SupersetClient") as mock_client, \
patch("src.services.resource_service.GitService"):
from src.services.resource_service import ResourceService
service = ResourceService()
mock_client.return_value.get_dashboards_summary.return_value = [
{"id": 1, "title": "Dashboard 1", "slug": "dash-1"}
]
task_naive = MagicMock()
task_naive.id = "task-naive"
task_naive.plugin_id = "llm_dashboard_validation"
task_naive.status = "SUCCESS"
task_naive.params = {"dashboard_id": "1", "environment_id": "prod"}
task_naive.started_at = datetime(2024, 1, 1, 10, 0, 0)
task_aware = MagicMock()
task_aware.id = "task-aware"
task_aware.plugin_id = "llm_dashboard_validation"
task_aware.status = "SUCCESS"
task_aware.params = {"dashboard_id": "1", "environment_id": "prod"}
task_aware.started_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
env = MagicMock()
env.id = "prod"
result = await service.get_dashboards_with_status(env, [task_naive, task_aware])
assert result[0]["last_task"]["task_id"] == "task-aware"
# [/DEF:test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes:Function]
# [DEF:test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at:Function]
# @TEST: _get_last_task_for_resource handles mixed naive/aware created_at values.
# @PRE: Matching tasks include naive and aware created_at timestamps.
# @POST: Latest task is returned without raising datetime comparison errors.
def test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at():
from src.services.resource_service import ResourceService
service = ResourceService()
task_naive = MagicMock()
task_naive.id = "task-old"
task_naive.status = "SUCCESS"
task_naive.params = {"resource_id": "dashboard-1"}
task_naive.created_at = datetime(2024, 1, 1, 10, 0, 0)
task_aware = MagicMock()
task_aware.id = "task-new"
task_aware.status = "RUNNING"
task_aware.params = {"resource_id": "dashboard-1"}
task_aware.created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
result = service._get_last_task_for_resource("dashboard-1", [task_naive, task_aware])
assert result is not None
assert result["task_id"] == "task-new"
# [/DEF:test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at:Function]
# [/DEF:backend.src.services.__tests__.test_resource_service:Module]

View File

@@ -10,7 +10,7 @@
# [SECTION: IMPORTS]
from typing import List, Dict, Optional, Any
from datetime import datetime
from datetime import datetime, timezone
from ..core.superset_client import SupersetClient
from ..core.task_manager.models import Task
from ..services.git_service import GitService
@@ -179,12 +179,12 @@ class ResourceService:
return None
def _task_time(task_obj: Any) -> datetime:
return (
raw_time = (
getattr(task_obj, "started_at", None)
or getattr(task_obj, "finished_at", None)
or getattr(task_obj, "created_at", None)
or datetime.min
)
return self._normalize_datetime_for_compare(raw_time)
last_task = max(matched_tasks, key=_task_time)
raw_result = getattr(last_task, "result", None)
@@ -229,6 +229,20 @@ class ResourceService:
return status_text
return "UNKNOWN"
# [/DEF:_normalize_validation_status:Function]
# [DEF:_normalize_datetime_for_compare:Function]
# @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.
# @PRE: value may be datetime or any scalar.
# @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.
# @PARAM: value (Any) - Candidate datetime-like value.
# @RETURN: datetime - UTC-aware comparable datetime.
def _normalize_datetime_for_compare(self, value: Any) -> datetime:
if isinstance(value, datetime):
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
return datetime.min.replace(tzinfo=timezone.utc)
# [/DEF:_normalize_datetime_for_compare:Function]
# [DEF:get_datasets_with_status:Function]
# @PURPOSE: Fetch datasets from environment with mapping progress and last task status
@@ -391,8 +405,11 @@ class ResourceService:
if not resource_tasks:
return None
# Get most recent task
last_task = max(resource_tasks, key=lambda t: t.created_at)
# Get most recent task with timezone-safe comparison.
last_task = max(
resource_tasks,
key=lambda t: self._normalize_datetime_for_compare(getattr(t, "created_at", None)),
)
return {
'task_id': str(last_task.id),