From ee71e36da1a9bf0d95e3a9c88b66c1d135622657 Mon Sep 17 00:00:00 2001 From: Alicepoltora Date: Sun, 27 Sep 2026 04:22:17 +0700 Subject: [PATCH 1/2] fix(usage-metrics): replace deprecated datetime.utcnow() with timezone-aware datetime.now(timezone.utc) datetime.utcnow() was removed in Python 3.12 and returned a naive datetime (no timezone). _format_created_at() should emit ISO 8601 strings that carry timezone info (+00:00) so the upstream metrics API can parse the offset consistently with the rest of Studio. The change replaces: datetime.utcnow().isoformat() with: datetime.now(timezone.utc).isoformat() Regression tests verify the returned string contains a timezone offset. --- backend/services/usage_metrics_service.py | 4 ++-- tests/unit/test_usage_metrics_service.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/backend/services/usage_metrics_service.py b/backend/services/usage_metrics_service.py index 8b1e09ba6..af9dafaa7 100644 --- a/backend/services/usage_metrics_service.py +++ b/backend/services/usage_metrics_service.py @@ -4,7 +4,7 @@ import math import os from typing import Any, Optional -from datetime import datetime +from datetime import datetime, timezone import aiohttp from loguru import logger @@ -464,7 +464,7 @@ def _safe_int(self, value: Any) -> int: def _format_created_at(self, created_at) -> str: """Format created_at to ISO8601 string.""" if created_at is None: - return datetime.utcnow().isoformat() + return datetime.now(timezone.utc).isoformat() if isinstance(created_at, datetime): return created_at.isoformat() diff --git a/tests/unit/test_usage_metrics_service.py b/tests/unit/test_usage_metrics_service.py index f670b12d5..00e7b9305 100644 --- a/tests/unit/test_usage_metrics_service.py +++ b/tests/unit/test_usage_metrics_service.py @@ -378,3 +378,18 @@ def test_extract_llm_calls_falls_back_to_primary_without_token_metrics(): "costUsd": 0, } ] + + +def test_format_created_at_returns_utc_isoformat(): + service = UsageMetricsService() + result = service._format_created_at(None) + assert "+" in result # timezone-aware ISO string includes offset like "+00:00" + assert "Z" not in result # should use +00:00, not Z suffix + + +def test_format_created_at_returns_original_datetime_isoformat(): + from datetime import datetime, timezone + service = UsageMetricsService() + dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + result = service._format_created_at(dt) + assert result == "2024-01-01T12:00:00+00:00" From ee07028abf11bcb30e0a1b8e7fd6a48e551645f8 Mon Sep 17 00:00:00 2001 From: Alicepoltora Date: Sun, 27 Sep 2026 04:25:14 +0700 Subject: [PATCH 2/2] fix(node): use timezone-aware datetime in deploy_contract fallback _node.deploy_contract() uses _date_from_str(transaction_created_at) to parse the override timestamp, and when that returns None falls back to datetime.datetime.now(). datetime.datetime.now() returns a **naive** datetime (no timezone). _run_genvm() asserts before sending the datetime to the GenVM host, so the naive fallback would trigger an AssertionError as soon as any deploy request omits a sim_config.genvm_datetime override. Replace datetime.datetime.now() with datetime.datetime.now(tz=datetime.timezone.utc) so the fallback always carries a +00:00 offset. A regression test verifies the path passes a tz-aware datetime into _run_genvm even when transaction_created_at is None. --- backend/node/base.py | 2 +- tests/unit/test_leader_llm_recovery.py | 36 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/node/base.py b/backend/node/base.py index 34e1e2052..c388d91b6 100644 --- a/backend/node/base.py +++ b/backend/node/base.py @@ -805,7 +805,7 @@ async def deploy_contract( transaction_datetime = self._date_from_str(transaction_created_at) if transaction_datetime is None: - transaction_datetime = datetime.datetime.now() + transaction_datetime = datetime.datetime.now(tz=datetime.timezone.utc) return await self._run_genvm( from_address, diff --git a/tests/unit/test_leader_llm_recovery.py b/tests/unit/test_leader_llm_recovery.py index 62e08e6e7..c102e2287 100644 --- a/tests/unit/test_leader_llm_recovery.py +++ b/tests/unit/test_leader_llm_recovery.py @@ -1,5 +1,6 @@ """Unit tests for leader/validator fatal error handling in Node._run_genvm().""" +import asyncio import pytest from unittest.mock import MagicMock, AsyncMock, patch @@ -198,3 +199,38 @@ async def test_validator_fatal_error_returns_receipt(): assert receipt.vote == Vote.DETERMINISTIC_VIOLATION assert receipt.genvm_result["raw_error"]["fatal"] is True assert receipt.genvm_result["error_code"] == GenVMErrorCode.LLM_NO_PROVIDER + + +def test_deploy_contract_fallback_is_timezone_aware(): + """When transaction_created_at is None, deploy_contract must fall back to + a timezone-aware datetime so _run_genvm's assert on tzinfo does not fire.""" + import datetime as _dt + from unittest.mock import AsyncMock, patch + from backend.node.base import Node + from backend.node.types import Receipt + + node = Node( + contract_snapshot=_make_snapshot(), + validator_mode=ExecutionMode.LEADER, + validator=_make_validator(), + leader_receipt=None, + msg_handler=MagicMock(), + contract_snapshot_factory=None, + manager=MagicMock(), + ) + mock_genvm = AsyncMock(return_value=MagicMock(spec=Receipt)) + with patch.object(node, "_run_genvm", mock_genvm): + with patch("backend.node.base.datetime") as mock_dt: + mock_dt.datetime.now.return_value = _dt.datetime(2024, 1, 1, 12, 0, 0, tzinfo=_dt.timezone.utc) + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(node.deploy_contract( + from_address="0x00000000000000000000000000000001234", + code_to_deploy=b"", + calldata=b"", + transaction_created_at=None, + )) + finally: + loop.close() + td = mock_genvm.call_args.kwargs["transaction_datetime"] + assert td.tzinfo is not None