Skip to content

fix: adapter hygiene — Slack stream await, Teams divider, public worker APIs#48

Merged
patrick-chinchill merged 5 commits into
mainfrom
claude/review-open-issues-wDyB0
Apr 23, 2026
Merged

fix: adapter hygiene — Slack stream await, Teams divider, public worker APIs#48
patrick-chinchill merged 5 commits into
mainfrom
claude/review-open-issues-wDyB0

Conversation

@patrick-chinchill
Copy link
Copy Markdown
Collaborator

@patrick-chinchill patrick-chinchill commented Apr 23, 2026

Bundles the four open chat-sdk-python issues into one PR. All four are small, adapter-surface hygiene items.

Summary

  • SlackAdapter.stream() calls AsyncWebClient.chat_stream() without await #44 — SlackAdapter.stream() missing await: AsyncWebClient.chat_stream(...) is a coroutine function. Without the await, streamer was a coroutine and .append() raised AttributeError on the first chunk, so native Slack streaming failed for any consumer using the default adapter. One-line fix; existing tests updated to use AsyncMock for chat_stream so they actually mirror the real client.
  • Teams card_to_adaptive_card divider renders zero-height (empty Container with separator: True) #45 — Teams divider renders zero-height: Microsoft Teams renders an empty Container with separator: True at zero height, so the separator line is effectively invisible. New behavior post-processes the body: a divider between siblings hoists separator: True onto the following element; a trailing divider emits a minimal non-empty Container. Internal marker never leaks into the outgoing payload. Upstream TS ships the same bug; documented as a divergence in UPSTREAM_SYNC.md.
  • Public API for reconstructing a Thread in a worker process from persisted state #46Chat.thread(thread_id, *, current_message=None): new public worker-reconstruction factory mirroring TS chat.thread(threadId). Adapter is inferred from the thread-ID prefix; state and message history come from the Chat instance. current_message is preserved so Slack native streaming still works post-reconstruction (it reads recipient_user_id / recipient_team_id from the current message).
  • Expose public per-request token and client accessors on SlackAdapter #47SlackAdapter.current_token / current_client: public @property accessors for the request-context-bound bot token and a preconfigured AsyncWebClient. Replaces underscore access (_get_token / _get_client) from consumer code that makes direct Slack Web API calls from inside a handler (email resolution, user profile fetches, etc.). Documented as a Python-only extension in UPSTREAM_SYNC.md.

Test plan

  • uv run ruff check src/ tests/ scripts/ — all checks passed
  • uv run ruff format --check src/ tests/ scripts/ — 191 files already formatted
  • uv run python scripts/audit_test_quality.py — 0 hard failures
  • uv run python scripts/verify_test_fidelity.py — 0 missing
  • uv run pyrefly check --baseline=.pyrefly-baseline.json — 0 errors
  • uv run pytest tests/ --tb=short -q3371 passed, 11 skipped, 1 pre-existing failure on main (test_github_webhook.py::test_throws_when_no_auth, unrelated to this change)
  • New regression test for SlackAdapter.stream() calls AsyncWebClient.chat_stream() without await #44 (test_stream_awaits_chat_stream_coroutine) passes with AsyncMock(return_value=mock_streamer) — would fail without the await
  • New TestPublicContextAccessors class covers single-workspace default, ContextVar binding, mock-client pass-through, and AuthenticationError outside context
  • New divider tests cover the between-siblings, leading, trailing, and consecutive-dividers cases, plus a marker-leak assertion
  • New TestChatThreadFactory class covers adapter inference, current_message propagation, stub fallback, invalid-ID, and unregistered-adapter paths

Closes #44, #45, #46, #47.

https://claude.ai/code/session_01MXobPieFH93txkgm38oMpC


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Request-scoped accessors expose the current Slack bot token and configured client.
    • New factory to reconstruct/resume chat threads from a thread ID, with optional current-message seeding.
  • Bug Fixes

    • Slack streaming now properly awaits streamer initialization before appending.
    • Teams card dividers now render as visible separators, including trailing dividers.
  • Documentation

    • Changelog and compatibility docs updated.
  • Tests

    • Added tests covering thread factory, Slack streaming, token/client accessors, and Teams divider behavior.

…er APIs

Bundles four open adapter issues into one PR.

- #44 SlackAdapter.stream(): await chat_stream(...). Without the await
  the code called .append on an unawaited coroutine and native Slack
  streaming failed on the first chunk. Existing tests were updated to
  use AsyncMock for chat_stream so they mirror the real AsyncWebClient.
- #45 Teams divider now renders a visible separator. Dividers between
  siblings hoist separator: True onto the next element; a trailing
  divider emits a minimal non-empty Container. An empty Container with
  separator: True renders at zero height in Microsoft Teams.
  Documented as a divergence from upstream (same bug in TS).
- #46 New Chat.thread(thread_id, *, current_message=None) public
  factory. Mirrors TS chat.thread(threadId). Lets worker processes
  rebuild a Thread without reaching into ThreadImpl/_ThreadImplConfig
  internals. current_message is preserved for Slack native streaming
  (it populates recipient_user_id / recipient_team_id).
- #47 New SlackAdapter.current_token / current_client properties.
  Public accessors for the request-context-bound token and a
  preconfigured AsyncWebClient. Replaces underscore-prefixed
  _get_token / _get_client access from consumer code that makes direct
  Slack Web API calls from inside a handler.

Tests and CHANGELOG updated. UPSTREAM_SYNC.md divergence table updated
for #45 and #47.

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

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@patrick-chinchill has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 55 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 55 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fae6289-f317-4376-ad5b-8807376e0b75

📥 Commits

Reviewing files that changed from the base of the PR and between cd3a45d and 3297b3b.

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

Walkthrough

Fixes Slack streaming by awaiting chat_stream, changes Teams divider emission to hoist separators to visible siblings, exposes SlackAdapter.current_token and current_client, and adds Chat.thread(thread_id, *, current_message=None) to reconstruct worker threads.

Changes

Cohort / File(s) Summary
Docs
CHANGELOG.md, docs/UPSTREAM_SYNC.md
Add Unreleased entries documenting Slack streaming await fix, Teams divider hoisting, new SlackAdapter public accessors, and Chat.thread() factory.
Slack adapter
src/chat_sdk/adapters/slack/adapter.py
Add public properties current_token and current_client (request-scoped accessors) and await client.chat_stream(...) before using returned streamer.
Teams cards
src/chat_sdk/adapters/teams/cards.py
Emit divider marker and introduce _hoist_dividers pass to apply separator: True to the next visible sibling or produce a non-empty trailing container.
Chat thread factory
src/chat_sdk/chat.py
Add Chat.thread(thread_id, *, current_message=None) to parse adapter prefix, validate, and reconstruct a ThreadImpl, supplying a placeholder Message when needed.
Tests — Slack
tests/test_slack_api.py
Switch mocks to AsyncMock, add regression test asserting chat_stream is awaited once, and add tests for current_token / current_client.
Tests — Chat
tests/test_chat_resolver.py
Add TestChatThreadFactory to validate adapter inference, thread id preservation, current_message handling, shared adapter/state expectations, and error cases for malformed/unknown thread IDs.
Tests — Teams cards
tests/test_teams_cards.py
Expand divider tests for leading/middle/trailing cases and assert internal marker keys are not present in final Adaptive Card JSON.

Sequence Diagram(s)

sequenceDiagram
    participant Adapter as SlackAdapter
    participant Client as AsyncWebClient
    participant Streamer as ChatStreamer

    Adapter->>Client: call chat_stream(stream_kwargs)
    Note right of Client: previously returned coroutine (not awaited)
    Adapter-->>Client: await chat_stream(stream_kwargs)
    Client->>Streamer: returns initialized streamer
    Adapter->>Streamer: await Streamer.append(markdown_text=delta, token=token)
    Streamer-->>Adapter: append completes
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #47 — Adds SlackAdapter.current_token and current_client public properties; this PR implements those Python-only accessors.
  • #45 — Modifies Teams divider handling with a hoisting pass to avoid invisible empty Containers; this PR implements that change.

Poem

🐰 Streams now await and tokens gently show,

