@@ -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