From fe24e722ca6dd25f6a595d052c0e03aad62a7745 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sat, 17 Jan 2026 09:25:15 +0530 Subject: [PATCH 01/18] fix(web): allow session resume without new message (#4100) Signed-off-by: Akshat Kumar --- src/google/adk/cli/adk_web_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/cli/adk_web_server.py b/src/google/adk/cli/adk_web_server.py index b97932d042..570da8a97d 100644 --- a/src/google/adk/cli/adk_web_server.py +++ b/src/google/adk/cli/adk_web_server.py @@ -202,7 +202,7 @@ class RunAgentRequest(common.BaseModel): app_name: str user_id: str session_id: str - new_message: types.Content + new_message: Optional[types.Content] = None streaming: bool = False state_delta: Optional[dict[str, Any]] = None # for resume long-running functions From 1f4f0aa049923bb2418e62eeb4b26446daae05ec Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sat, 17 Jan 2026 10:14:50 +0530 Subject: [PATCH 02/18] fix(web): adds a unit test verifying that the /run endpoint accepts requests for session resumption without providing new content. Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 0c69605349..904492a6fe 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1411,5 +1411,29 @@ def test_builder_save_rejects_traversal(builder_test_client, tmp_path): assert not (tmp_path / "app" / "tmp" / "escape.yaml").exists() +def test_agent_run_resume_without_message(test_app, create_test_session): + """Test that /run allows resuming a session without providing a new message.""" + info = create_test_session + url = "/run" + # Note: 'new_message' is intentionally OMITTED here to test the fix + payload = { + "app_name": info["app_name"], + "user_id": info["user_id"], + "session_id": info["session_id"], + "streaming": False, + } + + response = test_app.post(url, json=payload) + + # Before your fix, this would return 422 (Validation Error) + # Now it should return 200 (Success) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + # Our mock runner (dummy_run_async) returns 3 events by default + assert len(data) == 3 + assert data[0]["author"] == "dummy agent" + + logger.info("Agent run resume without message test passed") if __name__ == "__main__": pytest.main(["-xvs", __file__]) From 929fe2e98f8d53bbefc0f7da1456c2d582fd4b26 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sat, 17 Jan 2026 11:07:58 +0530 Subject: [PATCH 03/18] fix: allow agent session resumption without new message Signed-off-by: Akshat Kumar --- src/google/adk/runners.py | 8 +++----- tests/unittests/cli/test_fast_api.py | 26 +++++++++++++++----------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 3aaa54e257..730f489fae 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -498,11 +498,9 @@ async def _run_with_trace( user_id=user_id, session_id=session_id ) if not invocation_id and not new_message: - raise ValueError( - 'Running an agent requires either a new_message or an ' - 'invocation_id to resume a previous invocation. ' - f'Session: {session_id}, User: {user_id}' - ) + # If nothing is provided, this is a no-op resume. We return early + # without yielding any events. + return if invocation_id: if ( diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 904492a6fe..236dccf553 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1411,11 +1411,19 @@ def test_builder_save_rejects_traversal(builder_test_client, tmp_path): assert not (tmp_path / "app" / "tmp" / "escape.yaml").exists() -def test_agent_run_resume_without_message(test_app, create_test_session): +def test_agent_run_resume_without_message(test_app, create_test_session, monkeypatch): """Test that /run allows resuming a session without providing a new message.""" info = create_test_session url = "/run" - # Note: 'new_message' is intentionally OMITTED here to test the fix + + # We simulate the NEW behavior of the real runner (returning no events) + async def mock_run_empty(*args, **kwargs): + if False: yield # Tells Python this is an async generator + return + + # Apply the mock to the Runner class + monkeypatch.setattr(Runner, "run_async", mock_run_empty) + payload = { "app_name": info["app_name"], "user_id": info["user_id"], @@ -1425,15 +1433,11 @@ def test_agent_run_resume_without_message(test_app, create_test_session): response = test_app.post(url, json=payload) - # Before your fix, this would return 422 (Validation Error) - # Now it should return 200 (Success) + # Verify the web server accepts it and the runner returns a clean empty list assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - # Our mock runner (dummy_run_async) returns 3 events by default - assert len(data) == 3 - assert data[0]["author"] == "dummy agent" - - logger.info("Agent run resume without message test passed") + assert response.json() == [] + + logger.info("Agent run resume without message test passed gracefully") + if __name__ == "__main__": pytest.main(["-xvs", __file__]) From 1afb89aac3e9283433b8e9a9ffb6bd3c58fb19a9 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sat, 17 Jan 2026 11:49:29 +0530 Subject: [PATCH 04/18] Style: cleanup unit test by removing unnecessary logging and blank lines Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 236dccf553..650ffd11fb 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1415,10 +1415,9 @@ def test_agent_run_resume_without_message(test_app, create_test_session, monkeyp """Test that /run allows resuming a session without providing a new message.""" info = create_test_session url = "/run" - # We simulate the NEW behavior of the real runner (returning no events) async def mock_run_empty(*args, **kwargs): - if False: yield # Tells Python this is an async generator + if False: yield # Tells Python this is an async generator return # Apply the mock to the Runner class @@ -1437,7 +1436,5 @@ async def mock_run_empty(*args, **kwargs): assert response.status_code == 200 assert response.json() == [] - logger.info("Agent run resume without message test passed gracefully") - if __name__ == "__main__": pytest.main(["-xvs", __file__]) From d10c1d319b7b43bfc8bb3244d37525f553227801 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sat, 17 Jan 2026 12:13:21 +0530 Subject: [PATCH 05/18] fix: add observability logging for no-op resume and cleanup tests Signed-off-by: Akshat Kumar --- src/google/adk/runners.py | 6 ++++++ tests/unittests/cli/test_fast_api.py | 10 ++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 730f489fae..cb57b821e5 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -497,7 +497,13 @@ async def _run_with_trace( session = await self._get_or_create_session( user_id=user_id, session_id=session_id ) + if not invocation_id and not new_message: + logger.info( + 'Performing no-op resume for session %s: no new_message or ' + 'invocation_id.', + session_id, + ) # If nothing is provided, this is a no-op resume. We return early # without yielding any events. return diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 650ffd11fb..0565a04a9b 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1415,26 +1415,20 @@ def test_agent_run_resume_without_message(test_app, create_test_session, monkeyp """Test that /run allows resuming a session without providing a new message.""" info = create_test_session url = "/run" - # We simulate the NEW behavior of the real runner (returning no events) async def mock_run_empty(*args, **kwargs): - if False: yield # Tells Python this is an async generator + if False: yield return - # Apply the mock to the Runner class monkeypatch.setattr(Runner, "run_async", mock_run_empty) - payload = { "app_name": info["app_name"], "user_id": info["user_id"], "session_id": info["session_id"], "streaming": False, } - response = test_app.post(url, json=payload) - - # Verify the web server accepts it and the runner returns a clean empty list assert response.status_code == 200 assert response.json() == [] - + if __name__ == "__main__": pytest.main(["-xvs", __file__]) From 00b0f203fbb7662667196ccc51bd82a5564d9f4c Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sat, 17 Jan 2026 12:32:35 +0530 Subject: [PATCH 06/18] fix: align dummy runner logic with real runner and update docstring Signed-off-by: Akshat Kumar --- src/google/adk/runners.py | 3 +-- tests/unittests/cli/test_fast_api.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index cb57b821e5..9a871f0945 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -481,8 +481,7 @@ async def run_async( The events generated by the agent. Raises: - ValueError: If the session is not found; If both invocation_id and - new_message are None. + If both invocation_id and new_message are None, a no-op resume is performed. """ run_config = run_config or RunConfig() diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 0565a04a9b..6e05114d38 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -132,6 +132,10 @@ async def dummy_run_async( run_config: Optional[RunConfig] = None, invocation_id: Optional[str] = None, ): + + if not invocation_id and not new_message: + return + run_config = run_config or RunConfig() yield _event_1() await asyncio.sleep(0) @@ -1411,22 +1415,21 @@ def test_builder_save_rejects_traversal(builder_test_client, tmp_path): assert not (tmp_path / "app" / "tmp" / "escape.yaml").exists() -def test_agent_run_resume_without_message(test_app, create_test_session, monkeypatch): +def test_agent_run_resume_without_message(test_app, create_test_session): """Test that /run allows resuming a session without providing a new message.""" info = create_test_session url = "/run" - async def mock_run_empty(*args, **kwargs): - if False: yield - return - - monkeypatch.setattr(Runner, "run_async", mock_run_empty) + # We no longer mock the runner. This tests the real logic in runners.py payload = { "app_name": info["app_name"], "user_id": info["user_id"], "session_id": info["session_id"], "streaming": False, } + response = test_app.post(url, json=payload) + + # Verify the web server and real runner work together to return success assert response.status_code == 200 assert response.json() == [] From 29879319f073c83e0da18dd547ee707956b9a8b2 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sat, 17 Jan 2026 12:38:16 +0530 Subject: [PATCH 07/18] docs: remove misleading comment in unit test per bot feedback Signed-off-by: Akshat Kumar --- src/google/adk/runners.py | 2 +- tests/unittests/cli/test_fast_api.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 9a871f0945..f9e5b4eb0d 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -481,7 +481,7 @@ async def run_async( The events generated by the agent. Raises: - If both invocation_id and new_message are None, a no-op resume is performed. + ValueError: If the session is not found and auto-creation is disabled. """ run_config = run_config or RunConfig() diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 6e05114d38..34bb72b363 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1419,7 +1419,6 @@ def test_agent_run_resume_without_message(test_app, create_test_session): """Test that /run allows resuming a session without providing a new message.""" info = create_test_session url = "/run" - # We no longer mock the runner. This tests the real logic in runners.py payload = { "app_name": info["app_name"], "user_id": info["user_id"], @@ -1429,7 +1428,7 @@ def test_agent_run_resume_without_message(test_app, create_test_session): response = test_app.post(url, json=payload) - # Verify the web server and real runner work together to return success + # Verify the web server and dummy runner work together to return success assert response.status_code == 200 assert response.json() == [] From c2511c597df6641de7a03bb561cf60aa91eacae8 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 18 Jan 2026 22:11:21 +0530 Subject: [PATCH 08/18] fix: handle state_delta warning in no-op resume and cleanup dummy runner Signed-off-by: Akshat Kumar --- src/google/adk/runners.py | 12 +++++++++--- tests/unittests/cli/test_fast_api.py | 2 ++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index f9e5b4eb0d..0b11f5285a 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -498,10 +498,16 @@ async def _run_with_trace( ) if not invocation_id and not new_message: + if state_delta: + logger.warning( + 'state_delta provided without new_message or invocation_id for ' + 'session %s. The state_delta will be ignored.', + session_id, + ) logger.info( - 'Performing no-op resume for session %s: no new_message or ' - 'invocation_id.', - session_id, + 'Performing no-op resume for session %s: no new_message or ' + 'invocation_id.', + session_id, ) # If nothing is provided, this is a no-op resume. We return early # without yielding any events. diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 34bb72b363..c0ade12b3a 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -134,6 +134,8 @@ async def dummy_run_async( ): if not invocation_id and not new_message: + if state_delta: + logger.warning("state_delta ignored in no-op resume") return run_config = run_config or RunConfig() From ac9f90447cbc39d463f16e45b059b4eae4fe47e9 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Wed, 21 Jan 2026 19:21:53 +0530 Subject: [PATCH 09/18] fix: the formatting Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index c0ade12b3a..1abd14e2ad 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -132,7 +132,7 @@ async def dummy_run_async( run_config: Optional[RunConfig] = None, invocation_id: Optional[str] = None, ): - + if not invocation_id and not new_message: if state_delta: logger.warning("state_delta ignored in no-op resume") @@ -1434,5 +1434,6 @@ def test_agent_run_resume_without_message(test_app, create_test_session): assert response.status_code == 200 assert response.json() == [] + if __name__ == "__main__": pytest.main(["-xvs", __file__]) From 1fdf7efd15f070c82937671093e6383ad9d9b304 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sat, 24 Jan 2026 23:16:50 +0530 Subject: [PATCH 10/18] fix: finalize session resumption logic and test coverage Signed-off-by: Akshat Kumar --- src/google/adk/runners.py | 3 ++- tests/unittests/cli/test_fast_api.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 0b11f5285a..8a0203bbb9 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -481,7 +481,8 @@ async def run_async( The events generated by the agent. Raises: - ValueError: If the session is not found and auto-creation is disabled. + None: if both invocation_id and new_message are None, a no-op resume + is performed. """ run_config = run_config or RunConfig() diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 1abd14e2ad..bd8d1879a6 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1434,6 +1434,27 @@ def test_agent_run_resume_without_message(test_app, create_test_session): assert response.status_code == 200 assert response.json() == [] +def test_agent_run_resume_without_message_with_state_delta( + test_app, create_test_session, caplog +): + """Test that /run with no message ignores state_delta and logs a warning.""" + info = create_test_session + url = "/run" + payload = { + "app_name": info["app_name"], + "user_id": info["user_id"], + "session_id": info["session_id"], + "streaming": False, + "state_delta": {"some_key": "some_value"}, + } + + caplog.set_level(logging.WARNING) + response = test_app.post(url, json=payload) + + assert response.status_code == 200 + assert response.json() == [] + # Verifies the warning you added to runners.py + assert "state_delta ignored in no-op resume" in caplog.text if __name__ == "__main__": pytest.main(["-xvs", __file__]) From 5af99c7a0e5be4202c5300c831fb3551a1c09f4b Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 25 Jan 2026 00:31:27 +0530 Subject: [PATCH 11/18] fix: align mock warning with production and update docstring Signed-off-by: Akshat Kumar --- src/google/adk/runners.py | 3 +-- tests/unittests/cli/test_fast_api.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 8a0203bbb9..057df62563 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -481,8 +481,7 @@ async def run_async( The events generated by the agent. Raises: - None: if both invocation_id and new_message are None, a no-op resume - is performed. + ValueError: If the session is not found and `auto_create_session` is False. """ run_config = run_config or RunConfig() diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index bd8d1879a6..70ddc0a4d0 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -135,7 +135,11 @@ async def dummy_run_async( if not invocation_id and not new_message: if state_delta: - logger.warning("state_delta ignored in no-op resume") + logger.warning( + "state_delta provided without new_message or invocation_id for " + "session %s. The state_delta will be ignored.", + session_id, + ) return run_config = run_config or RunConfig() @@ -1434,6 +1438,7 @@ def test_agent_run_resume_without_message(test_app, create_test_session): assert response.status_code == 200 assert response.json() == [] + def test_agent_run_resume_without_message_with_state_delta( test_app, create_test_session, caplog ): @@ -1454,7 +1459,10 @@ def test_agent_run_resume_without_message_with_state_delta( assert response.status_code == 200 assert response.json() == [] # Verifies the warning you added to runners.py - assert "state_delta ignored in no-op resume" in caplog.text + assert ( + "state_delta provided without new_message or invocation_id" in caplog.text + ) + if __name__ == "__main__": pytest.main(["-xvs", __file__]) From e3ed2692cd177e5ed9d63b2613a5eca91acc26aa Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 25 Jan 2026 00:40:08 +0530 Subject: [PATCH 12/18] test: improve robustness of no-op resume warning assertion Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 70ddc0a4d0..9321ba8b1e 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1458,9 +1458,13 @@ def test_agent_run_resume_without_message_with_state_delta( assert response.status_code == 200 assert response.json() == [] - # Verifies the warning you added to runners.py + + # Robust log verification as requested by the code review + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "WARNING" assert ( - "state_delta provided without new_message or invocation_id" in caplog.text + "state_delta provided without new_message or invocation_id" + in caplog.records[0].message ) From d8d340148607d8172f1777e6f3ef72d117a8b4ac Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 25 Jan 2026 00:46:08 +0530 Subject: [PATCH 13/18] test: improve robustness of no-op resume warning assertion Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 9321ba8b1e..b636ec4c9e 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -140,6 +140,11 @@ async def dummy_run_async( "session %s. The state_delta will be ignored.", session_id, ) + logger.info( + "Performing no-op resume for session %s: no new_message or " + "invocation_id.", + session_id, + ) return run_config = run_config or RunConfig() From c5ac29afb5ed9e38b0c29de423a9c848c4789d81 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 25 Jan 2026 01:42:27 +0530 Subject: [PATCH 14/18] fix: implement graceful session resumption and separate test concerns Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 14 +++-------- tests/unittests/test_runners.py | 37 +++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index b636ec4c9e..dd453022d5 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1445,9 +1445,9 @@ def test_agent_run_resume_without_message(test_app, create_test_session): def test_agent_run_resume_without_message_with_state_delta( - test_app, create_test_session, caplog + test_app, create_test_session ): - """Test that /run with no message ignores state_delta and logs a warning.""" + """Test that /run with no message accepts the request even with state_delta.""" info = create_test_session url = "/run" payload = { @@ -1458,20 +1458,12 @@ def test_agent_run_resume_without_message_with_state_delta( "state_delta": {"some_key": "some_value"}, } - caplog.set_level(logging.WARNING) response = test_app.post(url, json=payload) + # Only verify the HTTP layer (FastAPI accepts the request) assert response.status_code == 200 assert response.json() == [] - # Robust log verification as requested by the code review - assert len(caplog.records) == 1 - assert caplog.records[0].levelname == "WARNING" - assert ( - "state_delta provided without new_message or invocation_id" - in caplog.records[0].message - ) - if __name__ == "__main__": pytest.main(["-xvs", __file__]) diff --git a/tests/unittests/test_runners.py b/tests/unittests/test_runners.py index 62b8d7334b..a9a0a4bce8 100644 --- a/tests/unittests/test_runners.py +++ b/tests/unittests/test_runners.py @@ -11,13 +11,14 @@ # 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 importlib +import logging from pathlib import Path import sys import textwrap from typing import AsyncGenerator from typing import Optional +from unittest import mock from unittest.mock import AsyncMock from google.adk.agents.base_agent import BaseAgent @@ -1321,5 +1322,39 @@ def test_infer_agent_origin_detects_mismatch_for_user_agent( assert "actual_name" in runner._app_name_alignment_hint +@pytest.mark.asyncio +async def test_run_async_no_op_resume_logging(caplog): + """Verifies that the actual Runner logic logs a warning when state_delta is ignored.""" + from google.adk.runners import Runner + + # 1. Setup dependencies + mock_agent = mock.MagicMock() + mock_agent.name = "test_agent" + + # Added app_name="test_app" to satisfy validation + runner = Runner( + app_name="test_app", + agent=mock_agent, + session_service=mock.AsyncMock(), + ) + + # 2. Capture logs while running the actual logic in runners.py + with caplog.at_level(logging.WARNING): + # Call run_async without message or invocation_id, but WITH state_delta + async for _ in runner.run_async( + user_id="user", + session_id="session", + new_message=None, + state_delta={"test": 1}, + ): + pass + + # 3. This verifies the REAL warning logic in src/google/adk/runners.py + assert any( + "state_delta provided without new_message" in r.message + for r in caplog.records + ) + + if __name__ == "__main__": pytest.main([__file__]) From a397ea3a54fbacc4fa0a4bd9b9a9f111c42b848c Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 25 Jan 2026 02:06:36 +0530 Subject: [PATCH 15/18] refactor: use targeted mocks for session resumption tests per bot feedback Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index dd453022d5..b01b249d75 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1426,8 +1426,19 @@ def test_builder_save_rejects_traversal(builder_test_client, tmp_path): assert not (tmp_path / "app" / "tmp" / "escape.yaml").exists() -def test_agent_run_resume_without_message(test_app, create_test_session): +async def _noop_run_async(*args, **kwargs): + """A mock that does nothing and yields no events for no-op resume tests.""" + if False: + yield + + +def test_agent_run_resume_without_message( + test_app, create_test_session, monkeypatch +): """Test that /run allows resuming a session without providing a new message.""" + # Override the global mock with a specific no-op mock for this test + monkeypatch.setattr("google.adk.runners.Runner.run_async", _noop_run_async) + info = create_test_session url = "/run" payload = { @@ -1439,15 +1450,18 @@ def test_agent_run_resume_without_message(test_app, create_test_session): response = test_app.post(url, json=payload) - # Verify the web server and dummy runner work together to return success + # Verify the web server handles the empty message and returns success assert response.status_code == 200 assert response.json() == [] def test_agent_run_resume_without_message_with_state_delta( - test_app, create_test_session + test_app, create_test_session, monkeypatch ): """Test that /run with no message accepts the request even with state_delta.""" + # Override the global mock with a specific no-op mock for this test + monkeypatch.setattr("google.adk.runners.Runner.run_async", _noop_run_async) + info = create_test_session url = "/run" payload = { From 222cd2d7f2fe62170154654a264f46762543ba94 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 25 Jan 2026 02:20:17 +0530 Subject: [PATCH 16/18] refactor: remove logic duplication in global mock per bot feedback Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index b01b249d75..9baf95035c 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -132,21 +132,7 @@ async def dummy_run_async( run_config: Optional[RunConfig] = None, invocation_id: Optional[str] = None, ): - - if not invocation_id and not new_message: - if state_delta: - logger.warning( - "state_delta provided without new_message or invocation_id for " - "session %s. The state_delta will be ignored.", - session_id, - ) - logger.info( - "Performing no-op resume for session %s: no new_message or " - "invocation_id.", - session_id, - ) - return - + run_config = run_config or RunConfig() yield _event_1() await asyncio.sleep(0) From 1faac6f27f354bc81df819fa58d6bf7297049e22 Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 25 Jan 2026 02:31:04 +0530 Subject: [PATCH 17/18] refactor: use parameterized tests for session resumption scenarios Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 39 +++++++++------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 9baf95035c..df940bea35 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -132,7 +132,7 @@ async def dummy_run_async( run_config: Optional[RunConfig] = None, invocation_id: Optional[str] = None, ): - + run_config = run_config or RunConfig() yield _event_1() await asyncio.sleep(0) @@ -1418,8 +1418,16 @@ async def _noop_run_async(*args, **kwargs): yield +@pytest.mark.parametrize( + "extra_payload", + [ + {}, + {"state_delta": {"some_key": "some_value"}}, + ], + ids=["no_state_delta", "with_state_delta"], +) def test_agent_run_resume_without_message( - test_app, create_test_session, monkeypatch + test_app, create_test_session, monkeypatch, extra_payload ): """Test that /run allows resuming a session without providing a new message.""" # Override the global mock with a specific no-op mock for this test @@ -1432,35 +1440,12 @@ def test_agent_run_resume_without_message( "user_id": info["user_id"], "session_id": info["session_id"], "streaming": False, + **extra_payload, } response = test_app.post(url, json=payload) - # Verify the web server handles the empty message and returns success - assert response.status_code == 200 - assert response.json() == [] - - -def test_agent_run_resume_without_message_with_state_delta( - test_app, create_test_session, monkeypatch -): - """Test that /run with no message accepts the request even with state_delta.""" - # Override the global mock with a specific no-op mock for this test - monkeypatch.setattr("google.adk.runners.Runner.run_async", _noop_run_async) - - info = create_test_session - url = "/run" - payload = { - "app_name": info["app_name"], - "user_id": info["user_id"], - "session_id": info["session_id"], - "streaming": False, - "state_delta": {"some_key": "some_value"}, - } - - response = test_app.post(url, json=payload) - - # Only verify the HTTP layer (FastAPI accepts the request) + # Verify the web server handles the request and returns success assert response.status_code == 200 assert response.json() == [] From 74bb3dbd7f2712170b3db50ee37263f6ac9982ef Mon Sep 17 00:00:00 2001 From: Akshat Kumar Date: Sun, 25 Jan 2026 02:36:15 +0530 Subject: [PATCH 18/18] style: use explicit empty loop for async generator mock per bot feedback Signed-off-by: Akshat Kumar --- tests/unittests/cli/test_fast_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index df940bea35..58e4307c13 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -1414,8 +1414,8 @@ def test_builder_save_rejects_traversal(builder_test_client, tmp_path): async def _noop_run_async(*args, **kwargs): """A mock that does nothing and yields no events for no-op resume tests.""" - if False: - yield + for item in []: + yield item @pytest.mark.parametrize(