@@ -877,5 +877,73 @@ def iter_lines(self):
877877 c .close ()
878878
879879
880+ class TestClientResetHttp (unittest .TestCase ):
881+ """_reset_http replaces the httpx client so a poisoned connection
882+ pool does not doom subsequent requests."""
883+
884+ def test_reset_http_replaces_client (self ):
885+ c = make_offline_client ()
886+ old = c ._http
887+ c ._reset_http ()
888+ self .assertIsNot (c ._http , old )
889+ self .assertFalse (c ._http .is_closed )
890+ c .close ()
891+
892+ def test_reset_http_closes_old_client (self ):
893+ c = make_offline_client ()
894+ old = c ._http
895+ with mock .patch .object (old , "close" ) as cl :
896+ c ._reset_http ()
897+ cl .assert_called_once_with ()
898+ c .close ()
899+
900+ def test_reset_http_tolerates_close_failure (self ):
901+ c = make_offline_client ()
902+ old = c ._http
903+ with mock .patch .object (old , "close" , side_effect = RuntimeError ("boom" )):
904+ c ._reset_http () # must not raise
905+ self .assertIsNot (c ._http , old )
906+ c .close ()
907+
908+ def test_connection_error_resets_pool_for_next_request (self ):
909+ """After retries exhaust on a connection error, the next chat()
910+ call uses a fresh httpx client (not the poisoned one)."""
911+ from python_agent_harness .client import ApiError
912+
913+ c = make_offline_client (retry_max = 2 , retry_base_delay = 0.01 , retry_max_delay = 0.01 )
914+ # Simulate persistent connection failures
915+ with mock .patch .object (
916+ c ._http , "stream" , side_effect = httpx .ConnectError ("refused" )
917+ ):
918+ with self .assertRaises (ApiError ):
919+ c .chat ([Message (role = "user" , content = "hi" )])
920+ # After the error, _http must be a fresh (non-poisoned) client
921+ # that was NOT the one we patched
922+ self .assertFalse (c ._http .is_closed )
923+ # Verify it's a different object (reset happened)
924+ # The patched mock is on the OLD client; the new one is real
925+ self .assertNotIsInstance (c ._http .stream , mock .Mock )
926+ c .close ()
927+
928+ def test_cancel_during_backoff_also_resets_pool (self ):
929+ """If cancel_check fires during retry backoff, the pool is
930+ still reset so the next run starts clean."""
931+ from python_agent_harness .client import ApiError
932+
933+ c = make_offline_client (retry_max = 3 , retry_base_delay = 60.0 , retry_max_delay = 60.0 )
934+ old_http = c ._http
935+ with mock .patch .object (
936+ c ._http , "stream" , side_effect = httpx .ConnectError ("refused" )
937+ ):
938+ with self .assertRaises (ApiError ):
939+ c .chat (
940+ [Message (role = "user" , content = "hi" )],
941+ cancel_check = lambda : True ,
942+ )
943+ self .assertIsNot (c ._http , old_http )
944+ self .assertFalse (c ._http .is_closed )
945+ c .close ()
946+
947+
880948if __name__ == "__main__" :
881949 unittest .main ()
0 commit comments