[DES-196] Adapter webhook dispatch (Part B)#3
Merged
Conversation
Port GoogleChatAdapter Part B to match upstream adapter-gchat/src/index.ts: - handle_webhook accepts both direct HTTP MESSAGE/ADDED_TO_SPACE/REMOVED_FROM_SPACE events and Pub/Sub push envelopes, funneling both into _dispatch_event. - New pubsub.py helper detects envelope shape and unwraps CloudEvents metadata so the Workspace Events Pub/Sub flow routes through the same handler chain. - Outbound surface: post_message (plain text + Card v2), edit_message with update_mask=text,cards_v2, delete_message, add_reaction / remove_reaction (emoji → Unicode via DEFAULT_EMOJI_MAP), fetch_messages with pageToken/pageSize pagination, fetch_channel_info, fetch_channel_messages, list_threads, open_dm, start_typing, stream (placeholder → periodic edit → final flush). - open_modal raises chat.NotImplementedError with feature="modals" (Google Chat has no Slack-style modals; upstream uses Card v2 inline instead). - REST dispatch via pluggable _rest_client (tests inject SimpleNamespace + AsyncMock; production falls back to an httpx-backed attribute-walk shim). - ADDED_TO_SPACE triggers create_space_subscription when pubsub_topic is set; REMOVED_FROM_SPACE tears the cached subscription down. - Adapter Protocol conformance: adds initialize, disconnect, post_channel_message, subscribe/unsubscribe, get_channel_visibility; declares lock_scope="thread" and persist_message_history=False class attrs. Tests: new tests/test_dispatch.py with 16 cycles covering 3.2–3.8 plus the Phase 0 conformance test now flips GREEN (152 gchat tests pass). E2E skeleton at examples/e2e/gchat/echo.py — mirrors slack/echo.py shape, ast.parses cleanly, exits with clear message when required env vars missing.
- LinearAdapter now conforms to chat.types.Adapter Protocol (added lock_scope, persist_message_history, disconnect, subscribe, unsubscribe, post_channel_message, fetch_channel_info, fetch_channel_messages, list_threads, open_dm, open_modal, is_dm, get_channel_visibility). - Unsupported surfaces raise chat.errors.NotImplementedError with feature= kwarg (matches Teams/WhatsApp pattern). - remove_reaction upgraded from a silent warn no-op to a pinned chat.NotImplementedError(feature='removeReaction') stub; add_reaction remains fully implemented (Linear reactionCreate). - test_dispatch.py pins Comment webhook round-trip via Chat.handle_webhook (HMAC-SHA256 verified end-to-end, no monkeypatch) and the Author dataclass shape (guards against author-dict regression). - examples/e2e/linear/echo.py skeleton — LINEAR_API_KEY + LINEAR_WEBHOOK_SECRET env vars, fails clearly when missing. - docs/parity.md: Linear 'react' column updated to 'partial' with a dedicated stub-listing subsection for the 7 Linear stubs.
…f-test Add a parametrised cross-adapter dispatch test that routes canned webhook bodies through ``Chat.handle_webhook(<name>, body, headers)`` for all 8 shipped adapters (slack / gchat / discord / github / whatsapp / teams / linear / telegram) and asserts the expected handler fires. Body fixtures live under ``tests/__fixtures__/`` with provenance comments; signature- bearing adapters (Slack / GitHub / WhatsApp / Linear) compute HMAC signatures dynamically against known secrets, while Ed25519 (Discord) and JWT (Teams / GChat) verifiers are monkeypatched at factory time. Also add a parity-doc self-test that walks ``packages/`` and asserts every ``## Dispatch surface`` row in ``docs/parity.md`` corresponds to a real ``chat-adapter-<name>`` module — catches stale parity entries. Closes DES-196.
Three bugs surfaced running examples/e2e/slack/echo.py against a real workspace: - examples/e2e/_common.py: FastAPI could not resolve the `Request` type annotation on the webhook handler — imports lived inside the function scope but `from __future__ import annotations` stringifies hints, so FastAPI fell back to treating `request` as a query parameter and returned 422 before the body was ever parsed. Move the fastapi / uvicorn / Response imports to module level behind a defensive ImportError guard. - packages/chat-adapter-slack/src/chat_adapter_slack/adapter.py: SlackAdapter.initialize() now calls auth.test() to populate bot_user_id / bot_id when they weren't passed via config. Mirrors upstream's initialize — without this, Chat._detect_mention can never match `<@U_BOT>` on plain `message.channels` events and the `on_new_mention` handler never fires for non-app_mention traffic. - examples/e2e/slack/echo.py: handlers take (thread, message, context), matching Chat._dispatch_to_handlers; use `author.user_id` instead of the nonexistent `author.id`. Plus ruff RUF100 / format cleanup across the sibling echo skeletons. 214 Slack tests still green. Manual E2E verified live — Slack url_verification 200 + @-mention dispatch + in-thread subscribe/echo.
Adds the `--mode webhook|socket` flag the original plan flagged as a nice-to-have. `examples/e2e/_common.py` gains a `run_socket_client` helper that calls `Chat.initialize` (which in turn drives `SlackAdapter.connect`, opening the websocket), then blocks on SIGINT/SIGTERM and cleans up via `adapter.disconnect`. `--mode socket` requires SLACK_APP_TOKEN; defaults to webhook. Picks up $SLACK_MODE as a default if no flag is passed. Live-verified end-to-end: websocket connects, `auth.test` resolves identity, `apps.connections.open` succeeds, Slack routes @-mention + thread-message events through the socket, handlers fire, bot replies in-thread — no ngrok required.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the last gap blocking the v0.1.0 publish: adapter event dispatch.
handle_webhook/post_message/edit_message/delete_message/add_reaction/remove_reaction/ streaming).NotImplementedErrorstub (Teams 9, WhatsApp 7, Linear 7, Telegram 3, + Discord/GChat/GitHubopen_modal/channel-surface stubs) is pinned by a test and documented indocs/parity.md.chat.types.Adapteris a real@runtime_checkableProtocol(wasAny).isinstance(create_*_adapter(), Adapter)passes for all 8 adapters.Chat.handle_webhookis covered by a cross-adapter parametrized matrix inchat-integration-tests.docs/parity.mdhas a "Dispatch surface" section; a self-test enforces table/package drift can't land silently.Scope notes
thoughts/taras/plans/2026-04-22-des-196-adapter-webhook-dispatch.md(status flipped tocompleted).RED → GREEN → COMMITper phase.@Localillomention → bot replies → in-thread subscribe/echo). Other provider E2Es ship as runnable skeletons gated on creds.Commit trail
492652dAdapterProtocol + parity.md + CHANGELOG83328a1613c546d3f892ad0e3ba87b532b7db632a576c3244f734287c93342f0bf22e1Chat.handle_webhookmatrix + parity self-test56829d6_common.pyFastAPIRequestresolution,auth.test()to auto-populatebot_user_id, echo handler arityTotals
ruff check,ruff format --check,pytest packages/all greenmypy packages/chat/src: 32 pre-existing errors, baseline unchanged (documented as known gaps)Test plan
uv run ruff check packages/ && uv run ruff format --check packages/ && uv run pytest packages/parity.mdrow maps to a real package, and vice versa)uv sync --group e2e && uv run python examples/e2e/slack/echo.py+ ngrok → real workspace mention + thread echoexamples/e2e/<adapter>/echo.pyscripts (eachsys.exit()s with a clear message on missing creds — not gating for v0.1.0)SLACK_APP_TOKEN=xapp-... uv run python examples/e2e/slack/echo.py;--mode socketflag on echo.py is a nice-to-have followup; the adapter supports it via config already)Ticket: https://linear.app/desplega-labs/issue/DES-196