Files
adk-python/tests/unittests/sessions/test_vertex_ai_session_service.py
T
Alexis MarasiganandCopybara-Service b725045e5a fix: fix httpx client closure during event pagination
Merge https://github.com/google/adk-python/pull/3756

move event iteration inside api_client context in get_session

Move event iteration inside the api_client context manager in VertexAiSessionService.get_session() to prevent client closure during multi-page event fetching.

**Please ensure you have read the [contribution guide](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) before creating a pull request.**

### Link to Issue or Description of Change

**1. Link to an existing issue (if applicable):**

- Closes: #3757

**2. Or, if no issue exists, describe the change:**

**Problem:**

When a session contains more than 100 events (requiring pagination), `VertexAiSessionService.get_session()` fails with:

```
RuntimeError: Cannot send a request, as the client has been closed.
```

The root cause is that the `events_iterator` is consumed **outside** the `async with self._get_api_client() as api_client:` context block. When the iterator needs to fetch page 2, 3, etc., the API client has already been closed because the `async with` block has exited.

```python
# Current buggy flow:
async with self._get_api_client() as api_client:
    get_session_response, events_iterator = await asyncio.gather(...)
# ← Client closed here

async for event in events_iterator:  # ← Fails on page 2+ (client closed)
    session.events.append(...)
```

**Solution:**

Move the session creation, user validation, and event iteration **inside** the `async with` block so the API client remains open during the entire pagination process:

```python
async with self._get_api_client() as api_client:
    get_session_response, events_iterator = await asyncio.gather(...)
    # Validation and session creation...
    async for event in events_iterator:  # ← Now works for all pages
        session.events.append(...)
# Client closed after all events are fetched
```

### Testing Plan

**Unit Tests:**

- [x] I have added or updated unit tests for my change.
- [x] All unit tests pass locally.

```bash
pytest tests/unittests/sessions/test_vertex_ai_session_service.py -v
```

**Added regression test:** `test_get_session_pagination_keeps_client_open`
- Creates a `MockAsyncClientWithPagination` that tracks whether it's inside the `async with` context
- Raises `RuntimeError` if iteration happens outside the context (matching real httpx behavior)
- Simulates 3 pages of events (100 + 100 + 50 = 250 events)
- Verifies all 250 events are successfully retrieved

**Manual End-to-End (E2E) Tests:**

1. Deploy an ADK agent to Vertex AI Agent Engine
2. Create a session and send 100+ messages to accumulate >100 events
3. Verify `get_session()` successfully retrieves all events without error

**Before fix:**
```
RuntimeError: Cannot send a request, as the client has been closed.
```

**After fix:**
- Session with 201 events (3 pages) loads successfully
- All events are retrieved and appended to the session

### Checklist

- [x] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document.
- [x] I have performed a self-review of my own code.
- [x] I have commented my code, particularly in hard-to-understand areas.
- [x] I have added tests that prove my fix is effective or that my feature works.
- [x] New and existing unit tests pass locally with my changes.
- [x] I have manually tested my changes end-to-end.
- [x] Any dependent changes have been merged and published in downstream modules.

### Additional context

This bug affects any production deployment where users have extended conversations. Sessions accumulating >100 events (which triggers pagination) become completely unusable as the agent cannot load the session to process new messages.

The fix is minimal and maintains backward compatibility - it only changes the scope of the `async with` block without altering any logic or return values.

**Affected versions:** Tested on google-adk 1.19.0, but the bug exists in earlier versions as well.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/3756 from AlexisMarasigan:fix/vertex-ai-session-service-paginatio 01fbafa6524312f24f7c9feaffb07bff0ad49b77
PiperOrigin-RevId: 855451813
2026-01-12 17:27:29 -08:00

