@@ -151,10 +151,25 @@ def test_invalidate_client_removes_entry(self):
151151# ---------------------------------------------------------------------------
152152
153153
154- def _make_slack_api_error (error_code : str ) -> Exception :
155- """Build a mock SlackApiError whose response contains *error_code*."""
154+ def _make_slack_api_error (error_code : str , * , use_slack_response : bool = False , retry_after : str | None = None ) -> Exception :
155+ """Build a mock SlackApiError whose response contains *error_code*.
156+
157+ When *use_slack_response* is True, the response mimics a ``SlackResponse``
158+ object (with ``.data`` dict and ``.headers``) instead of a plain dict.
159+ """
156160 err = Exception (f"Slack error: { error_code } " )
157- err .response = {"error" : error_code } # type: ignore[attr-defined]
161+ if use_slack_response :
162+
163+ class _FakeSlackResponse :
164+ def __init__ (self ):
165+ self .data = {"error" : error_code }
166+ self .headers = {}
167+ if retry_after is not None :
168+ self .headers ["Retry-After" ] = retry_after
169+
170+ err .response = _FakeSlackResponse () # type: ignore[attr-defined]
171+ else :
172+ err .response = {"error" : error_code } # type: ignore[attr-defined]
158173 return err
159174
160175
@@ -193,3 +208,81 @@ def test_handle_slack_error_non_auth_error_keeps_cache(self):
193208 adapter ._handle_slack_error (_make_slack_api_error ("channel_not_found" ))
194209
195210 assert "xoxb-tok" in adapter ._client_cache , "Non-auth error should not evict the client"
211+
212+ def test_handle_slack_error_slack_response_object (self ):
213+ """Auth eviction must work when resp is a SlackResponse (not a dict)."""
214+ adapter = _make_adapter (bot_token = "xoxb-tok" )
215+ adapter ._get_client ("xoxb-tok" )
216+ assert "xoxb-tok" in adapter ._client_cache
217+
218+ with pytest .raises (Exception , match = "invalid_auth" ):
219+ adapter ._handle_slack_error (
220+ _make_slack_api_error ("invalid_auth" , use_slack_response = True )
221+ )
222+
223+ assert "xoxb-tok" not in adapter ._client_cache
224+
225+
226+ # ---------------------------------------------------------------------------
227+ # _handle_slack_error — rate limiting
228+ # ---------------------------------------------------------------------------
229+
230+
231+ class TestHandleSlackErrorRateLimit :
232+ """_handle_slack_error should raise AdapterRateLimitError on ratelimited."""
233+
234+ def test_rate_limit_from_dict_response (self ):
235+ """Rate limit detection with a plain dict response."""
236+ from chat_sdk .shared .errors import AdapterRateLimitError
237+
238+ adapter = _make_adapter (bot_token = "xoxb-tok" )
239+ with pytest .raises (AdapterRateLimitError ):
240+ adapter ._handle_slack_error (_make_slack_api_error ("ratelimited" ))
241+
242+ def test_rate_limit_from_slack_response (self ):
243+ """Rate limit detection with a SlackResponse-like object."""
244+ from chat_sdk .shared .errors import AdapterRateLimitError
245+
246+ adapter = _make_adapter (bot_token = "xoxb-tok" )
247+ with pytest .raises (AdapterRateLimitError ) as exc_info :
248+ adapter ._handle_slack_error (
249+ _make_slack_api_error ("ratelimited" , use_slack_response = True , retry_after = "30" )
250+ )
251+ assert exc_info .value .retry_after == 30
252+
253+ def test_rate_limit_without_retry_after (self ):
254+ """Rate limit with no Retry-After header should still raise."""
255+ from chat_sdk .shared .errors import AdapterRateLimitError
256+
257+ adapter = _make_adapter (bot_token = "xoxb-tok" )
258+ with pytest .raises (AdapterRateLimitError ) as exc_info :
259+ adapter ._handle_slack_error (
260+ _make_slack_api_error ("ratelimited" , use_slack_response = True )
261+ )
262+ assert exc_info .value .retry_after is None
263+
264+
265+ # ---------------------------------------------------------------------------
266+ # Configurable client_cache_max
267+ # ---------------------------------------------------------------------------
268+
269+
270+ class TestConfigurableCacheMax :
271+ """client_cache_max should be configurable via SlackAdapterConfig."""
272+
273+ def test_default_cache_max (self ):
274+ """Default cache max should be 100."""
275+ adapter = _make_adapter ()
276+ assert adapter ._client_cache_max == 100
277+
278+ def test_custom_cache_max (self ):
279+ """Custom cache max should override the default."""
280+ adapter = _make_adapter (client_cache_max = 50 )
281+ assert adapter ._client_cache_max == 50
282+
283+ # Fill to capacity + 1
284+ for i in range (51 ):
285+ adapter ._get_client (f"tok-{ i } " )
286+
287+ assert len (adapter ._client_cache ) == 50
288+ assert "tok-0" not in adapter ._client_cache
0 commit comments