diff --git a/src/google/adk/sessions/migration/migrate_from_sqlalchemy_pickle.py b/src/google/adk/sessions/migration/migrate_from_sqlalchemy_pickle.py index 65a78c94012..d88c2460563 100644 --- a/src/google/adk/sessions/migration/migrate_from_sqlalchemy_pickle.py +++ b/src/google/adk/sessions/migration/migrate_from_sqlalchemy_pickle.py @@ -220,7 +220,12 @@ def _safe_json_load(val: Any) -> dict[str, Any] | None: author=row.get("author", "agent"), branch=row.get("branch"), actions=actions, - timestamp=timestamp.replace(tzinfo=timezone.utc).timestamp(), + # v0 wrote this column as a naive datetime in local time (via + # datetime.fromtimestamp) and read it back the same way, so interpret a + # naive value as local time here too. Forcing UTC would shift every + # migrated timestamp by the host's UTC offset. datetime.timestamp() + # treats naive datetimes as local and honors tzinfo when present. + timestamp=timestamp.timestamp(), long_running_tool_ids=long_running_tool_ids, partial=row.get("partial"), turn_complete=row.get("turn_complete"), diff --git a/tests/unittests/sessions/migration/test_migration.py b/tests/unittests/sessions/migration/test_migration.py index 9f54a2e0ace..45250f337b9 100644 --- a/tests/unittests/sessions/migration/test_migration.py +++ b/tests/unittests/sessions/migration/test_migration.py @@ -19,6 +19,7 @@ from datetime import timezone import os import pickle +import time from fastapi.openapi.models import HTTPBearer from google.adk.auth.auth_tool import AuthConfig @@ -367,6 +368,38 @@ def test_migrate_from_sqlalchemy_pickle_ignores_non_object_json_fields(): assert event.content is None +def test_migrate_from_sqlalchemy_pickle_reads_naive_timestamp_as_local( + monkeypatch, +): + """Naive v0 event timestamps must migrate as local time, not UTC. + + The v0 schema stored the event ``timestamp`` column as a naive datetime in + local time (``StorageEvent.from_event`` uses ``datetime.fromtimestamp`` and + ``to_event`` reads it back with naive ``.timestamp()``). Forcing UTC on that + naive value shifted every migrated timestamp by the host's UTC offset. Pin a + fixed non-UTC zone so the round trip is exact regardless of the host. + """ + monkeypatch.setenv("TZ", "Asia/Kolkata") + time.tzset() + try: + original_epoch = 1000000.0 + # Exactly what v0.StorageEvent.from_event persisted: naive local time. + naive_local_timestamp = datetime.fromtimestamp(original_epoch) + assert naive_local_timestamp.tzinfo is None + + event = mfsp._row_to_event({ + "id": "event-naive-timestamp", + "invocation_id": "invoke1", + "author": "user", + "actions": EventActions(), + "timestamp": naive_local_timestamp, + }) + + assert event.timestamp == original_epoch + finally: + time.tzset() + + def test_migrate_from_sqlalchemy_pickle_blocks_unsafe_actions_pickle( tmp_path, monkeypatch ):