Skip to content

feat(slack): expose web_client property on SlackAdapter (#98)#127

Open
patrick-chinchill wants to merge 5 commits into
mainfrom
claude/port-slack-web-client
Open

feat(slack): expose web_client property on SlackAdapter (#98)#127
patrick-chinchill wants to merge 5 commits into
mainfrom
claude/port-slack-web-client

Conversation

@patrick-chinchill
Copy link
Copy Markdown
Collaborator

@patrick-chinchill patrick-chinchill commented May 30, 2026

What's ported

Worklist item P1-3 (tracking issue #98): expose direct WebClient access on the Slack adapter.

  • SlackAdapter.web_client — a synchronous slack_sdk.WebClient bound to the bot token for the current request context (multi-workspace) or the configured default token (single-workspace). For any Slack Web API call not covered by the adapter's high-level methods (e.g. adapter.web_client.pins_add(...)).
  • SlackAdapter._get_web_client_for_token(token) — direct port of upstream's getClientForToken: one cached WebClient per distinct token. Kept separate from the existing async _client_cache (which holds AsyncWebClient instances used by the adapter's own API calls) because the sync and async client types are not interchangeable. slack_sdk import stays deferred (optional dependency).
  • SlackAdapter.client — deprecated one-release alias delegating to web_client; emits DeprecationWarning.

Token resolution (3-level resolver, matches upstream + existing pattern)

web_client resolves via the existing sync _get_token():

  1. ContextVar request-context token (set during webhook handling, or by with_bot_token / with_bot_token_async)
  2. Static default bot_token config
  3. else raise AuthenticationError

Uses the existing token ContextVar (_request_context / _resolved_default_token) from the dynamic-bot-token work — no or fallbacks; resolution is is not None-based throughout the resolver it delegates to.

Upstream refs

  • 8366b8b (PR #471) — feat(slack): expose direct WebClient access via adapter.client
  • 2279f1d (PR #472) — revert
  • fdebde7 (PR #476) — reapply
  • 2f108bd (PR #478) — rename adapter.clientadapter.webClient (Python: web_client)

Net effect: the Slack adapter exposes a WebClient, with client retained as a deprecated alias.

Tests

tests/test_slack_web_client.py mirrors upstream's webClient getter describe block:

  • single-tenant: web_client returns a client bound to the configured token; same instance cached per token; cache keyed by resolved token.
  • deprecated client alias returns the same object as web_client, emits exactly one DeprecationWarning; web_client itself emits none.
  • multi-tenant: ContextVar token resolves correctly inside with_bot_token, and overrides the static default within that scope.
  • no-context (multi-workspace) → raises AuthenticationError on both web_client and client.
  • callable/unresolved-async bot_token → raises AuthenticationError from the sync property.

Follow-ups (out of scope for this PR)

The 2f108bd rename also added adapter.octokit (GitHub) and adapter.linearClient (Linear) alongside webClient. In chat-sdk-python the GitHub and Linear adapters do not yet have a .client property to rename, so those equivalents are deferred to separate PRs (not part of #98 / P1-3).

Validation

All run from the worktree against origin/main (3ba6456):

  • uv run ruff check src/ tests/All checks passed
  • uv run ruff format --check src/ tests/196 files already formatted
  • uv run python scripts/audit_test_quality.py0 hard failures (pre-existing cross-file duplicate warnings only; none reference the new test file)
  • uv run pytest tests/ -q4057 passed, 3 skipped (baseline + 10 new tests; no regressions)
  • slack subset uv run pytest tests/ -q -k slack631 passed

Release coordination

Intentionally no CHANGELOG / pyproject.toml version changes — the coordinating 0.4.29 release-cut PR will add the CHANGELOG/version entry to avoid N-way CHANGELOG collisions across the worklist batch.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced web_client property providing synchronous Slack WebClient access.
    • Enhanced token resolution with support for both synchronous and asynchronous resolver functions.
  • Deprecations

    • client property deprecated; use web_client instead.
  • Tests

    • Expanded test coverage for WebClient functionality and token resolution behaviors.

Review Change Stack

Port the Slack adapter's direct WebClient access from upstream
vercel/chat (commits 8366b8b / fdebde7 / 2f108bd, PRs #471/#476/#478).

- Add ``SlackAdapter.web_client``: a synchronous ``slack_sdk.WebClient``
  bound to the current request-context token (multi-workspace) or the
  configured default token (single-workspace). Token resolution uses the
  existing 3-level resolver via ``_get_token()``: ContextVar token >
  static ``bot_token`` config > ``AuthenticationError`` (no ``or``
  fallbacks).
- Add ``_get_web_client_for_token`` mirroring upstream's
  ``getClientForToken`` — one cached ``WebClient`` per distinct token.
  Kept separate from the async ``_client_cache`` (``AsyncWebClient``).
  ``slack_sdk`` import stays deferred (optional dependency, hazard #10).
- Add deprecated ``client`` property alias delegating to ``web_client``
  (one-release deprecation; emits ``DeprecationWarning``).

Tests (tests/test_slack_web_client.py) mirror upstream's "webClient
getter" block: single-tenant binding + per-token caching identity,
multi-tenant ContextVar resolution under ``with_bot_token``, no-context
and unresolved-async-resolver -> ``AuthenticationError``, and the
deprecated alias returning the same object plus emitting the warning.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Warning

Review limit reached

@patrick-chinchill, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 45 minutes and 25 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b535428-690b-448b-b689-898d5fea9be4

📥 Commits

Reviewing files that changed from the base of the PR and between 41c8f31 and e8a7dae.

📒 Files selected for processing (3)
  • src/chat_sdk/adapters/slack/adapter.py
  • tests/test_slack_dynamic_token_and_verifier.py
  • tests/test_slack_web_client.py
📝 Walkthrough

Walkthrough

This PR adds synchronous WebClient access to SlackAdapter with resolver-aware token resolution. A new web_client property returns a cached, token-bound Slack client; a deprecated client alias maintains backward compatibility. The sync token resolution path now handles both sync and async resolvers, directly invoking sync callables and raising errors for unresolved async resolvers.

Changes

Slack Adapter Synchronous WebClient Feature

Layer / File(s) Summary
Infrastructure, caching, and synchronous WebClient properties
src/chat_sdk/adapters/slack/adapter.py
Adds warnings import, initializes _web_client_cache for per-token WebClient instances, exposes web_client property resolved from request context or default token, adds deprecated client alias with DeprecationWarning, and implements _get_web_client_for_token and _invalidate_client helpers.
Sync token resolution for resolver-backed bot_token
src/chat_sdk/adapters/slack/adapter.py
Updates _get_token to invoke sync callables directly, validate non-empty string results, detect and close awaitables returned by sync resolvers (raising AuthenticationError), and raise AuthenticationError when async resolvers are configured but not yet invoked.
Updated token resolver tests covering sync/async paths
tests/test_slack_dynamic_token_and_verifier.py
Replaces broad "before resolver runs" test with two focused scenarios: sync resolver is invoked and cached by current_token; async resolver causes sync access to fail. Updates rotation-safety regression test to use async resolver stimulus and validate async-resolution path primes sync cache.
Comprehensive test module for web_client property and resolver scenarios
tests/test_slack_web_client.py
New test module covering single-workspace caching and invalidation, deprecated client alias warning behavior, multi-workspace token precedence, unresolved async resolver errors, sync resolver invocation and caching, async resolver in sync path, sync callable returning coroutine (with cleanup), and empty resolver result validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Chinchill-AI/chat-sdk-python#87: Both PRs modify the Slack adapter's dynamic bot_token resolver flow—specifically the sync current_token/_get_token behavior, resolver priming expectations, and request/token-cache interactions—so the main PR's updates are directly related to the resolver implementation introduced in the retrieved PR.

Poem

A rabbit hops through sync and async ways,
Caching tokens through all its days.
With web_client new and client old and gray,
The Slack adapter works in a modern way! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: exposing a new web_client property on SlackAdapter, which is the central feature of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a synchronous web_client property and a deprecated client alias to the SlackAdapter to provide direct access to a synchronous slack_sdk.WebClient instance, backed by a new cache and covered by comprehensive unit tests. The review feedback points out that the newly introduced _web_client_cache is not cleared when _invalidate_client is called, which could leave stale or revoked clients in the cache, and suggests updating the invalidation method to clear both caches.

"""Remove a cached client (e.g., on token revocation)."""
self._client_cache.pop(token, None)

def _get_web_client_for_token(self, token: str) -> Any:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The newly introduced self._web_client_cache is not cleared when _invalidate_client is called (e.g., during token revocation or authentication errors in _handle_slack_error). To prevent stale or revoked synchronous WebClient instances from remaining in the cache, please update _invalidate_client to also pop the token from self._web_client_cache:

def _invalidate_client(self, token: str) -> None:
    """Remove a cached client (e.g., on token revocation)."""
    self._client_cache.pop(token, None)
    self._web_client_cache.pop(token, None)
    self._client_cache.pop(token, None)
    self._web_client_cache.pop(token, None)

Comment thread tests/test_slack_web_client.py Fixed
…test (review)

Two review follow-ups on the web_client port:

- Gemini: `_invalidate_client` cleared only the async `_client_cache`, leaving
  stale/revoked synchronous `WebClient` instances in `_web_client_cache` on
  token revocation / auth-error eviction. Pop the token from both caches.
  New `test_invalidate_client_clears_web_client_cache` is load-bearing.

- github-code-quality: `assert adapter.web_client is adapter.web_client`
  tripped "comparison of identical values". The test is a genuine caching
  check (the property is invoked twice), but binding each access to a name
  makes that intent explicit and silences the false-positive.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Copy link
Copy Markdown
Collaborator Author

@codex review


Generated by Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7f5b53957

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

the *synchronous* ``WebClient`` (the analog of the single TS
``WebClient``), so its methods are not awaitables.
"""
return self._get_web_client_for_token(self._get_token())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invoke synchronous token resolvers for web_client

When SlackAdapterConfig.bot_token is a synchronous callable and web_client is accessed before a webhook or current_token_async() has primed _default_bot_token_cache, this path calls _get_token(), which raises AuthenticationError instead of invoking the configured resolver. That leaves single-workspace apps that use a sync resolver for rotation or lazy secret loading unable to use the new direct WebClient outside a request context, even though the resolver can be evaluated synchronously here.

Useful? React with 👍 / 👎.

…odex review)

The sync ``_get_token`` path only handled the static-string and primed
cache cases — a sync ``bot_token`` callable (used e.g. for secret
rotation or lazy load from a sync source) raised
``AuthenticationError`` from ``web_client`` outside any webhook /
ContextVar scope until an async path had primed the cache. Proactive
sends from single-workspace apps using a sync resolver therefore
failed.

Detect the sync-resolver case via ``inspect.iscoroutinefunction``,
invoke the callable, validate the result, and prime
``_default_bot_token_cache`` with the same semantics the async
``_resolve_default_token`` path uses. Async resolvers still raise from
the sync property (cannot be awaited). Defensive check for sync
callables that *return* a coroutine (rare but real:
``lambda: some_async_fn()``) — refuse to cache the coroutine.

The two existing tests that asserted the previous deficient behavior
(sync resolver raising before resolution) are updated to assert the
new correct behavior; the cache-refresh regression test switches to an
async resolver so its sanity precondition still holds.
Copy link
Copy Markdown
Collaborator Author

@GHCodex review


Generated by Claude Code

…error (audit)

The defensive `inspect.isawaitable(resolved)` branch in `_get_token`'s
sync-callable handler raised AuthenticationError but never closed the
coroutine the resolver returned. Callers saw a noisy
`RuntimeWarning: coroutine was never awaited` on every triggering call.

Close the awaitable via its `close()` method (Coroutine protocol) before
raising. The existing regression test
`test_sync_callable_returning_coroutine_raises` is strengthened to capture
warnings and assert none of "never awaited" kind leaked — confirmed
load-bearing under `pytest -W error::RuntimeWarning`.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Copy link
Copy Markdown
Collaborator Author

@GHCodex review


Generated by Claude Code

@patrick-chinchill patrick-chinchill marked this pull request as ready for review May 30, 2026 16:59
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41c8f3168a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/adapters/slack/adapter.py Outdated
"slack",
"Bot token resolver returned an empty or non-string value.",
)
self._default_bot_token_cache = resolved
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid pinning sync resolver tokens after first access

When bot_token is a synchronous resolver used for rotation, this assignment makes the first resolved value permanent for all later sync reads because _get_token() checks _default_bot_token_cache before calling the provider. The config contract in types.py says callable bot tokens are invoked on each use to support rotation, but after adapter.web_client or current_token is accessed once, subsequent accesses keep returning a WebClient for the stale token even if the resolver now returns a rotated token.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/chat_sdk/adapters/slack/adapter.py`:
- Around line 844-847: _invalidate_client currently only removes entries from
_client_cache and _web_client_cache, but leaves the stale token in
_default_bot_token_cache and _resolved_default_token so subsequent calls to
_get_token will return the revoked token; update _invalidate_client to also pop
the token from _default_bot_token_cache and clear/reset _resolved_default_token
(and any resolver-cached state) for the given token so that _get_token will be
forced to re-resolve and not recreate clients with a revoked token; reference
the _invalidate_client method and the _default_bot_token_cache and
_resolved_default_token fields to locate where to add the additional cache
invalidation and reset logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b15ce462-bb98-4d5a-b9ea-384b45dc8720

📥 Commits

Reviewing files that changed from the base of the PR and between 3ba6456 and 41c8f31.

📒 Files selected for processing (3)
  • src/chat_sdk/adapters/slack/adapter.py
  • tests/test_slack_dynamic_token_and_verifier.py
  • tests/test_slack_web_client.py

Comment thread src/chat_sdk/adapters/slack/adapter.py
…validation

Addresses two P2 review findings on PR #127:

**Codex P2 — sync resolver rotation broken**
The previous sync-callable branch in ``_get_token`` cached the first
resolved value in ``_default_bot_token_cache`` and the cache-first
early-return prevented re-invocation, freezing rotating resolvers
(e.g., secret-manager-backed). The contract on
``SlackAdapterConfig.bot_token`` says callable resolvers are "called
on each use to support rotation."

Track ``_is_dynamic_bot_token`` at construction time. In ``_get_token``,
sync dynamic resolvers now invoke fresh on every call and never write
the process-wide cache. Static-string configs keep their cache fast
path (nothing to rotate). Async resolvers still require a webhook /
``current_token_async`` entry to be awaited.

The previously-added test
``test_sync_current_token_with_sync_resolver_invokes_resolver``
asserted the cache was primed — flipped to assert the inverse, with a
cross-reference to the dedicated rotation pin in
``test_sync_callable_invoked_fresh_each_access``.

**CodeRabbit P2 — _invalidate_client retained revoked tokens**
``_invalidate_client(token)`` evicted the WebClient and AsyncWebClient
caches but left ``_default_bot_token_cache`` / ``_resolved_default_token``
holding the revoked value, so the next ``_get_token`` returned the
same token and the adapter just rebuilt clients around it.

Now clears the resolved-token caches for dynamic-resolver configs so
the next access re-invokes the resolver. Guarded on
``_is_dynamic_bot_token`` so static-string configs retain their cache
(no refresh path — clearing would only make subsequent sync access
raise with no way to recover).

Tests: rewrote the caching test to assert rotation (resolver invoked
fresh on every access, cache stays None); added invalidation tests
covering dynamic-resolver clearing, ContextVar clearing, static-string
no-op, and token-mismatch no-op. Full suite green (4067 passed) under
``-W error::RuntimeWarning``.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants