Skip to content

feat(slack): Socket Mode transport (vercel/chat#162)#86

Merged
patrick-chinchill merged 10 commits into
mainfrom
claude/port-slack-socket-mode-J7S7H
May 28, 2026
Merged

feat(slack): Socket Mode transport (vercel/chat#162)#86
patrick-chinchill merged 10 commits into
mainfrom
claude/port-slack-socket-mode-J7S7H

Conversation

@patrick-chinchill
Copy link
Copy Markdown
Collaborator

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

Summary

Port of upstream vercel/chat#162 — adds an opt-in Slack Socket Mode transport so bots can consume Slack events over a persistent WebSocket instead of (or alongside) signed HTTP webhooks.

The webhook code path is unchanged. Socket Mode is enabled per-adapter via SlackAdapterConfig(mode="socket", app_token="xapp-...").

Design notes

Lifecycle (hazard #5: async task lifecycle)

  • start_socket_mode() spawns a tracked asyncio.Task that runs the connect / run / reconnect loop. Returns once the first connect succeeds (or raises if the first connect fails — bad app token, offline, etc.). Idempotent.
  • stop_socket_mode() flips a shutdown flag, disconnects the live SocketModeClient, cancels the loop task, and awaits it. Idempotent.
  • initialize() calls start_socket_mode() when mode == "socket". disconnect() always calls stop_socket_mode() (no-op in webhook mode).

Reconnect strategy

We layer an outer reconnect loop on top of slack_sdk.socket_mode.aiohttp.SocketModeClient (which already does its own auto_reconnect_enabled=True). Exponential backoff 1s → 30s with the loop polling for shutdown every 250ms so stop_socket_mode() never has to wait the full backoff window. Rationale: the inner SDK retries inside an established session, but doesn't retry if connect() itself raises (network down at startup, transient DNS failure). The outer loop is belt-and-suspenders, not a behavior change. Documented in docs/UPSTREAM_SYNC.md.

Acknowledgment protocol

Each socket request hits _on_socket_request which constructs an ack(payload=None) callback that wraps client.send_socket_mode_response(SocketModeResponse(...)). Slack retries (retry_attempt > 0) are still acked but not dispatched, so we don't double-process. Interactive view_submission errors round-trip through the ack payload.

Forwarded events (serverless variant)

handle_webhook now accepts x-slack-socket-token-bearing JSON POSTs at any time (constant-time compared via hmac.compare_digest). This lets a separate long-lived process run the WebSocket and POST events to a stateless webhook handler — the upstream startSocketModeListener pattern. The full Vercel cron glue isn't ported (Vercel-specific), but the receiving end is.

Hazard coverage

Other in-scope changes

  • ModalResponse(action="clear") — closes the entire modal view stack; emits response_action: clear. Matches upstream's ModalClearResponse type.
  • New optional extra slack-socket = ["slack-sdk>=3.27.0", "aiohttp>=3.9"].
  • docs/UPSTREAM_SYNC.md: documents the outer reconnect loop and the not-ported serverless startSocketModeListener glue.

Divergences from upstream

Area Python TS Rationale
Outer reconnect loop on top of SocketModeClient Yes (1s → 30s exp backoff with shutdown signaling) No (relies on @slack/socket-mode's internal reconnect only) connect() can raise before the inner loop owns the lifecycle; we don't want adapter setup to give up on first DNS blip. Inner auto-reconnect still runs.
startSocketModeListener / runSocketModeListener (serverless cron variant) Not ported (forwarding receiver IS ported) Ported Vercel-specific deployment glue with waitUntil semantics. The receiving side (x-slack-socket-token handling in handle_webhook) is ported so a separate Python process can run the listener.

Both rows added to the by-design non-parity table in docs/UPSTREAM_SYNC.md.

Test plan

  • uv run ruff check src/ tests/ scripts/
  • uv run ruff format --check src/ tests/ scripts/
  • uv run python scripts/audit_test_quality.py — 0 hard failures
  • uv run pyrefly check src/chat_sdk/adapters/slack/ — 0 errors
  • uv run pytest tests/ --tb=short -q3692 passed, 1 pre-existing failure (tests/test_github_webhook.py::TestGitHubAdapterConstructor::test_throws_when_no_auth, unrelated)
  • 24 new tests in tests/test_slack_socket_mode.py:
    • Config validation: app_token required, xapp- prefix enforced, signing optional in socket mode, env var pickup, multi-workspace rejected by factory, forwarding-secret fallback
    • Modal clear action emits response_action: clear
    • Forwarded events: webhook in socket mode returns 405 without token, accepts valid token, rejects invalid token, rejects when no secret configured
    • _route_socket_event: events_api dispatch, missing-event-field handling, slash command ack-then-dispatch, interactive ack with errors body, retry skipped but acked, unknown event type acked
    • Lifecycle: start/stop, start idempotency, stop idempotency, first-connect failure surfaces, reconnect after transient disconnect
    • ContextVar: per-event request context survives the dispatch into spawned tasks; outer context not polluted

Upstream PR: vercel/chat#162

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Socket Mode support for Slack adapter as an alternative to webhook-based integration
    • Added "clear" action support for modal responses
  • Bug Fixes

    • Enhanced webhook security with improved signature verification and event validation
  • Documentation

    • Updated Socket Mode documentation and known parity information
  • Tests

    • Added comprehensive test coverage for Socket Mode functionality

Review Change Stack

Add an opt-in Socket Mode transport to ``SlackAdapter`` so bots can
consume Slack events over a persistent WebSocket instead of webhooks.
Mirrors the upstream JS port at vercel/chat#162.

Highlights:

* New ``SlackAdapterConfig`` fields: ``mode`` (``"webhook"`` default or
  ``"socket"``), ``app_token`` (xapp-* with prefix validation), and
  ``socket_forwarding_secret`` for the forwarded-event HTTP path.
* ``signing_secret`` is now optional in socket mode (Slack does not
  sign socket events). ``_verify_signature`` refuses when no secret is
  configured so a stray webhook in socket mode can't HMAC against ``""``.
* ``initialize()`` opens the WebSocket when ``mode == "socket"``.
  ``disconnect()`` cancels a tracked reconnect loop and closes the
  client; both ``start_socket_mode()`` and ``stop_socket_mode()`` are
  idempotent.
* ``_route_socket_event`` dispatches ``events_api`` / ``slash_commands``
  / ``interactive`` payloads into the same handlers the webhook path
  uses (no fork). Skips Slack retries (``retry_attempt > 0``) but still
  acks. Interactive view-submission errors round-trip through the ack
  payload.
* ``handle_webhook`` now accepts forwarded socket events at any time
  via ``x-slack-socket-token`` (constant-time compared against the
  configured secret); refuses direct webhook POSTs in socket mode.
* New ``ModalResponse(action="clear")`` produces ``response_action:
  clear`` for closing the entire modal view stack.
* ``slack-socket`` extra in ``pyproject.toml`` (``slack-sdk`` +
  ``aiohttp``); the import inside ``start_socket_mode`` stays lazy.

Hazard coverage: explicit ``asyncio.Task`` tracking and shutdown
signaling for the WebSocket loop (#5); ``contextvars.copy_context()``
preserved through the socket dispatch path so per-event token
resolution still inherits into spawned handlers (#6); single-process
SocketModeClient lifecycle (#11); ``app_token`` format validated, never
logged (#12).

Tests: 24 new unit tests covering config validation, modal-clear
emission, forwarded-event auth (accept / reject / no-secret-configured /
invalid-token), the full ``_route_socket_event`` matrix (events_api /
slash / interactive / retry / unknown), lifecycle (start, stop, idempotency,
first-connect failure surfaced, transient-disconnect reconnect), and the
ContextVar boundary on multi-workspace token resolution. The new file
stubs ``slack_sdk.socket_mode.*`` in ``sys.modules`` to match the
existing ``test_slack_client_cache.py`` pattern.

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

coderabbitai Bot commented May 8, 2026

📝 Walkthrough

Walkthrough

This PR adds Socket Mode transport to the Slack adapter alongside the existing webhook mode. Socket Mode enables receiving Slack events via persistent WebSocket connections with an outer reconnect loop, exponential backoff, and context-aware token resolution for multi-workspace deployments. Events may arrive over the WebSocket or be forwarded via HTTP with cryptographic validation.

Changes

Slack Socket Mode Transport

Layer / File(s) Summary
Type contracts and dependency setup
src/chat_sdk/adapters/slack/types.py, src/chat_sdk/types.py, pyproject.toml
SlackAdapterMode type alias, extended SlackAdapterConfig with app_token, mode, socket_forwarding_secret, and connect_timeout_s fields; ModalResponse.action adds "clear" option; new slack-socket optional dependency on aiohttp>=3.9.
Adapter initialization and mode-aware configuration
src/chat_sdk/adapters/slack/adapter.py
SlackAdapter.__init__ validates mode-specific requirements (webhook requires signing_secret; socket requires xapp-… app_token), exposes mode and is_socket_mode properties, and create_slack_adapter rejects socket mode combined with OAuth multi-workspace credentials.
Webhook HTTP integration for forwarded Socket Mode events
src/chat_sdk/adapters/slack/adapter.py
handle_webhook gains forwarded event support via x-slack-socket-token header with secret validation, freshness-window enforcement, and timestamp auto-detection (ms vs seconds); direct POSTs rejected with 405 in socket mode; signature verification hardened to refuse empty signing_secret.
Interactive payload refactoring and modal response support
src/chat_sdk/adapters/slack/adapter.py
Introduces _dispatch_interactive_payload to centralize interactive routing reused by both webhook and Socket Mode paths; modal response mapping extends to support response_action="clear".
Socket Mode core runtime: lifecycle and event routing
src/chat_sdk/adapters/slack/adapter.py
start_socket_mode spawns tracked background connect/run/reconnect loop with exponential backoff and 30s handshake timeout; stop_socket_mode idempotently tears down client and task; _route_socket_event manages per-event Slack acks, multi-workspace token scoping via ContextVar, and error responses; _handle_forwarded_socket_event validates and dispatches HTTP-forwarded payloads; disconnect() cleanly stops Socket Mode.
Upstream sync documentation
docs/UPSTREAM_SYNC.md
Documents outer reconnect loop with exponential backoff around socket_sdk; clarifies serverless listener variant not ported while forwarding receiver is available.
Comprehensive Socket Mode test suite
tests/test_slack_socket_mode.py
989 new lines covering config validation, forwarded event token/freshness checks, _route_socket_event dispatch ordering and error handling, lifecycle idempotency, ContextVar request isolation across concurrent events, connection timeout, freshness window enforcement, interactive error ack payloads, and socket-vs-webhook payload parity.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Chinchill-AI/chat-sdk-python#68: The PR directly implements Slack Socket Mode transport with mode/app_token fields, start/stop lifecycle, event routing, and multi-workspace token context—addressing the feature scope of the issue.

Possibly related PRs

  • Chinchill-AI/chat-sdk-python#66: Both PRs modify the interactive payload dispatch path in SlackAdapter; the retrieved PR adds type=="block_suggestion" routing while this PR refactors the dispatcher into _dispatch_interactive_payload reused by both webhook and Socket Mode.

Poem

🐰 A socket appears, persistent and bright,
Events flow swift through WebSocket's might.
With contexts preserved and tokens held tight,
Modals stay open when things aren't quite right.
No webhook alone—now there's choice, oh what a delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.42% 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 'feat(slack): Socket Mode transport' directly and clearly describes the main feature addition—implementing Socket Mode transport for Slack—which is the primary changeset across all modified files.
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.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

Comment thread src/chat_sdk/adapters/slack/adapter.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 implements Slack Socket Mode support, enabling the adapter to handle events via a long-lived WebSocket. It includes a robust background reconnect loop with exponential backoff, logic for handling forwarded socket events in serverless setups, and updated configuration validation. Review feedback focuses on preventing a potential task leak during the connection phase and optimizing the backoff sleep mechanism by replacing the current polling approach with a more efficient asyncio.Event-based wait.

Comment thread src/chat_sdk/adapters/slack/adapter.py Outdated
Comment on lines +1372 to +1375
first_done, _ = await asyncio.wait(
{asyncio.create_task(connected.wait()), self._socket_task},
return_when=asyncio.FIRST_COMPLETED,
)
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 task created for connected.wait() remains running if the background loop task (self._socket_task) finishes first (e.g., due to a connection error or crash during the initial attempt). It is better to explicitly track and cancel this temporary wait task to avoid a potential task leak.

Suggested change
first_done, _ = await asyncio.wait(
{asyncio.create_task(connected.wait()), self._socket_task},
return_when=asyncio.FIRST_COMPLETED,
)
wait_task = asyncio.create_task(connected.wait())
first_done, pending = await asyncio.wait(
{wait_task, self._socket_task},
return_when=asyncio.FIRST_COMPLETED,
)
if wait_task in pending:
wait_task.cancel()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in b8a6e07 ("fix(slack): asyncio.Event for socket shutdown + cancel orphan wait_task"). The orphan wait_task is now explicitly cancelled when the loop task finishes first, and additionally drained on the new timeout-cleanup path added in e1c8047. Marking stale.


Generated by Claude Code

Comment thread src/chat_sdk/adapters/slack/adapter.py Outdated
Comment on lines +1494 to +1504
async def _socket_sleep_with_backoff(self, seconds: float) -> None:
"""Sleep for ``seconds`` but wake immediately on shutdown."""
# Poll every 0.25s so a stop_socket_mode call doesn't have to wait
# the full backoff window. asyncio.Event would be slightly cleaner
# but we don't want to add an Event per loop iteration.
deadline = asyncio.get_event_loop().time() + seconds
while not self._socket_shutdown:
remaining = deadline - asyncio.get_event_loop().time()
if remaining <= 0:
return
await asyncio.sleep(min(0.25, remaining))
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 _socket_sleep_with_backoff method uses a polling loop to check for shutdown every 0.25s. This is less efficient than using an asyncio.Event. By using a single asyncio.Event on the adapter instance (e.g., self._socket_shutdown_event), you can wait for either the timeout or the shutdown signal immediately using asyncio.wait_for(self._socket_shutdown_event.wait(), timeout=seconds). This avoids periodic wake-ups and is more idiomatic in async Python. Contrary to the inline comment, you only need one event instance per adapter, not one per loop iteration.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in b8a6e07. _socket_sleep_with_backoff now uses a single self._socket_shutdown_event: asyncio.Event on the adapter and await asyncio.wait_for(self._socket_shutdown_event.wait(), timeout=seconds) — no more 250ms polling. Marking stale.


Generated by Claude Code

Address gemini-code-assist review on PR #86:

1. Line 1375: ``connected.wait()`` was wrapped in an untracked task that
   stayed pending if the loop task finished first (e.g., first connect
   failed). Track it explicitly and cancel when it lands in ``pending``.

2. Line 1504: replace the ``not self._socket_shutdown`` polling loop with
   a per-adapter ``asyncio.Event`` waited via ``asyncio.wait_for``. One
   event per adapter (not per backoff iteration); wakeup latency is now
   bounded by the event-loop scheduler rather than the previous 250ms
   poll cadence. Updates ``start_socket_mode`` (clear), ``stop_socket_mode``
   (set), and the four loop checks.

24 socket-mode tests still pass.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
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.

Code review — Slack Socket Mode port

Reviewed against upstream vercel/chat#162 (7e90d9c) on f55378a, applying docs/UPSTREAM_SYNC.md hazards #5/#6/#11/#12 and docs/SELF_REVIEW.md. Reviewed feat(slack): Socket Mode transport + the asyncio.Event follow-up. Net assessment: solid port with thoughtful Python-specific hardening. Two real issues worth tightening; the rest are nits.

🟡 Medium

  • No connect timeout on SocketModeClient.connect() (hazard #11). start_socket_mode does await asyncio.wait({wait_task, self._socket_task}, return_when=FIRST_COMPLETED) with no timeout=. If the slack_sdk handshake hangs (DNS black-hole, TLS stall, mis-routed firewall), initialize() blocks forever and the caller has no observable signal. Upstream inherits the same problem from @slack/socket-mode, so this is parity, but docs/UPSTREAM_SYNC.md Hazard #11 explicitly flags "shared sessions plus explicit cleanup". Suggest wrapping the wait with asyncio.wait_for(..., timeout=connect_timeout_s) (configurable, default ~30s) and on timeout calling await self.stop_socket_mode() before raising. Add a regression test that injects a connect() that await asyncio.sleep(60)s.

  • Forwarded events have no freshness check, even though the envelope ships a timestamp (hazard #12). _handle_forwarded_socket_event accepts any payload whose x-slack-socket-token matches. The forwarded JSON includes timestamp: Date.now() from the listener but neither side validates it. A leaked forwarding secret + replayed body would re-execute handlers indefinitely (and dedupe only catches identical message IDs, not re-injected modal submits). Upstream has the same gap; worth either (a) adding a 5-minute window check mirroring _verify_signature, or (b) documenting it in the non-parity table so future readers don't assume the timestamp is load-bearing. Same goes for the missing per-request nonce.

🔵 Nit

  • stop_socket_mode suppresses Exception after await task (adapter.py:1413). with contextlib.suppress(asyncio.CancelledError, Exception): is broader than necessary — Exception covers everything that isn't a system-exiting BaseException, so a real bug surfaced post-cancellation is lost. The loop's except already logs; suggest narrowing to (asyncio.CancelledError,) and letting anything else log to stderr.

  • asyncio.get_event_loop().create_task(...) in _route_socket_event.wrap_async (adapter.py:1568). Deprecated since 3.10 when no loop is set. Inside a running coroutine get_running_loop() is correct and matches start_socket_mode (which already uses it).

  • _route_socket_event in interactive swallows dispatch errors then ack's empty (adapter.py:1672–1681). Upstream lets the error propagate to the slack_event listener; Python catches and ack's with no body. Net effect: a view_submission whose handler raises silently closes the modal instead of showing errors. Consider logging at error (already done) plus ack'ing with {"response_action": "errors", "errors": {"_": "internal error"}} so the user sees the modal didn't take.

  • Cross-contamination test gap. TestSocketContextVar covers single-event isolation, but there's no test that fires two concurrent _route_socket_event(events_api) calls for different teams via asyncio.gather and asserts each handler sees the right token. Given the contextvars.copy_context() reliance (hazard #6) this is the exact regression class the doc warns about.

  • Extra is_ext_shared_channel field added to the synthesized event_callback payload (adapter.py:1597) that upstream's routeSocketEvent doesn't include. Harmless additive, but if you want strict parity-on-the-wire, drop it (and the existing webhook path doesn't add it either, so this introduces a socket-vs-webhook asymmetry).

✅ Looks good

  • app_token validated for xapp- prefix at construction; never logged. (Hazard #12)
  • _verify_signature correctly refuses when _signing_secret is None so socket-mode adapters can't silently HMAC against "". (Hazard #12)
  • hmac.compare_digest used for x-slack-socket-token. (Hazard #12)
  • socket_forwarding_secret defaults to app_token matching upstream + lets ops override via env — the docstring even calls out the recommendation. (Hazard #12)
  • _socket_task tracked, cancelled, awaited on shutdown. The wait_task orphan is now explicitly cancelled per the follow-up. (Hazard #5)
  • _socket_shutdown_event (asyncio.Event) replaces the 250ms poll — _socket_sleep_with_backoff correctly uses wait_for; seconds=0 is well-defined (immediate TimeoutError) and unreachable here since _socket_initial_backoff_s = 1.0. (Hazard #5)
  • Reconnect resets backoff = self._socket_initial_backoff_s after a successful connect; old SocketModeClient is disconnect()'d before the next iteration's SocketModeClient(...). No instance leak. (Hazard #11)
  • Lazy import of slack_sdk.socket_mode.aiohttp inside start_socket_mode / _socket_mode_loop. (Hazard #10)
  • mode="webhook" default preserves all existing webhook behavior; the dispatch path branches only on the x-slack-socket-token header and self._mode == "socket". Backwards-compatible.
  • create_slack_adapter rejects mode="socket" + multi-workspace upfront — a real footgun that upstream silently allows.
  • Outer reconnect layer + slack_sdk's auto_reconnect_enabled=True are documented divergence in the non-parity table; they don't fight because the outer loop only re-enters after the inner client is disconnect()'d and is_connected() returns False.

Posted by an automated reviewer agent. https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj


Generated by Claude Code

Hardens the socket-mode port against the seven review findings on
PR #86:

* Add ``connect_timeout_s`` config (default 30s) and wrap the initial
  socket-mode handshake in ``asyncio.wait_for`` so a hung
  ``SocketModeClient.connect()`` can't make ``initialize()`` block
  forever (hazard #11). On timeout we tear the loop down before raising.
* Reject forwarded socket events whose ``timestamp`` field is outside
  the same 5-minute window ``_verify_signature`` enforces, so a captured
  forwarded payload can't be replayed indefinitely (hazard #12).
* Narrow the ``contextlib.suppress`` in ``stop_socket_mode`` to
  ``CancelledError`` only — surprising loop crashes are no longer
  silently swallowed during shutdown.
* Replace ``asyncio.get_event_loop().create_task`` in the socket-mode
  ``wrap_async`` helper with ``get_running_loop`` (Python 3.12+
  compatibility, hazard #5).
* Have the socket-mode interactive branch ack with
  ``response_action: errors`` instead of an empty ack when dispatch
  raises — an empty ack on ``view_submission`` silently closes the
  modal so the user sees no signal anything went wrong.
* Drop ``is_ext_shared_channel`` from the synthesized ``event_callback``
  payload in the socket-mode events_api branch so socket and webhook
  paths feed identical shapes into ``_process_event_payload`` (hazard
  #7).
* Add a regression test firing two concurrent ``_route_socket_event``
  events_api dispatches for different teams via ``asyncio.gather`` to
  pin down the existing ``copy_context()`` isolation against future
  drift (hazard #6).

Also fixes a pre-existing B010 lint warning in the test stub.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
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-review of e1c8047 ("address PR #86 review findings")

Verified fixes (7/7)

  1. connect_timeout_s (default 30s) + wait_for + cleanup on timeout — Verified at src/chat_sdk/adapters/slack/adapter.py:256-258, 1399-1415. Regression test test_hung_connect_raises_timeout_and_cleans_up injects a 60s sleep and asserts _socket_task / _socket_client are nulled. test_connect_timeout_default_is_30s pins the default. PASS.
  2. 5-min freshness on forwarded events + replay test + missing-timestamp test — Implemented at adapter.py:817-827; tests at tests/test_slack_socket_mode.py:782-820. PASS for the check shape, but see medium #M1 below — the units are wrong.
  3. stop_socket_mode contextlib.suppress narrowed to (asyncio.CancelledError,)adapter.py:1451-1452. PASS.
  4. asyncio.get_running_loop() everywheregrep confirms no get_event_loop() remains in the file (all four call sites use get_running_loop()). PASS.
  5. Interactive dispatch error → response_action: errors ackadapter.py:1721; regression test asserts body_arg.get("response_action") == "errors". PASS.
  6. Concurrent cross-team asyncio.gather testtests/test_slack_socket_mode.py:648-723; the slow first-team resolver (asyncio.sleep(0.02)) interleaves the second dispatch, so it would actually fail without copy_context() isolation. Real test, not phantom. PASS.
  7. is_ext_shared_channel dropped from synthesized event_callbackadapter.py:1635-1641; test at :867-895. Matches upstream routeSocketEvent shape. PASS for parity, but see nit #N1.

uv run pytest tests/test_slack_socket_mode.py -q31 passed.

New findings

M1 — Forwarded-event timestamp wire format is incompatible with the upstream emitter (medium)

adapter.py:822 does abs(int(time.time()) - ts_int) > 300, treating ts_int as seconds. Upstream's forwardSocketEvent always serializes timestamp: Date.now() (packages/adapter-slack/src/index.ts:1837, 6188, 6217, 6244, 6285, 6314, 6353, 6393, 6537, 6584 — every test fixture confirms this) — i.e. milliseconds. A real JS-listener-emitted forwarded payload arrives with ts ≈ 1.78e12; the Python receiver computes abs(1.78e9 - 1.78e12) ≈ 1.78e12 > 300 and rejects with 401 every time, ~56,000 years of skew. The replay test only passes because it manually emits seconds (int(_time.time()) at tests/test_slack_socket_mode.py:296, 791). Since start_socket_mode_listener was intentionally not ported (docs/UPSTREAM_SYNC.md:498), the only expected producer is a JS process — which this rejects 100% of the time. Two viable fixes:

  • Detect ms vs s by magnitude (if ts_int > 10**12: ts_int //= 1000) and update tests to also exercise the JS-shaped wire format, or
  • Multiply the budget: > 300_000 and treat the field as ms.
    Either way, add a fidelity test that round-trips a literal Date.now()-shaped int (e.g. int(time.time() * 1000)).

M2 — Socket-mode loses external/shared-channel tracking (medium, latent upstream parity)

_process_event_payload at adapter.py:959 reads payload.get("is_ext_shared_channel") to populate _external_channels. The webhook path forwards the whole raw payload (so this works); the socket path now synthesizes a payload that omits the field (adapter.py:1635-1641). Downstream consumers of _external_channels (used for posting policies — _externalChannels at upstream 4359, 4449, 4787) silently never see socket-mode shared channels. Upstream has the same bug (its routeSocketEvent also drops it), so dropping the field matches upstream parity — but the previous reviewer's framing of this as a "fix" is misleading, and it is a real loss of functionality compared to the webhook path. Recommend either:

  • Carrying is_ext_shared_channel through on the socket path as a Python-only divergence (tracked in docs/UPSTREAM_SYNC.md), or
  • At minimum documenting the limitation in the socket-mode docs and the hazard table so it isn't quietly inherited at the next sync.

Nits

  • N1 — The "fix" framing in commit e1c8047 for finding #7 reads as defense-in-depth, but the actual behavior change drops a field downstream code consumes. A one-line comment in _process_event_payload near the is_ext_shared_channel branch noting "socket-mode payloads never carry this; mirrors upstream routeSocketEvent" would prevent re-introducing it on a future port.
  • N2_route_socket_event's 3 branches (events_api / slash / interactive) all add multi-workspace token resolution that upstream's routeSocketEvent doesn't have. This is reachable only by bypassing the factory (which rejects the combo at create_slack_adapter line 3729). Either remove the dead resolver paths in _route_socket_event (keep the factory guard as the single point of truth), or move the multi-workspace ValidationError into SlackAdapter.__init__ so the dead branches can be removed entirely.
  • N3connect_timeout_s field default is 30.0 in SlackAdapterConfig (types.py:63), making the if config.connect_timeout_s is not None else 30.0 ternary at adapter.py:256-258 dead unless a caller explicitly passes None. Either drop the ternary or default the field to None.
  • N4asyncio.wait_for wrapping a shielded asyncio.wait(..., FIRST_COMPLETED) (adapter.py:1399-1407) means the inner asyncio.wait keeps running after timeout. It currently completes naturally because stop_socket_mode() cancels _socket_task and the wait_task.cancel() resolves the wait_task — so it's safe, but a comment would help future readers (or replace the shield with explicit cancellation of _socket_task in the timeout branch).
  • N5 — The slash_commands socket branch ignores the _handle_slash_command return value (matches upstream — slash responses go via response_url), but unlike upstream there's no wrapAsync(...).then(forwardToResponseUrl) wiring. This is consistent with how the webhook slash path also expects handlers to use response_url themselves, but worth a # slash responses are out-of-band via response_url, not the ack comment in _route_socket_event.

Upstream parity coverage hunted

  • Lifecycle: disconnect()stop_socket_mode() matches upstream disconnect(). PASS.
  • Reconnect: Python adds an outer reconnect loop on top of slack_sdk's built-in auto_reconnect_enabled; upstream relies entirely on @slack/socket-mode's internal reconnect. Documented as belt-and-suspenders in UPSTREAM_SYNC.md:497. PASS.
  • start_socket_mode_listener (transient serverless variant): intentionally not ported — UPSTREAM_SYNC.md:498. The forwarded-event receiver is ported, which is why M1 matters.
  • Ack protocol: upstream-parity for events_api (empty), slash (empty), interactive (parsed JSON body or empty). PASS.
  • Test fidelity vs upstream index.test.ts:5840-6600: all 5 factory-validation tests, all 4 socket-mode handleWebhook tests, all 6 routeSocketEvent dispatch tests, all 7 socket-mode-forwarding tests, the 3 startSocketModeListener tests (intentionally skipped per #498), and 3 routeSocketEvent-with-options tests are covered, plus 7 review-finding regressions. PASS.

Re-review verdict: FOLLOW-UP NEEDED — fixes 1, 3, 4, 5, 6, 7 are solid; fix 2 (forwarded-event freshness) is wired correctly but uses the wrong time unit and would reject every real upstream-emitted forwarded payload. M1 is a one-liner; M2 is a doc/decision item.

Posted by an automated re-reviewer agent. https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj


Generated by Claude Code

claude added 2 commits May 9, 2026 20:32
…stamps

Address PR #86 re-review M1 (critical interop break). Upstream's
forwardSocketEvent always emits ``timestamp: Date.now()`` — milliseconds
since epoch. Python's ``time.time()`` returns seconds. The previous
freshness check compared the two directly, treating a real JS-emitted
ms timestamp (~1.78e12) as 56,000 years skewed and rejecting every real
forwarded event with 401. Since startSocketModeListener was intentionally
not ported (UPSTREAM_SYNC.md non-parity), the only expected producer is a
JS process — making this a 100% interop break.

Auto-detect the unit by magnitude: anything > 10**11 is ms (that
magnitude crossed in 2001), normalize to seconds before comparing to
``time.time()``. Accepts both wire formats so a future Python-emitted
listener also works.

Adds two tests:
- test_js_emitted_milliseconds_timestamp_accepted: a real Date.now()-shaped
  ms timestamp now passes freshness.
- test_js_emitted_milliseconds_replay_rejected: a 6-minute-old ms timestamp
  is still rejected.

All 33 socket-mode tests pass.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
patrick-chinchill pushed a commit that referenced this pull request May 10, 2026
Final upstream-coverage audit before merging the 7 sync PRs (#84-#90)
identified one undocumented N/A item:

vercel/chat#415 (Teams SDK 2.0.8 + User-Agent) is a JS-only botbuilder
dependency bump. The Python Teams adapter uses raw aiohttp (no
botbuilder), so there is no equivalent dependency to bump. The optional
User-Agent: Vercel.ChatSDK header on the ~9 outbound aiohttp call sites
is a defense-in-depth nice-to-have; deferred as a follow-up rather than
landed in this sync.

Updates:
- CHANGELOG.md: tick all completed items and link them to their PRs
  (#84, #85, #86, #87, #88, #89, #90, plus already-merged PR #74).
  Document #415 inline as N/A.
- docs/UPSTREAM_SYNC.md non-parity table: add row for Teams User-Agent
  header divergence so future syncers don't try to "port" the JS bump.

Item #6 (concurrency.maxConcurrent) is already implementation-covered
in the Python port (existing divergence row at L492). The 4 new TS
concurrency tests in chat.test.ts have Python-specific equivalents at
test_chat_faithful.py L2969-3055 that don't name-match — leaving as
deferred fidelity-baseline polish since the behavior is verified.

Verdict from the coverage audit: all 18 substantive ports across PRs
#84-#90 are upstream-verified. No commits in chat@4.26.0..f55378a were
missed. Ready to start merging.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
* N1: comment near ``_process_event_payload`` is_ext_shared_channel branch
  noting socket-mode payloads never carry this field — mirrors upstream's
  ``routeSocketEvent`` shape, documented as a known divergence so it isn't
  quietly re-introduced on a future port.
* N3: drop the dead ``is not None else 30.0`` ternary; the config field
  is typed ``float`` with a 30s default so the fallback is unreachable.
* N4: comment on ``asyncio.wait_for(asyncio.shield(asyncio.wait(...)))``
  explaining why the shielded inner wait doesn't leak — both inner tasks
  get cancelled in the timeout branch.
* N5: comment near the ``slash_commands`` ack noting slash responses are
  out-of-band via ``response_url``, not the WebSocket ack.

Comment-only / dead-code-only changes; no behavior change. All 33
socket-mode tests pass; full suite 3701 passed (1 pre-existing failure).

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
``github-code-quality`` and other static analyzers periodically flag
the ``await wait_task`` / ``await task`` inside
``with contextlib.suppress(asyncio.CancelledError):`` blocks in
``_socket_connect_with_timeout`` (~L1432) and ``stop_socket_mode``
(~L1471) as "Statement has no effect" — they model ``await``
syntactically rather than as a side-effecting suspension that drains
a cancelled task.

The pattern is intentional and load-bearing:

- After ``task.cancel()`` we MUST ``await`` the task to deterministically
  drain it. Without the await, the cancellation request is queued but
  the function returns before the task actually tears down.
- For ``stop_socket_mode`` this breaks ``test_stop_idempotent`` (a
  second call can race the first's cleanup) and risks "Task was
  destroyed but it is pending!" warnings from the asyncio loop.
- ``contextlib.suppress(asyncio.CancelledError)`` absorbs the expected
  ``CancelledError`` raised by the awaited task on cancel.

The same false-positive pattern was flagged and explained on earlier
commits — the inline comments now make the intent visible at the
source so future readers (human or bot) don't have to re-discover it
via review history.

Tests: 33 socket-mode tests pass, ruff clean.
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
@patrick-chinchill patrick-chinchill marked this pull request as ready for review May 21, 2026 20:57
@patrick-chinchill patrick-chinchill marked this pull request as draft May 22, 2026 03:33
@patrick-chinchill patrick-chinchill marked this pull request as ready for review May 22, 2026 03:33
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: d4a0cdd5a4

ℹ️ 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
except ImportError as exc: # pragma: no cover - import-time failure
raise ValidationError(
"slack",
"slack_sdk is not installed. Install with `pip install chat-sdk[slack]`.",
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 Fix socket-mode dependency guidance in import error

When start_socket_mode() cannot import slack_sdk.socket_mode.aiohttp (for example because aiohttp is missing), this error tells users to install chat-sdk[slack], but this commit adds aiohttp only under the slack-socket extra in pyproject.toml. Following the current message can leave users in a broken loop where socket mode still fails after reinstall, so the runtime guidance should point to the extra that actually satisfies the dependency.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in ab8cd26.

The error message now points at chat-sdk[slack-socket] (the extra that actually pulls in aiohttp per pyproject.toml:51), so users following the hint will land on a working install. Added a regression test (TestSocketModeImportError) that asserts the message references chat-sdk[slack-socket] and explicitly guards against drifting back to chat-sdk[slack].


Generated by Claude Code

…extra

The aiohttp transport lives behind chat-sdk[slack-socket] (which adds
aiohttp on top of slack-sdk), not chat-sdk[slack]. Following the old
hint left users stuck because aiohttp was still missing after reinstall.

Adds a regression test covering the import-error pathway so the hint
can't drift back to the wrong extras name.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
Comment thread src/chat_sdk/adapters/slack/adapter.py Fixed
Copy link
Copy Markdown
Collaborator Author

@codex review

Latest commits since the prior review:

  • b312f36 re-review nits
  • d4a0cdd load-bearing-await comments
  • ab8cd26 socket-mode dependency-guidance fix

Worth a fresh pass on the reconnect / shutdown lifecycle (hazard #5), forwarded-event freshness (hazard #12), and ContextVar propagation (hazard #6). The is_ext_shared_channel socket-vs-webhook divergence and the multi-workspace defense-in-depth branches in _route_socket_event are both documented and don't need to be re-flagged.


Generated by Claude Code

… path

PR #86 commit ab8cd26 added
``test_missing_aiohttp_transport_message_points_at_slack_socket_extra``
which exercises this branch directly, so the
``# pragma: no cover - import-time failure`` marker is stale. Removing
it keeps coverage honest and prevents future regressions in the
hint-message wording from silently slipping past coverage gating.

Self-review (PR #86): no behavior change, just the inline marker.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
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: ab8cd26d5b

ℹ️ 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".

app's events for one workspace) and silently mixing the two would mask
a config mistake.
"""
if config is not None and (config.mode or "webhook") == "socket" and (config.client_id or config.client_secret):
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 Reject env-sourced OAuth creds in socket mode factory

Extend this guard to include credentials loaded from environment variables, not just config.client_id / config.client_secret. Right now create_slack_adapter(SlackAdapterConfig(mode="socket")) succeeds when SLACK_CLIENT_ID and SLACK_CLIENT_SECRET are set, even though the factory/docstring says multi-workspace OAuth is unsupported in socket mode; that silently admits an unsupported configuration and defers failure to runtime behavior instead of failing fast at construction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

@codex review

Re-requesting on the latest state (commit cf67818 since your prior P2 on the socket-mode dependency-guidance message).

Fresh focus: confirm the import-error hint now points at chat-sdk[slack-socket] and that the new regression test test_missing_aiohttp_transport_message_points_at_slack_socket_extra exercises the branch. The follow-up self-review pass cleared the stale pragma: no cover marker so coverage now enforces the hint wording.


Generated by Claude Code

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

ℹ️ 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".

# model ``await`` syntactically rather than as a
# side-effecting suspension — that's a false positive;
# do not remove this line.
await wait_task
# this as "statement has no effect" because they model
# ``await`` syntactically — that's a false positive; do
# not remove this line.
await task
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: 3

🤖 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`:
- Line 195: Replace the truthy fallback using "mode = config.mode or 'webhook'"
with an explicit None check so falsy-but-valid values aren't overridden; change
to conditional assignment using the pattern "config.mode if config.mode is not
None else 'webhook'". Update the same pattern wherever similar fallbacks are
used in this module (the other occurrence flagged in adapter.py) to follow the
"x if x is not None else default" rule, referencing the variable name "mode" and
the "config.mode" attribute to locate the spots to change.

In `@tests/test_slack_socket_mode.py`:
- Around line 309-310: Replace bare truthiness checks on the mock method
chat.process_message.called with explicit call-count assertions: use
chat.process_message.assert_called_once() (or assert_called_once_with(...) if
specific args are expected) to ensure the handler is invoked exactly once;
update the same pattern at the other occurrences referenced (the assertions
around lines 366-367 and 397-400) so all tests assert exact call counts rather
than truthiness.
- Line 493: The negative assertion in the import-hint regression check
mistakenly includes a trailing backtick ("chat-sdk[slack]`") so it may miss the
actual bad hint; update the assertion to check for the correct string
"chat-sdk[slack]" (i.e., change the assert that currently reads assert
"chat-sdk[slack]`" not in msg, msg to assert "chat-sdk[slack]" not in msg, msg)
so the test properly guards against the wrong import hint (referencing the
failing assert and the msg variable).
🪄 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: 2ce4e35a-65da-4ca8-a5e1-2a3a5e08464b

📥 Commits

Reviewing files that changed from the base of the PR and between 1ddc8d3 and 1f3aa3c.

📒 Files selected for processing (6)
  • docs/UPSTREAM_SYNC.md
  • pyproject.toml
  • src/chat_sdk/adapters/slack/adapter.py
  • src/chat_sdk/adapters/slack/types.py
  • src/chat_sdk/types.py
  • tests/test_slack_socket_mode.py

if config is None:
config = SlackAdapterConfig()

mode = config.mode or "webhook"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace truthy fallback on mode with explicit None checks.

Using or here violates the repo rule and can silently coerce unexpected falsy values.

Suggested patch
-        mode = config.mode or "webhook"
+        mode = config.mode if config.mode is not None else "webhook"
...
-    if config is not None and (config.mode or "webhook") == "socket" and (config.client_id or config.client_secret):
+    if (
+        config is not None
+        and (config.mode if config.mode is not None else "webhook") == "socket"
+        and (config.client_id or config.client_secret)
+    ):
As per coding guidelines, use `x if x is not None else default` instead of `x or default` to avoid truthiness traps when porting TypeScript to Python.

Also applies to: 3825-3825

🤖 Prompt for 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.

In `@src/chat_sdk/adapters/slack/adapter.py` at line 195, Replace the truthy
fallback using "mode = config.mode or 'webhook'" with an explicit None check so
falsy-but-valid values aren't overridden; change to conditional assignment using
the pattern "config.mode if config.mode is not None else 'webhook'". Update the
same pattern wherever similar fallbacks are used in this module (the other
occurrence flagged in adapter.py) to follow the "x if x is not None else
default" rule, referencing the variable name "mode" and the "config.mode"
attribute to locate the spots to change.

Comment on lines +309 to +310
assert chat.process_message.called

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use explicit call-count assertions instead of .called truthiness.

These assertions can pass even if handlers are invoked multiple times. Prefer assert_called_once() (or assert_called_once_with(...)) to make regressions fail deterministically.

Suggested tightening
-        assert chat.process_message.called
+        chat.process_message.assert_called_once()
...
-        assert chat.process_message.called
+        chat.process_message.assert_called_once()
...
-        assert chat.process_slash_command.called
+        chat.process_slash_command.assert_called_once()

As per coding guidelines, “Every test must fail when the code is wrong… no bare truthiness checks when specific values are available.”

Also applies to: 366-367, 397-400

🤖 Prompt for 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.

In `@tests/test_slack_socket_mode.py` around lines 309 - 310, Replace bare
truthiness checks on the mock method chat.process_message.called with explicit
call-count assertions: use chat.process_message.assert_called_once() (or
assert_called_once_with(...) if specific args are expected) to ensure the
handler is invoked exactly once; update the same pattern at the other
occurrences referenced (the assertions around lines 366-367 and 397-400) so all
tests assert exact call counts rather than truthiness.

msg = str(exc_info.value)
assert "chat-sdk[slack-socket]" in msg, msg
# Guard against regressing back to the wrong extra name.
assert "chat-sdk[slack]`" not in msg, msg
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the negative assertion typo in the import-hint regression check.

The current string includes a trailing backtick, so it may miss the actual wrong hint text and weaken the guard.

Proposed fix
-        assert "chat-sdk[slack]`" not in msg, msg
+        assert "chat-sdk[slack]" not in msg, msg
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert "chat-sdk[slack]`" not in msg, msg
assert "chat-sdk[slack]" not in msg, msg
🤖 Prompt for 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.

In `@tests/test_slack_socket_mode.py` at line 493, The negative assertion in the
import-hint regression check mistakenly includes a trailing backtick
("chat-sdk[slack]`") so it may miss the actual bad hint; update the assertion to
check for the correct string "chat-sdk[slack]" (i.e., change the assert that
currently reads assert "chat-sdk[slack]`" not in msg, msg to assert
"chat-sdk[slack]" not in msg, msg) so the test properly guards against the wrong
import hint (referencing the failing assert and the msg variable).

@patrick-chinchill patrick-chinchill merged commit d4632ff into main May 28, 2026
10 checks passed
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