From 69136c88db39ebacbfdbca62d4a4bae63faf60ae Mon Sep 17 00:00:00 2001 From: DDT <141296842+Hugo-DDT@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:47:56 +0800 Subject: [PATCH] fix(sessions): raise SessionNotFoundError when appending to an unknown Redis session --- .../redis/_redis_session_service.py | 9 ++++++-- .../integrations/redis/_fake_redis.py | 3 +++ .../redis/test_redis_session_service.py | 22 +++++++++++++++++++ tests/unittests/sessions/_conformance.py | 7 ------ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/google/adk/integrations/redis/_redis_session_service.py b/src/google/adk/integrations/redis/_redis_session_service.py index 593cc7f2341..5d5825516b3 100644 --- a/src/google/adk/integrations/redis/_redis_session_service.py +++ b/src/google/adk/integrations/redis/_redis_session_service.py @@ -27,6 +27,7 @@ from typing import Optional from ...errors.already_exists_error import AlreadyExistsError +from ...errors.session_not_found_error import SessionNotFoundError from ...events.event import Event from ...platform import uuid as platform_uuid from ...sessions import _session_util @@ -339,6 +340,12 @@ async def get_user_state( async def append_event(self, session: Session, event: Event) -> Event: """Appends an event to the session and synchronizes state in Redis.""" client = self._get_redis() + key = self._session_key(session.app_name, session.user_id, session.id) + # Check before any write: the app/user state deltas below are stored under + # their own keys, and the session write would otherwise create a session + # that was never stored instead of failing like the other services. + if not event.partial and not await client.exists(key): + raise SessionNotFoundError(f"Session {session.id} not found.") event = await super().append_event(session, event) # Stamp the session with the event's own timestamp, matching # InMemorySessionService, SqliteSessionService and DatabaseSessionService. @@ -388,8 +395,6 @@ async def append_event(self, session: Session, event: Event) -> Event: last_update_time=session.last_update_time, ) - key = self._session_key(session.app_name, session.user_id, session.id) - async def append_to_storage(pipe: Any) -> None: raw = await pipe.get(key) if raw: diff --git a/tests/unittests/integrations/redis/_fake_redis.py b/tests/unittests/integrations/redis/_fake_redis.py index 3047c4fb6aa..86a35426550 100644 --- a/tests/unittests/integrations/redis/_fake_redis.py +++ b/tests/unittests/integrations/redis/_fake_redis.py @@ -57,6 +57,9 @@ async def get(self, key: str) -> str | None: return None return self._store.get(key) + async def exists(self, *keys: str) -> int: + return sum(not self._is_expired(key) for key in keys) + async def set( self, key: str, diff --git a/tests/unittests/integrations/redis/test_redis_session_service.py b/tests/unittests/integrations/redis/test_redis_session_service.py index 47948c09b96..e3348013838 100644 --- a/tests/unittests/integrations/redis/test_redis_session_service.py +++ b/tests/unittests/integrations/redis/test_redis_session_service.py @@ -20,11 +20,13 @@ from unittest import mock from google.adk.errors.already_exists_error import AlreadyExistsError +from google.adk.errors.session_not_found_error import SessionNotFoundError from google.adk.events.event import Event from google.adk.events.event import EventActions from google.adk.integrations.redis._config import RedisSessionServiceConfig from google.adk.integrations.redis._redis_session_service import RedisSessionService from google.adk.sessions.base_session_service import GetSessionConfig +from google.adk.sessions.session import Session from google.genai import types import pytest @@ -806,3 +808,23 @@ async def test_cumulative_user_state_creation(session_service): user_id="u1", ) assert user_state == {"theme": "dark", "lang": "en"} + + +@pytest.mark.asyncio +async def test_append_event_to_unknown_session_writes_nothing( + session_service, fake_redis +): + """A rejected append leaves no session, app state or user state behind.""" + session = Session(app_name="app1", user_id="u1", id="never_created") + event = Event( + author="agent", + actions=EventActions( + state_delta={"count": 1, "user:score": 100, "app:status": "active"} + ), + ) + + with pytest.raises(SessionNotFoundError): + await session_service.append_event(session, event) + + assert fake_redis._store == {} + assert session.events == [] diff --git a/tests/unittests/sessions/_conformance.py b/tests/unittests/sessions/_conformance.py index 5dd3053b795..cb80138d3d8 100644 --- a/tests/unittests/sessions/_conformance.py +++ b/tests/unittests/sessions/_conformance.py @@ -140,13 +140,6 @@ async def _make_per_agent_database( _Backend( 'redis', _make_redis, - divergences={ - 'test_append_event_to_unknown_session_raises_session_not_found': ( - 'Redis writes the session key unconditionally on append, so' - ' appending to a session it has never stored creates one' - ' instead of raising.' - ), - }, ), _Backend('per_agent_database', _make_per_agent_database), ]