Dividers hop to siblings in tidy row.
Threads rebuilt from IDs, messages in tow,
A rabbit's patch — quick, clean, and apropos! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.97% 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 accurately summarizes the four main changes: Slack stream await fix, Teams divider rendering correction, and two new public worker APIs (current_token/current_client and Chat.thread factory).
Linked Issues check ✅ Passed The pull request addresses the requirement from issue #44 by adding await to client.chat_stream() and updating tests to use AsyncMock, ensuring the streamer is fully initialized before use.
Out of Scope Changes check ✅ Passed All changes are within scope: Slack stream await fix (#44), Teams divider hoisting (#45), Chat.thread factory (#46), and public context accessors (#47) are all documented objectives with corresponding test coverage.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/review-open-issues-wDyB0

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

Comment thread tests/test_slack_api.py Fixed
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 fixes a Slack streaming crash by awaiting the chat_stream coroutine and resolves a Teams divider rendering bug by hoisting the separator property to adjacent elements. It also adds public accessors for Slack credentials and a Chat.thread factory for worker reconstruction. Reviewer feedback recommends improving type safety for the Slack client property and replacing non-deterministic datetime.now() calls with fixed timestamps in both the implementation and tests.

Comment thread src/chat_sdk/adapters/slack/adapter.py
Comment thread src/chat_sdk/chat.py Outdated
Comment thread tests/test_chat_resolver.py Outdated
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 23, 2026 09:38
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: 2

🧹 Nitpick comments (1)
tests/test_slack_api.py (1)

1011-1038: Add the missing no-auth test for current_client.

The new public accessor contract says current_client should raise AuthenticationError when no token is available, but only current_token exercises that path right now. That leaves half of the new public API able to regress unnoticed.

💡 Suggested test
     def test_current_token_raises_without_any_token(self):
         from chat_sdk.shared.errors import AuthenticationError

         adapter = _make_adapter(bot_token=None)
         with pytest.raises(AuthenticationError):
             _ = adapter.current_token
+
+    def test_current_client_raises_without_any_token(self):
+        from chat_sdk.shared.errors import AuthenticationError
+
+        adapter = _make_adapter(bot_token=None)
+        with pytest.raises(AuthenticationError):
+            _ = adapter.current_client

As per coding guidelines, tests/**/*.py: "Every test must fail when the code is wrong."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_slack_api.py` around lines 1011 - 1038, Add a test that mirrors
test_current_token_raises_without_any_token but for the public accessor
current_client: create an adapter with _make_adapter(bot_token=None) and assert
that accessing adapter.current_client raises AuthenticationError (imported from
chat_sdk.shared.errors); this ensures current_client enforces the no-auth
behavior like current_token and prevents regressions in the public API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/chat_sdk/chat.py`:
- Around line 1407-1427: The current Chat.thread() only checks the prefix of
thread_id (adapter_name) but allows empty adapter-specific portions like
"slack:" or "slack::", which later break adapter calls; update Chat.thread() to
validate that the portion after the first colon (the channel/thread identifier)
is non-empty (similar to channel()), and if it's empty raise ChatError (use the
same style as the existing ChatError messages) before looking up self._adapters
or constructing the stub_message; keep the existing adapter lookup
(self._adapters.get(adapter_name)) and return path to _create_thread unchanged
once validation passes.

In `@tests/test_chat_resolver.py`:
- Around line 431-493: Add assertions that the Thread returned by
Chat.thread(...) reuses the parent Chat's state/history; e.g., in
TestChatThreadFactory (methods like test_infers_adapter_from_thread_id_prefix
and test_propagates_explicit_current_message) assert that the returned thread
references the original chat (thread.chat is chat or thread._chat is chat) and
that the thread's history/state object is the same instance as the chat's
(thread.history is chat.history or thread._history is chat._history). Update
whichever tests create thread via Chat.thread(...) to include these identity
assertions so the factory cannot return a detached thread.

---

Nitpick comments:
In `@tests/test_slack_api.py`:
- Around line 1011-1038: Add a test that mirrors
test_current_token_raises_without_any_token but for the public accessor
current_client: create an adapter with _make_adapter(bot_token=None) and assert
that accessing adapter.current_client raises AuthenticationError (imported from
chat_sdk.shared.errors); this ensures current_client enforces the no-auth
behavior like current_token and prevents regressions in the public API.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f7efcc59-79a8-457f-a6fc-3c21cc53fb41

📥 Commits

Reviewing files that changed from the base of the PR and between b1a9562 and 0a12e57.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/UPSTREAM_SYNC.md
  • src/chat_sdk/adapters/slack/adapter.py
  • src/chat_sdk/adapters/teams/cards.py
  • src/chat_sdk/chat.py
  • tests/test_chat_resolver.py
  • tests/test_slack_api.py
  • tests/test_teams_cards.py

