From 17b2ff873163be0b034b8b1ef121398076cae258 Mon Sep 17 00:00:00 2001 From: lei_lei <96427312+leilei3167@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:44:58 +0000 Subject: [PATCH] fix(sessions): trim session id before the in-memory duplicate check create_session checked the raw id, then stripped it and stored under the trimmed key, so a padded id overwrote the existing session. Normalize first, matching sqlite. Fixes #6887 --- .../adk/sessions/in_memory_session_service.py | 7 +--- tests/unittests/sessions/_conformance.py | 23 ++++++++++- .../sessions/test_session_service.py | 38 +++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/google/adk/sessions/in_memory_session_service.py b/src/google/adk/sessions/in_memory_session_service.py index d2775cefa4..e7613e8f50 100644 --- a/src/google/adk/sessions/in_memory_session_service.py +++ b/src/google/adk/sessions/in_memory_session_service.py @@ -114,6 +114,7 @@ def _create_session_impl( state: Optional[dict[str, Any]] = None, session_id: Optional[str] = None, ) -> Session: + session_id = session_id.strip() if session_id else None if session_id and self._get_session_impl( app_name=app_name, user_id=user_id, session_id=session_id ): @@ -129,11 +130,7 @@ def _create_session_impl( user_state_delta ) - session_id = ( - session_id.strip() - if session_id and session_id.strip() - else platform_uuid.new_uuid() - ) + session_id = session_id or platform_uuid.new_uuid() session = Session( app_name=app_name, user_id=user_id, diff --git a/tests/unittests/sessions/_conformance.py b/tests/unittests/sessions/_conformance.py index 3cccf40eb2..c523ee0013 100644 --- a/tests/unittests/sessions/_conformance.py +++ b/tests/unittests/sessions/_conformance.py @@ -135,7 +135,20 @@ async def _make_per_agent_database( BACKENDS = [ _Backend('in_memory', _make_in_memory), _Backend('in_memory_light_copy', _make_in_memory_light_copy), - _Backend('database', _make_database), + _Backend( + 'database', + _make_database, + divergences={ + 'test_create_session_id_is_matched_after_trimming': ( + 'Database stores the client-supplied id as-is, so a padded id' + ' is a different session from its trimmed form.' + ), + 'test_create_session_with_blank_id_generates_one': ( + 'Database stores a blank id as an empty string instead of' + ' generating one.' + ), + }, + ), _Backend('sqlite', _make_sqlite), # Two more Redis divergences have no contract test to hang an xfail on # yet: it builds its key scan pattern from a truthiness check on the user @@ -154,6 +167,14 @@ async def _make_per_agent_database( 'Redis stamps the session with the wall clock instead of the' " appended event's timestamp." ), + 'test_create_session_id_is_matched_after_trimming': ( + 'Redis stores the client-supplied id as-is, so a padded id' + ' is a different session from its trimmed form.' + ), + 'test_create_session_with_blank_id_generates_one': ( + 'Redis stores a blank id as whitespace instead of generating' + ' one.' + ), }, ), _Backend('per_agent_database', _make_per_agent_database), diff --git a/tests/unittests/sessions/test_session_service.py b/tests/unittests/sessions/test_session_service.py index 95b9a3f552..dd1341cb18 100644 --- a/tests/unittests/sessions/test_session_service.py +++ b/tests/unittests/sessions/test_session_service.py @@ -1196,6 +1196,44 @@ async def test_create_session_with_existing_id_raises_error(session_service): ) +@pytest.mark.asyncio +async def test_create_session_id_is_matched_after_trimming(session_service): + """A padded id names the same session as its trimmed form. + + Whichever form a backend stores, the two must not become two sessions, and + the second create must not silently replace the first. + """ + await session_service.create_session( + app_name='my_app', + user_id='test_user', + session_id='existing_session', + state={'keep': 'original'}, + ) + + with pytest.raises(AlreadyExistsError): + await session_service.create_session( + app_name='my_app', + user_id='test_user', + session_id=' existing_session ', + state={'keep': 'clobbered'}, + ) + + session = await session_service.get_session( + app_name='my_app', user_id='test_user', session_id='existing_session' + ) + assert session is not None + assert session.state['keep'] == 'original' + + +@pytest.mark.asyncio +async def test_create_session_with_blank_id_generates_one(session_service): + """A blank client-supplied id is treated as "no id given".""" + session = await session_service.create_session( + app_name='my_app', user_id='test_user', session_id=' ' + ) + assert session.id.strip() + + @pytest.mark.asyncio async def test_append_event_bytes(session_service): app_name = 'my_app'