1515
1616from __future__ import annotations
1717
18+ import contextlib
1819from datetime import datetime
1920from datetime import timezone
2021import os
@@ -368,24 +369,69 @@ def test_migrate_from_sqlalchemy_pickle_ignores_non_object_json_fields():
368369 assert event .content is None
369370
370371
371- def test_migrate_from_sqlalchemy_pickle_reads_naive_timestamp_as_local (
372- monkeypatch ,
373- ):
372+ @contextlib .contextmanager
373+ def _pinned_local_timezone (name : str ):
374+ """Pins the process timezone for the duration of the block.
375+
376+ ``time.tzset`` is POSIX-only, so on other platforms the block runs in the
377+ host zone instead. Restoring ``TZ`` without a second ``tzset`` would leave
378+ the C library pinned for the rest of the session, so both are undone.
379+ """
380+ if not hasattr (time , "tzset" ):
381+ yield
382+ return
383+ previous = os .environ .get ("TZ" )
384+ os .environ ["TZ" ] = name
385+ time .tzset ()
386+ try :
387+ yield
388+ finally :
389+ if previous is None :
390+ os .environ .pop ("TZ" , None )
391+ else :
392+ os .environ ["TZ" ] = previous
393+ time .tzset ()
394+
395+
396+ def test_migrate_from_sqlalchemy_pickle_reads_naive_timestamp_as_local ():
374397 """Naive v0 event timestamps must migrate as local time, not UTC.
375398
376399 The v0 schema stored the event ``timestamp`` column as a naive datetime in
377400 local time (``StorageEvent.from_event`` uses ``datetime.fromtimestamp`` and
378401 ``to_event`` reads it back with naive ``.timestamp()``). Forcing UTC on that
379- naive value shifted every migrated timestamp by the host's UTC offset. Pin a
380- fixed non-UTC zone so the round trip is exact regardless of the host.
402+ naive value shifted every migrated timestamp by the host's UTC offset.
381403 """
382- monkeypatch .setenv ("TZ" , "Asia/Kolkata" )
383- time .tzset ()
384- try :
385- original_epoch = 1000000.0
404+ original_epoch = 1000000.0
405+
406+ class NaiveLocalDatetime (datetime ):
407+ """Local naive datetime that rejects a timezone being forced onto it.
408+
409+ ``replace`` and ``astimezone`` return instances of this subclass, so a
410+ migration that pins a timezone before reading the epoch back trips the
411+ guard even on a host whose local zone is already UTC and where the
412+ resulting epoch would be unchanged.
413+ """
414+
415+ def timestamp (self ) -> float :
416+ assert (
417+ self .tzinfo is None
418+ ), f"migration forced { self .tzinfo } onto a naive v0 timestamp"
419+ return super ().timestamp ()
420+
421+ # The pinned zone is what catches a UTC recomputation that arrives by some
422+ # other route, e.g. calendar.timegm(), which the guard above cannot see.
423+ with _pinned_local_timezone ("Asia/Kolkata" ):
386424 # Exactly what v0.StorageEvent.from_event persisted: naive local time.
387- naive_local_timestamp = datetime .fromtimestamp (original_epoch )
388- assert naive_local_timestamp .tzinfo is None
425+ local = datetime .fromtimestamp (original_epoch )
426+ naive_local_timestamp = NaiveLocalDatetime (
427+ local .year ,
428+ local .month ,
429+ local .day ,
430+ local .hour ,
431+ local .minute ,
432+ local .second ,
433+ local .microsecond ,
434+ )
389435
390436 event = mfsp ._row_to_event ({
391437 "id" : "event-naive-timestamp" ,
@@ -396,8 +442,6 @@ def test_migrate_from_sqlalchemy_pickle_reads_naive_timestamp_as_local(
396442 })
397443
398444 assert event .timestamp == original_epoch
399- finally :
400- time .tzset ()
401445
402446
403447def test_migrate_from_sqlalchemy_pickle_blocks_unsafe_actions_pickle (
@@ -528,7 +572,7 @@ def __reduce__(self):
528572
529573
530574def test_migrate_from_sqlalchemy_pickle_with_async_driver_urls (tmp_path ):
531- """Tests that migration works with async driver URLs (fixes issue #4176) .
575+ """Tests that migration works with async driver URLs.
532576
533577 Users often provide async driver URLs (e.g., postgresql+asyncpg://) since
534578 that's what ADK requires at runtime. The migration tool should handle these
@@ -564,7 +608,7 @@ def test_migrate_from_sqlalchemy_pickle_with_async_driver_urls(tmp_path):
564608 source_session .commit ()
565609 source_session .close ()
566610
567- # This should NOT raise an error about async drivers (the fix for #4176)
611+ # This should NOT raise an error about async drivers.
568612 mfsp .migrate (source_db_url , dest_db_url )
569613
570614 # Verify destination DB
0 commit comments