Comment thread src/chat_sdk/chat.py Outdated
Comment thread tests/test_chat_resolver.py
- **`Chat.thread()`**: validate adapter-specific remainder of thread ID,
  not just the prefix. Previously `slack:` or `slack::` would construct a
  ThreadImpl with an empty channel ID that blew up later on the first
  adapter call — raise ChatError at construction time instead. Also
  defer to `adapter.channel_id_from_thread_id()` to catch invalid
  platform-specific shapes (CodeRabbit minor).
- **Stub message date_sent**: use `datetime.fromtimestamp(0, tz=UTC)`
  instead of `datetime.now()` — makes the method deterministic and
  keeps tests repeatable (Gemini medium).
- **Test for empty remainder**: new `test_empty_remainder_raises`
  covering `"slack:"` and `"slack::"`.
- **Test for shared state/history**: new
  `test_reuses_parent_chat_state_and_history` asserting the factory
  binds the new Thread to the parent Chat's `_state_adapter` and
  `_message_history`. This surfaced a real finding — `_create_thread`
  skips message_history when `adapter.persist_message_history` is
  falsy. Added `test_omits_history_when_adapter_does_not_persist` to
  lock in that intentional behavior (CodeRabbit minor, actually caught
  a real contract gap in tests).
- **Test datetime**: fixed timestamp `datetime(2024, 1, 1, ...)` in
  `test_propagates_explicit_current_message` (Gemini medium).
- **Remove unused `within_ctx` local function** from
  `test_current_token_honors_per_request_context` (CodeQL).
- **`AsyncWebClient` forward ref**: `current_client` now returns
  `"AsyncWebClient"` with a TYPE_CHECKING import, replacing `Any`
  (Gemini medium). Ruff auto-stripped the quotes since
  `from __future__ import annotations` is in effect.

Validation:
- `uv run ruff check src/ tests/ scripts/` — clean
- `uv run ruff format --check src/ tests/ scripts/` — clean
- `uv run pytest tests/ --tb=short -q` — 3485 passed, 2 skipped
…line

PR #48 is based off main, which doesn't yet have PR #49's
replace-imports-with-any submodule wildcards (slack_sdk.*). The
TYPE_CHECKING import of AsyncWebClient from slack_sdk.web.async_client
therefore tripped missing-import on this branch.

Reverted to 'current_client -> Any' with a docstring capturing the
actual runtime type. Once PR #49 merges, we can promote to a typed
forward ref in a follow-up (or in the next release).
- **Structural thread-ID validation**: reject `slack::`, `slack:::`,
  `slack::::` etc. via `any(seg for seg in remainder.split(":"))`
  BEFORE calling the adapter. Previously relied on
  `adapter.channel_id_from_thread_id()` returning exactly
  `"{adapter}:"` to catch the empty-remainder case, but different
  adapters return different things for malformed input (MockAdapter
  returns `"slack:"`, real Slack regex-matches and returns the full
  string unchanged, etc.). The structural check doesn't depend on
  adapter-specific behavior, so it's a single source of truth.
- **Expand `test_empty_remainder_raises`** to cover `slack:::` and
  `slack::::` variants.
- **Clean up `test_omits_history_when_adapter_does_not_persist`**:
  explicitly set `adapter.persist_message_history = None` instead of
  asserting on the mock's default. Prevents silent test regression
  if the mock default ever changes, and makes intent unambiguous.
Review caught: 'any segment non-empty' accepted empty-channel IDs like
`slack::thread` because the thread segment was non-empty. The intent
is specifically to require a non-empty **channel** segment.

Fix: after splitting off the adapter prefix, partition the remainder
on ':' and check the first piece. Added 2 regression cases:
  - `slack::thread` (empty channel, non-empty thread)
  - `slack::foo:bar` (empty channel, multi-segment rest)
Copy link
Copy Markdown
Collaborator Author

@patrick-chinchill patrick-chinchill left a comment

Choose a reason for hiding this comment

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

Re-reviewed current head 3297b3b. This looks good to merge. Focused verification passed: uv run pytest tests/test_chat_resolver.py tests/test_slack_api.py tests/test_teams_cards.py -q (139 passed). I couldn't submit an approval because GitHub does not allow approving your own pull request.

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.

SlackAdapter.stream() calls AsyncWebClient.chat_stream() without await

2 participants