803 lines
25 KiB
Python

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import datetime
import re
import types
from typing import Any
from typing import List
from typing import Optional
from typing import Tuple
from unittest import mock
from dateutil.parser import isoparse
from fastapi.openapi import models as openapi_models
from google.adk.auth import auth_schemes
from google.adk.auth.auth_tool import AuthConfig
from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.adk.sessions.base_session_service import GetSessionConfig
from google.adk.sessions.session import Session
from google.adk.sessions.vertex_ai_session_service import VertexAiSessionService
from google.api_core import exceptions as api_core_exceptions
from google.genai import types as genai_types
import pytest
MOCK_SESSION_JSON_1 = {
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/1'
),
'create_time': '2024-12-12T12:12:12.123456Z',
'update_time': '2024-12-12T12:12:12.123456Z',
'session_state': {
'key': {'value': 'test_value'},
},
'user_id': 'user',
}
MOCK_SESSION_JSON_2 = {
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/2'
),
'update_time': '2024-12-13T12:12:12.123456Z',
'user_id': 'user',
}
MOCK_SESSION_JSON_3 = {
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/3'
),
'update_time': '2024-12-14T12:12:12.123456Z',
'user_id': 'user2',
}
MOCK_EVENT_JSON = [
{
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/1/events/123'
),
'invocation_id': '123',
'author': 'user',
'timestamp': '2024-12-12T12:12:12.123456Z',
'content': {
'parts': [
{'text': 'test_content'},
],
},
'actions': {
'state_delta': {
'key': {'value': 'test_value'},
},
'transfer_agent': 'agent',
},
'event_metadata': {
'partial': False,
'turn_complete': True,
'interrupted': False,
'branch': '',
'long_running_tool_ids': ['tool1'],
},
},
]
MOCK_EVENT_JSON_2 = [
{
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/2/events/123'
),
'invocation_id': '222',
'author': 'user',
'timestamp': '2024-12-12T12:12:12.123456Z',
},
]
MOCK_EVENT_JSON_3 = [
{
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/2/events/456'
),
'invocation_id': '333',
'author': 'user',
'timestamp': '2024-12-12T12:12:13.123456Z',
},
]
MOCK_SESSION_JSON_PAGE1 = {
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/page1'
),
'update_time': '2024-12-15T12:12:12.123456Z',
'user_id': 'user_with_pages',
}
MOCK_SESSION_JSON_PAGE2 = {
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/page2'
),
'update_time': '2024-12-16T12:12:12.123456Z',
'user_id': 'user_with_pages',
}
MOCK_SESSION_JSON_5 = {
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/5'
),
'update_time': '2024-12-12T12:15:12.123456Z',
'user_id': 'user_with_many_events',
}
def _generate_mock_events_for_session_5(num_events):
events = []
start_time = isoparse('2024-12-12T12:12:12.123456Z')
for i in range(num_events):
event_time = start_time + datetime.timedelta(microseconds=i * 1000)
events.append({
'name': (
'projects/test-project/locations/test-location/'
f'reasoningEngines/123/sessions/5/events/{i}'
),
'invocation_id': f'invocation_{i}',
'author': 'user_with_many_events',
'timestamp': event_time.isoformat().replace('+00:00', 'Z'),
})
return events
MANY_EVENTS_COUNT = 200
MOCK_EVENTS_JSON_5 = _generate_mock_events_for_session_5(MANY_EVENTS_COUNT)
MOCK_SESSION = Session(
app_name='123',
user_id='user',
id='1',
state=MOCK_SESSION_JSON_1['session_state'],
last_update_time=isoparse(MOCK_SESSION_JSON_1['update_time']).timestamp(),
events=[
Event(
id='123',
invocation_id='123',
author='user',
timestamp=isoparse(MOCK_EVENT_JSON[0]['timestamp']).timestamp(),
content=genai_types.Content(
parts=[genai_types.Part(text='test_content')]
),
actions=EventActions(
transfer_to_agent='agent',
state_delta={'key': {'value': 'test_value'}},
),
partial=False,
turn_complete=True,
interrupted=False,
branch='',
long_running_tool_ids={'tool1'},
),
],
)
MOCK_SESSION_2 = Session(
app_name='123',
user_id='user',
id='2',
last_update_time=isoparse(MOCK_SESSION_JSON_2['update_time']).timestamp(),
events=[
Event(
id='123',
invocation_id='222',
author='user',
timestamp=isoparse(MOCK_EVENT_JSON_2[0]['timestamp']).timestamp(),
),
Event(
id='456',
invocation_id='333',
author='user',
timestamp=isoparse(MOCK_EVENT_JSON_3[0]['timestamp']).timestamp(),
),
],
)
class PydanticNamespace(types.SimpleNamespace):
def model_dump(self, exclude_none=True, mode='python'):
d = {}
for k, v in self.__dict__.items():
if exclude_none and v is None:
continue
if isinstance(v, PydanticNamespace):
d[k] = v.model_dump(exclude_none=exclude_none, mode=mode)
elif isinstance(v, list):
d[k] = [
i.model_dump(exclude_none=exclude_none, mode=mode)
if isinstance(i, PydanticNamespace)
else i
for i in v
]
else:
d[k] = v
return d
def _convert_to_object(data):
if isinstance(data, dict):
kwargs = {}
for key, value in data.items():
if key in [
'timestamp',
'update_time',
'create_time',
] and isinstance(value, str):
kwargs[key] = isoparse(value)
elif key in [
'session_state',
'state_delta',
'artifact_delta',
'custom_metadata',
'requested_auth_configs',
]:
kwargs[key] = value
else:
kwargs[key] = _convert_to_object(value)
return PydanticNamespace(**kwargs)
elif isinstance(data, list):
return [_convert_to_object(item) for item in data]
else:
return data
async def to_async_iterator(data):
for item in data:
yield item
class MockAsyncClient:
"""Mocks the API Client."""
def __init__(self) -> None:
"""Initializes MockClient."""
self.session_dict: dict[str, Any] = {}
self.event_dict: dict[str, Tuple[List[Any], Optional[str]]] = {}
self.agent_engines = mock.AsyncMock()
self.agent_engines.sessions.get.side_effect = self._get_session
self.agent_engines.sessions.list.side_effect = self._list_sessions
self.agent_engines.sessions.delete.side_effect = self._delete_session
self.agent_engines.sessions.create.side_effect = self._create_session
self.agent_engines.sessions.events.list.side_effect = self._list_events
self.agent_engines.sessions.events.append.side_effect = self._append_event
self.last_create_session_config: dict[str, Any] = {}
async def __aenter__(self):
"""Enters the asynchronous context."""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Exits the asynchronous context."""
pass
async def _get_session(self, name: str):
session_id = name.split('/')[-1]
if session_id in self.session_dict:
return _convert_to_object(self.session_dict[session_id])
raise api_core_exceptions.NotFound(f'Session not found: {session_id}')
async def _list_sessions(self, name: str, config: dict[str, Any]):
filter_val = config.get('filter', '')
user_id_match = re.search(r'user_id="([^"]+)"', filter_val)
if user_id_match:
user_id = user_id_match.group(1)
if user_id == 'user_with_pages':
return [
_convert_to_object(MOCK_SESSION_JSON_PAGE1),
_convert_to_object(MOCK_SESSION_JSON_PAGE2),
]
return [
_convert_to_object(session)
for session in self.session_dict.values()
if session['user_id'] == user_id
]
# No user filter, return all sessions
return [
_convert_to_object(session) for session in self.session_dict.values()
]
async def _delete_session(self, name: str):
session_id = name.split('/')[-1]
self.session_dict.pop(session_id)
async def _create_session(
self, name: str, user_id: str, config: dict[str, Any]
):
self.last_create_session_config = config
new_session_id = '4'
self.session_dict[new_session_id] = {
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/'
+ new_session_id
),
'user_id': user_id,
'session_state': config.get('session_state', {}),
'update_time': '2024-12-12T12:12:12.123456Z',
}
return _convert_to_object({
'name': (
'projects/test_project/locations/test_location/'
'reasoningEngines/123/sessions/'
+ new_session_id
+ '/operations/111'
),
'done': True,
'response': self.session_dict['4'],
})
async def _list_events(self, name: str, **kwargs):
session_id = name.split('/')[-1]
events = []
if session_id in self.event_dict:
events_tuple = self.event_dict[session_id]
events.extend(events_tuple[0])
if events_tuple[1] == 'my_token':
events.extend(MOCK_EVENT_JSON_3)
config = kwargs.get('config', {})
filter_str = config.get('filter', None)
if filter_str:
match = re.search(r'timestamp>="([^"]+)"', filter_str)
if match:
after_timestamp_str = match.group(1)
after_timestamp = isoparse(after_timestamp_str)
events = [
event
for event in events
if isoparse(event['timestamp']) >= after_timestamp
]
return to_async_iterator([_convert_to_object(event) for event in events])
async def _append_event(
self,
name: str,
author: str,
invocation_id: str,
timestamp: Any,
config: dict[str, Any],
):
session_id = name.split('/')[-1]
event_list, token = self.event_dict.get(session_id, ([], None))
event_id = str(len(event_list) + 1000) # generate unique ID
event_timestamp_str = timestamp.isoformat().replace('+00:00', 'Z')
event_json = {
'name': f'{name}/events/{event_id}',
'invocation_id': invocation_id,
'author': author,
'timestamp': event_timestamp_str,
}
event_json.update(config)
if session_id in self.session_dict:
self.session_dict[session_id]['update_time'] = event_timestamp_str
if session_id in self.event_dict:
self.event_dict[session_id][0].append(event_json)
else:
self.event_dict[session_id] = ([event_json], None)
class MockAsyncClientWithPagination:
"""Mock client that simulates pagination requiring an open client connection.
This mock tracks whether the client context is active and raises RuntimeError
if iteration occurs outside the context, simulating the real httpx behavior.
"""
def __init__(self, session_data: dict, events_pages: list[list[dict]]):
self._session_data = session_data
self._events_pages = events_pages
self._context_active = False
self.agent_engines = mock.AsyncMock()
self.agent_engines.sessions.get.side_effect = self._get_session
self.agent_engines.sessions.events.list.side_effect = self._list_events
async def __aenter__(self):
self._context_active = True
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
self._context_active = False
async def _get_session(self, name: str):
return _convert_to_object(self._session_data)
async def _list_events(self, name: str, **kwargs):
return self._paginated_events_iterator()
async def _paginated_events_iterator(self):
for page in self._events_pages:
for event in page:
if not self._context_active:
raise RuntimeError(
'Cannot send a request, as the client has been closed.'
)
yield _convert_to_object(event)
def _generate_events_for_page(session_id: str, start_idx: int, count: int):
events = []
start_time = isoparse('2024-12-12T12:12:12.123456Z')
for i in range(count):
idx = start_idx + i
event_time = start_time + datetime.timedelta(microseconds=idx * 1000)
events.append({
'name': (
'projects/test-project/locations/test-location/'
f'reasoningEngines/123/sessions/{session_id}/events/{idx}'
),
'invocation_id': f'invocation_{idx}',
'author': 'pagination_user',
'timestamp': event_time.isoformat().replace('+00:00', 'Z'),
})
return events
@pytest.mark.asyncio
async def test_get_session_pagination_keeps_client_open():
"""Regression test: event iteration must occur inside the api_client context.
This test verifies that get_session() keeps the API client open while
iterating through paginated events. Before the fix, the events_iterator
was consumed outside the async with block, causing RuntimeError when
fetching subsequent pages.
"""
session_data = {
'name': (
'projects/test-project/locations/test-location/'
'reasoningEngines/123/sessions/pagination_test'
),
'update_time': '2024-12-12T12:12:12.123456Z',
'user_id': 'pagination_user',
}
page1_events = _generate_events_for_page('pagination_test', 0, 100)
page2_events = _generate_events_for_page('pagination_test', 100, 100)
page3_events = _generate_events_for_page('pagination_test', 200, 50)
mock_client = MockAsyncClientWithPagination(
session_data=session_data,
events_pages=[page1_events, page2_events, page3_events],
)
session_service = mock_vertex_ai_session_service()
with mock.patch.object(
session_service, '_get_api_client', return_value=mock_client
):
session = await session_service.get_session(
app_name='123', user_id='pagination_user', session_id='pagination_test'
)
assert session is not None
assert len(session.events) == 250
assert session.events[0].invocation_id == 'invocation_0'
assert session.events[249].invocation_id == 'invocation_249'
def mock_vertex_ai_session_service(
project: Optional[str] = 'test-project',
location: Optional[str] = 'test-location',
agent_engine_id: Optional[str] = None,
express_mode_api_key: Optional[str] = None,
):
"""Creates a mock Vertex AI Session service for testing."""
return VertexAiSessionService(
project=project,
location=location,
agent_engine_id=agent_engine_id,
express_mode_api_key=express_mode_api_key,
)
@pytest.fixture
def mock_api_client_instance():
"""Creates a mock API client instance for testing."""
api_client = MockAsyncClient()
api_client.session_dict = {
'1': MOCK_SESSION_JSON_1,
'2': MOCK_SESSION_JSON_2,
'3': MOCK_SESSION_JSON_3,
'page1': MOCK_SESSION_JSON_PAGE1,
'page2': MOCK_SESSION_JSON_PAGE2,
}
api_client.event_dict = {
'1': (copy.deepcopy(MOCK_EVENT_JSON), None),
'2': (copy.deepcopy(MOCK_EVENT_JSON_2), 'my_token'),
}
return api_client
@pytest.fixture
def mock_get_api_client(mock_api_client_instance):
"""Mocks the _get_api_client method to return a mock API client."""
with mock.patch(
'google.adk.sessions.vertex_ai_session_service.VertexAiSessionService._get_api_client',
return_value=mock_api_client_instance,
):
yield
@pytest.mark.asyncio
async def test_initialize_with_project_location_and_api_key_error():
with pytest.raises(ValueError) as excinfo:
mock_vertex_ai_session_service(
project='test-project',
location='test-location',
express_mode_api_key='test-api-key',
)
assert (
'Cannot specify project or location and express_mode_api_key. Either use'
' project and location, or just the express_mode_api_key.'
in str(excinfo.value)
)
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
@pytest.mark.parametrize('agent_engine_id', [None, '123'])
async def test_get_empty_session(agent_engine_id):
session_service = mock_vertex_ai_session_service(agent_engine_id)
with pytest.raises(api_core_exceptions.NotFound) as excinfo:
await session_service.get_session(
app_name='123', user_id='user', session_id='0'
)
assert str(excinfo.value) == '404 Session not found: 0'
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
@pytest.mark.parametrize('agent_engine_id', [None, '123'])
async def test_get_another_user_session(agent_engine_id):
session_service = mock_vertex_ai_session_service(agent_engine_id)
with pytest.raises(ValueError) as excinfo:
await session_service.get_session(
app_name='123', user_id='user2', session_id='1'
)
assert str(excinfo.value) == 'Session 1 does not belong to user user2.'
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_get_and_delete_session():
session_service = mock_vertex_ai_session_service()
assert (
await session_service.get_session(
app_name='123', user_id='user', session_id='1'
)
== MOCK_SESSION
)
await session_service.delete_session(
app_name='123', user_id='user', session_id='1'
)
with pytest.raises(api_core_exceptions.NotFound) as excinfo:
await session_service.get_session(
app_name='123', user_id='user', session_id='1'
)
assert str(excinfo.value) == '404 Session not found: 1'
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_get_session_with_page_token():
session_service = mock_vertex_ai_session_service()
assert (
await session_service.get_session(
app_name='123', user_id='user', session_id='2'
)
== MOCK_SESSION_2
)
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_get_session_with_after_timestamp_filter():
session_service = mock_vertex_ai_session_service()
session = await session_service.get_session(
app_name='123',
user_id='user',
session_id='2',
config=GetSessionConfig(
after_timestamp=isoparse('2024-12-12T12:12:13.0Z').timestamp()
),
)
assert session is not None
assert len(session.events) == 1
assert session.events[0].id == '456'
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_get_session_keeps_events_newer_than_update_time(
mock_api_client_instance: MockAsyncClient,
) -> None:
future_event_time = isoparse(
MOCK_SESSION_JSON_1['update_time']
) + datetime.timedelta(seconds=1)
event = mock_api_client_instance.event_dict['1'][0][0]
event['timestamp'] = future_event_time.isoformat().replace('+00:00', 'Z')
session_service = mock_vertex_ai_session_service()
session = await session_service.get_session(
app_name='123', user_id='user', session_id='1'
)
assert session is not None
assert len(session.events) == 1
assert session.events[0].timestamp == future_event_time.timestamp()
assert session.events[0].timestamp > session.last_update_time, (
'Event timestamp should exceed session update_time to guard against'
' filtering.'
)
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_get_session_with_many_events(mock_api_client_instance):
mock_api_client_instance.session_dict['5'] = MOCK_SESSION_JSON_5
mock_api_client_instance.event_dict['5'] = (
copy.deepcopy(MOCK_EVENTS_JSON_5),
None,
)
session_service = mock_vertex_ai_session_service()
session = await session_service.get_session(
app_name='123', user_id='user_with_many_events', session_id='5'
)
assert session is not None
assert len(session.events) == MANY_EVENTS_COUNT
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_list_sessions():
session_service = mock_vertex_ai_session_service()
sessions = await session_service.list_sessions(app_name='123', user_id='user')
assert len(sessions.sessions) == 2
assert sessions.sessions[0].id == '1'
assert sessions.sessions[1].id == '2'
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_list_sessions_with_pagination():
session_service = mock_vertex_ai_session_service()
sessions = await session_service.list_sessions(
app_name='123', user_id='user_with_pages'
)
assert len(sessions.sessions) == 2
assert sessions.sessions[0].id == 'page1'
assert sessions.sessions[1].id == 'page2'
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_list_sessions_all_users():
session_service = mock_vertex_ai_session_service()
sessions = await session_service.list_sessions(app_name='123', user_id=None)
assert len(sessions.sessions) == 5
assert {s.id for s in sessions.sessions} == {
'1',
'2',
'3',
'page1',
'page2',
}
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_create_session():
session_service = mock_vertex_ai_session_service()
state = {'key': 'value'}
session = await session_service.create_session(
app_name='123', user_id='user', state=state
)
assert session.state == state
assert session.app_name == '123'
assert session.user_id == 'user'
assert session.last_update_time is not None
session_id = session.id
assert session == await session_service.get_session(
app_name='123', user_id='user', session_id=session_id
)
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_create_session_with_custom_session_id():
session_service = mock_vertex_ai_session_service()
with pytest.raises(ValueError) as excinfo:
await session_service.create_session(
app_name='123', user_id='user', session_id='1'
)
assert str(excinfo.value) == (
'User-provided Session id is not supported for VertexAISessionService.'
)
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_create_session_with_custom_config(mock_api_client_instance):
session_service = mock_vertex_ai_session_service()
expire_time = '2025-12-12T12:12:12.123456Z'
await session_service.create_session(
app_name='123', user_id='user', expire_time=expire_time
)
assert (
mock_api_client_instance.last_create_session_config['expire_time']
== expire_time
)
@pytest.mark.asyncio
@pytest.mark.usefixtures('mock_get_api_client')
async def test_append_event():
session_service = mock_vertex_ai_session_service()
session_before_append = await session_service.get_session(
app_name='123', user_id='user', session_id='1'
)
event_to_append = Event(
invocation_id='new_invocation',
author='model',
timestamp=1734005533.0,
content=genai_types.Content(parts=[genai_types.Part(text='new_content')]),
actions=EventActions(
transfer_to_agent='another_agent',
state_delta={'new_key': 'new_value'},
skip_summarization=True,
requested_auth_configs={
'test_auth': AuthConfig(
auth_scheme=auth_schemes.OAuth2(
flows=openapi_models.OAuthFlows(
implicit=openapi_models.OAuthFlowImplicit(
authorizationUrl='http://test.com/auth',
scopes={},
)
)
),
),
},
),
error_code='1',
error_message='test_error',
branch='test_branch',
custom_metadata={'custom': 'data'},
long_running_tool_ids={'tool2'},
)
await session_service.append_event(session_before_append, event_to_append)
retrieved_session = await session_service.get_session(
app_name='123', user_id='user', session_id='1'
)
assert len(retrieved_session.events) == 2
event_to_append.id = retrieved_session.events[1].id
assert retrieved_session.events[1] == event_to_append