# Copyright 2026 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 asyncio import types from typing import Any from fastapi.testclient import TestClient from google.adk.agents.base_agent import BaseAgent from google.adk.cli.adk_web_server import AdkWebServer from google.adk.events.event import Event from google.adk.sessions.in_memory_session_service import InMemorySessionService import pytest from starlette.websockets import WebSocketDisconnect class _DummyAgent(BaseAgent): def __init__(self) -> None: super().__init__(name="dummy_agent") self.sub_agents = [] class _DummyAgentLoader: def load_agent(self, app_name: str) -> BaseAgent: return _DummyAgent() def list_agents(self) -> list[str]: return ["test_app"] def list_agents_detailed(self) -> list[dict[str, Any]]: return [] class _CapturingRunner: def __init__(self) -> None: self.captured_run_config = None async def run_live( self, *, session, live_request_queue, run_config=None, **unused_kwargs, ): self.captured_run_config = run_config yield Event(author="runner") def test_run_live_applies_run_config_query_options(): session_service = InMemorySessionService() asyncio.run( session_service.create_session( app_name="test_app", user_id="user", session_id="session", state={}, ) ) runner = _CapturingRunner() adk_web_server = AdkWebServer( agent_loader=_DummyAgentLoader(), session_service=session_service, memory_service=types.SimpleNamespace(), artifact_service=types.SimpleNamespace(), credential_service=types.SimpleNamespace(), eval_sets_manager=types.SimpleNamespace(), eval_set_results_manager=types.SimpleNamespace(), agents_dir=".", ) async def _get_runner_async(_self, _app_name: str): return runner adk_web_server.get_runner_async = _get_runner_async.__get__(adk_web_server) # pytype: disable=attribute-error fast_api_app = adk_web_server.get_fast_api_app( setup_observer=lambda _observer, _server: None, tear_down_observer=lambda _observer, _server: None, ) client = TestClient(fast_api_app) url = ( "/run_live" "?app_name=test_app" "&user_id=user" "&session_id=session" "&modalities=TEXT" "&modalities=AUDIO" "&proactive_audio=true" "&enable_affective_dialog=true" "&enable_session_resumption=true" "&save_live_blob=true" "&explicit_vad_signal=true" ) with client.websocket_connect(url) as ws: _ = ws.receive_text() run_config = runner.captured_run_config assert run_config is not None assert run_config.response_modalities == ["TEXT", "AUDIO"] assert run_config.enable_affective_dialog is True assert run_config.proactivity is not None assert run_config.proactivity.proactive_audio is True assert run_config.session_resumption is not None assert run_config.session_resumption.transparent is True assert run_config.save_live_blob is True assert run_config.explicit_vad_signal is True @pytest.mark.parametrize( ( "query,expected_enable_affective,expected_proactive_audio," "expected_session_resumption_transparent,expected_save_live_blob," "expected_explicit_vad_signal" ), [ ("", None, None, None, False, None), ("&proactive_audio=true", None, True, None, False, None), ("&proactive_audio=false", None, False, None, False, None), ("&enable_affective_dialog=true", True, None, None, False, None), ("&enable_affective_dialog=false", False, None, None, False, None), ("&enable_session_resumption=true", None, None, True, False, None), ("&enable_session_resumption=false", None, None, False, False, None), ("&save_live_blob=true", None, None, None, True, None), ("&save_live_blob=false", None, None, None, False, None), ("&explicit_vad_signal=true", None, None, None, False, True), ("&explicit_vad_signal=false", None, None, None, False, False), ], ) def test_run_live_defaults_and_individual_options( query: str, expected_enable_affective: bool | None, expected_proactive_audio: bool | None, expected_session_resumption_transparent: bool | None, expected_save_live_blob: bool, expected_explicit_vad_signal: bool | None, ): session_service = InMemorySessionService() asyncio.run( session_service.create_session( app_name="test_app", user_id="user", session_id="session", state={}, ) ) runner = _CapturingRunner() adk_web_server = AdkWebServer( agent_loader=_DummyAgentLoader(), session_service=session_service, memory_service=types.SimpleNamespace(), artifact_service=types.SimpleNamespace(), credential_service=types.SimpleNamespace(), eval_sets_manager=types.SimpleNamespace(), eval_set_results_manager=types.SimpleNamespace(), agents_dir=".", ) async def _get_runner_async(_self, _app_name: str): return runner adk_web_server.get_runner_async = _get_runner_async.__get__(adk_web_server) # pytype: disable=attribute-error fast_api_app = adk_web_server.get_fast_api_app( setup_observer=lambda _observer, _server: None, tear_down_observer=lambda _observer, _server: None, ) client = TestClient(fast_api_app) url = ( "/run_live" "?app_name=test_app" "&user_id=user" "&session_id=session" "&modalities=AUDIO" f"{query}" ) with client.websocket_connect(url) as ws: _ = ws.receive_text() run_config = runner.captured_run_config assert run_config is not None assert run_config.enable_affective_dialog == expected_enable_affective if expected_proactive_audio is None: assert run_config.proactivity is None else: assert run_config.proactivity is not None assert run_config.proactivity.proactive_audio is expected_proactive_audio if expected_session_resumption_transparent is None: assert run_config.session_resumption is None else: assert run_config.session_resumption is not None assert ( run_config.session_resumption.transparent is expected_session_resumption_transparent ) assert run_config.save_live_blob is expected_save_live_blob assert run_config.explicit_vad_signal is expected_explicit_vad_signal _WS_BASE_URL = ( "/run_live" "?app_name=test_app" "&user_id=user" "&session_id=session" "&modalities=AUDIO" ) def _build_ws_client(): """Build a TestClient wired to a capturing runner.""" session_service = InMemorySessionService() asyncio.run( session_service.create_session( app_name="test_app", user_id="user", session_id="session", state={}, ) ) runner = _CapturingRunner() adk_web_server = AdkWebServer( agent_loader=_DummyAgentLoader(), session_service=session_service, memory_service=types.SimpleNamespace(), artifact_service=types.SimpleNamespace(), credential_service=types.SimpleNamespace(), eval_sets_manager=types.SimpleNamespace(), eval_set_results_manager=types.SimpleNamespace(), agents_dir=".", ) async def _get_runner_async(_self, _app_name: str): return runner adk_web_server.get_runner_async = _get_runner_async.__get__(adk_web_server) # pytype: disable=attribute-error fast_api_app = adk_web_server.get_fast_api_app( setup_observer=lambda _observer, _server: None, tear_down_observer=lambda _observer, _server: None, ) return TestClient(fast_api_app) def test_run_live_rejects_disallowed_origin(): client = _build_ws_client() with pytest.raises(WebSocketDisconnect) as exc_info: with client.websocket_connect( _WS_BASE_URL, headers={"origin": "https://evil.com"}, ) as ws: ws.receive_text() assert exc_info.value.code == 1008 def test_run_live_allows_matching_origin(): client = _build_ws_client() with client.websocket_connect( _WS_BASE_URL, headers={"origin": "http://testserver"}, ) as ws: _ = ws.receive_text() def test_run_live_allows_no_origin_header(): """Non-browser clients (curl, wscat, SDKs) send no Origin header.""" client = _build_ws_client() with client.websocket_connect(_WS_BASE_URL) as ws: _ = ws.receive_text()