Skip to content

feat(chat): port onOptionsLoad handler (#50)#66

Merged
patrick-chinchill merged 7 commits intomainfrom
feat/options-load-50
Apr 24, 2026
Merged

feat(chat): port onOptionsLoad handler (#50)#66
patrick-chinchill merged 7 commits intomainfrom
feat/options-load-50

Conversation

@patrick-chinchill
Copy link
Copy Markdown
Collaborator

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

Summary

  • Ports onOptionsLoad + processOptionsLoad from upstream Vercel Chat: specific action IDs match before catch-all handlers, errors log-and-skip so later handlers still run, first handler result that isn't None wins (including [], which short-circuits — matches upstream TS if (options) where arrays are truthy).
  • Wires the Slack adapter's block_suggestion interactive payload to process_options_load and serializes results to {options: [...]} JSON.
  • Ports OPTIONS_LOAD_TIMEOUT_MS = 2500 verbatim using asyncio.wait_for + asyncio.shield so the orphaned handler task still logs late errors when the budget is exceeded.
  • New public exports: OptionsLoadEvent, OptionsLoadHandler.

Refs #50. Queued under ## Unreleased for bundled 0.4.26.2 release.

Fidelity impact

Closed 4 [Options Load] gaps in chat.test.ts.

Test plan

  • Full validation block from CLAUDE.md passes
  • 4 faithful test ports + 2 Slack adapter tests
  • fidelity check: 4 gaps closed

Notes

  • Scope: Slack-only. Upstream has no Teams wiring. Teams deferred to a follow-up issue.
  • 2.5s timeout ported verbatim from upstream.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Public option-load APIs for dynamic external-select options and registering options-load handlers
    • Slack integration: interactive option requests routed through options-load handlers with a 2.5s timeout; empty options returned on timeout and orphaned tasks are logged/managed
  • Tests

    • Added tests for options-load routing, handler precedence, timeout behavior, and wait-until registration
  • Documentation

    • Added changelog entry for the unreleased options-load and Slack behavior changes

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds external-select options-load support: new OptionsLoadEvent type and OptionsLoadHandler alias; Chat.on_options_load and Chat.process_options_load with handler registry and dispatch; Slack adapter routes block_suggestion payloads to process_options_load with a 2.5s timeout, orphaned-task handling, and JSON serialization; tests and changelog added.

Changes

Cohort / File(s) Summary
Types
src/chat_sdk/types.py
Adds OptionsLoadEvent dataclass and extends ChatInstance protocol with process_options_load signature.
Public Exports
src/chat_sdk/__init__.py
Exports OptionsLoadHandler and OptionsLoadEvent via package __all__.
Chat core
src/chat_sdk/chat.py
Adds OptionsLoadHandler alias, handler registry, Chat.on_options_load registration (action-scoped and catch-all), and Chat.process_options_load dispatch with per-handler exception logging and first-non-None short-circuiting.
Slack adapter
src/chat_sdk/adapters/slack/adapter.py
Handles block_suggestion payloads: constructs OptionsLoadEvent, calls process_options_load under asyncio.wait_for(..., 2.5s) + asyncio.shield, logs timeouts, attaches done-callback for late errors, optionally registers orphaned task with WebhookOptions.wait_until, and serializes up to 100 SelectOptionElement results into Slack {"options":[...]} JSON; returns {"options":[]} on timeout/error.
Tests
tests/test_chat_faithful.py, tests/test_options_load.py
Adds tests validating handler precedence, error resilience, Slack block_suggestion dispatch, timeout fallback behavior, and WebhookOptions.wait_until registration/edge cases.
Changelog
CHANGELOG.md
Adds Unreleased entry documenting new options-load APIs and Slack adapter behavior (documentation only).

Sequence Diagram(s)

sequenceDiagram
    participant Slack as Slack Client
    participant Adapter as SlackAdapter
    participant Chat as Chat Instance
    participant Handler as Options Handler

    Slack->>Adapter: POST interactive (block_suggestion) (action_id, value, user)
    Adapter->>Adapter: Parse payload → OptionsLoadEvent
    Adapter->>Chat: process_options_load(event) [await wait_for(2.5s) + shield]
    Chat->>Chat: Match handlers (action-specific → catch-all)
    Chat->>Handler: Invoke handler(s) until non-None result
    Handler-->>Chat: list<SelectOptionElement> or None
    Chat-->>Adapter: options list or None
    Adapter->>Adapter: Serialize up to 100 items → {"options":[...]}
    Adapter-->>Slack: HTTP 200 {"options":[...]} (or {"options":[]} on timeout/error)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Poem

🐰 I twitch my whiskers, sniff the thread,
I hop to handlers, follow where they're led,
Two-point-five beats — I wait and stare,
If none return, an empty list I bear,
I nibble logs and leave a carrot there. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.28% 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(chat): port onOptionsLoad handler' clearly summarizes the main change—porting the onOptionsLoad handler functionality to the Chat class, which is reflected across multiple files (chat.py, types.py, adapters, tests).
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/options-load-50

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

Comment thread src/chat_sdk/types.py
) -> Awaitable[ModalResponse | None]: ...
def process_options_load(
self, event: Any, options: WebhookOptions | None = None
) -> Awaitable[list[SelectOptionElement] | None]: ...
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.

False positive — standard typing.Protocol method body per PEP 544. Consistent with every other Protocol method in this file.

Comment thread tests/test_options_load.py Fixed
Copy link
Copy Markdown

@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 support for handling dynamic option loading for external selects, specifically porting Slack's block_suggestion dispatch logic. It introduces Chat.on_options_load and Chat.process_options_load to manage these events, allowing for both specific action ID handlers and catch-all handlers. The Slack adapter implementation includes a 2.5-second timeout budget and uses asyncio.shield to ensure that orphaned tasks can still log errors. Feedback was provided regarding the logic in process_options_load, suggesting that checking for is not None instead of truthiness would allow handlers to explicitly return an empty list and stop further processing.

Comment thread src/chat_sdk/chat.py Outdated
Comment on lines +966 to +967
if result:
return result
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The check if result: will skip handlers that return an empty list []. If the intention is to allow a handler to explicitly return an empty list of options and stop the search (meaning "I handled this ID and there are no matches"), this should be if result is not None:. However, if the goal is to find the first handler that actually provides options, the current implementation is correct. Given the description "first non-empty result wins", this seems intended, but it's worth noting that a specific handler returning [] will fall through to catch-all handlers.

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 3fedbbcif result is not None: now correctly handles handlers returning [] as an explicit "handled, no options" short-circuit, matching upstream TS if (options) where [] is truthy. See thread.py:951 docstring.

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: 186148aa94

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

)

load_task.add_done_callback(_late_error)
_pin_task(load_task)
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 Register timed-out options task with wait_until

When block_suggestion times out, the handler task is only pinned in-process and immediately returned, but it is never passed to WebhookOptions.wait_until. In runtimes that end request work unless tasks are explicitly registered (the same pattern already used elsewhere in this adapter), the timed-out process_options_load task can be terminated before completion, so the intended late-error logging path may never run. This breaks the commit’s stated timeout behavior specifically in webhook environments that rely on wait_until for background work.

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.

Addressed in 71e2113 — timed-out handler task now registers with options.wait_until (matching _handle_reaction_event pattern), then guarded with try/except in 6e96891 in case runtime-provided wait_until raises.

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 (2)
src/chat_sdk/chat.py (1)

87-87: Narrow OptionsLoadHandler return type instead of Any.

A stricter alias improves static checks and prevents accidental non-option return values from propagating.

♻️ Proposed type narrowing
-OptionsLoadHandler = Callable[[OptionsLoadEvent], Any]
+OptionsLoadHandler = Callable[
+    [OptionsLoadEvent],
+    Awaitable[list[SelectOptionElement] | None] | list[SelectOptionElement] | None,
+]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/chat_sdk/chat.py` at line 87, The alias OptionsLoadHandler currently
returns Any which is too permissive; change it to a stricter return type (e.g.,
None | OptionsType | Coroutine[Any, Any, OptionsType] depending on async usage)
so handlers can only return option objects or None. Update the
OptionsLoadHandler declaration to use the concrete OptionsType (or
Optional[OptionsType] / Awaitable[OptionsType]) and adjust any handler
implementations and call-sites expecting Any to match the new signature;
reference the OptionsLoadHandler and OptionsLoadEvent types when making this
change so static checkers validate handler returns correctly.
src/chat_sdk/types.py (1)

1368-1370: Use a typed event in process_options_load protocol signature.

event: Any weakens the contract for this process_* method and makes incorrect payload shapes easier to pass undetected.

♻️ Proposed typing fix
     def process_options_load(
-        self, event: Any, options: WebhookOptions | None = None
+        self, event: OptionsLoadEvent, options: WebhookOptions | None = None
     ) -> Awaitable[list[SelectOptionElement] | None]: ...

As per coding guidelines "Use typed dataclasses instead of raw dicts when passing data to process_* methods."

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

In `@src/chat_sdk/types.py` around lines 1368 - 1370, The protocol method
process_options_load currently accepts event: Any which weakens type safety;
replace Any with a specific typed dataclass (e.g., OptionsLoadEvent or
OptionsLoadPayload) and update the protocol signature to def
process_options_load(self, event: OptionsLoadEvent, options: WebhookOptions |
None = None) -> Awaitable[list[SelectOptionElement] | None]:, add/import that
dataclass in the module, and update any callers/tests to construct and pass the
typed dataclass instead of raw dicts so payload shapes are enforced at
compile-time.
🤖 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/adapters/slack/adapter.py`:
- Around line 1031-1037: Replace truthy `or` fallbacks with explicit None
checks: for user_name and full_name, retrieve username/name into temporaries and
assign using the `x if x is not None else y` pattern so falsy-but-valid values
(e.g. empty string or 0) are preserved; likewise replace payload.get("value") or
"" passed into OptionsLoadEvent.query with `val if val is not None else ""`
where val = payload.get("value"); apply the same change to the other occurrence
noted (the variable named result around the referenced location) and any similar
uses in this adapter so all fallbacks use explicit `is not None` coalescing
rather than `or`.

In `@tests/test_options_load.py`:
- Around line 87-88: The test helper currently builds the request body as raw
JSON with body = f"payload={json.dumps(payload)}", which can break when the JSON
contains & or =; update the helper so the JSON payload is URL-encoded before
building the body (use urllib.parse.quote_plus or urllib.parse.urlencode on the
payload JSON) and then call _make_signed_request(body) as before; reference the
body construction and the _make_signed_request call to locate and change the
code.

---

Nitpick comments:
In `@src/chat_sdk/chat.py`:
- Line 87: The alias OptionsLoadHandler currently returns Any which is too
permissive; change it to a stricter return type (e.g., None | OptionsType |
Coroutine[Any, Any, OptionsType] depending on async usage) so handlers can only
return option objects or None. Update the OptionsLoadHandler declaration to use
the concrete OptionsType (or Optional[OptionsType] / Awaitable[OptionsType]) and
adjust any handler implementations and call-sites expecting Any to match the new
signature; reference the OptionsLoadHandler and OptionsLoadEvent types when
making this change so static checkers validate handler returns correctly.

In `@src/chat_sdk/types.py`:
- Around line 1368-1370: The protocol method process_options_load currently
accepts event: Any which weakens type safety; replace Any with a specific typed
dataclass (e.g., OptionsLoadEvent or OptionsLoadPayload) and update the protocol
signature to def process_options_load(self, event: OptionsLoadEvent, options:
WebhookOptions | None = None) -> Awaitable[list[SelectOptionElement] | None]:,
add/import that dataclass in the module, and update any callers/tests to
construct and pass the typed dataclass instead of raw dicts so payload shapes
are enforced at compile-time.
🪄 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: ac0d3d01-1422-4d74-94bf-48b827af46a0

📥 Commits

Reviewing files that changed from the base of the PR and between d23b6d9 and 186148a.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/chat_sdk/__init__.py
  • src/chat_sdk/adapters/slack/adapter.py
  • src/chat_sdk/chat.py
  • src/chat_sdk/types.py
  • tests/test_chat_faithful.py
  • tests/test_options_load.py

Comment thread src/chat_sdk/adapters/slack/adapter.py Outdated
Comment thread tests/test_options_load.py Outdated
patrick-chinchill added a commit that referenced this pull request Apr 24, 2026
- process_options_load: switch to `is not None` check so handlers
  returning [] short-circuit correctly (Port Rule #1 truthiness trap)
- Slack adapter: replace `or ""` fallbacks in OptionsLoadEvent
  construction with explicit None checks
- test helper: URL-encode payload JSON in signed-request body
- Narrow OptionsLoadHandler return type and typed Protocol signature
  for process_options_load (replace Any with OptionsLoadEvent)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread tests/test_options_load.py Fixed
patrick-chinchill added a commit that referenced this pull request Apr 24, 2026
…_until

Addresses Codex P2 on PR #66. When the 2.5s options-load budget fires,
the shielded handler task kept running in-process (pinned against GC and
wired to a late-error logging callback) but was never handed off to
`WebhookOptions.wait_until`. On serverless runtimes that end request
work unless tasks are registered — the same pattern other handlers in
this adapter already use — the orphan could be terminated before its
late-error path ran, silently dropping the commit's stated late-error
logging guarantee.

Registers the pinned task with `options.wait_until` when the timeout
path is taken and adds a regression test that asserts the task handed
to `wait_until` is still live at return time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread tests/test_options_load.py Fixed
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 the current code and only fix it if needed.

Inline comments:
In `@src/chat_sdk/adapters/slack/adapter.py`:
- Around line 1081-1083: The options.wait_until callback invocation is unguarded
and can throw, causing the timeout path to fail and not return the empty-options
response; wrap the call to options.wait_until(load_task) in a try/except that
catches any exception from the user-provided callback (preferably logging it)
and then proceed to return self._options_load_response([]) regardless, ensuring
failures in options.wait_until (referenced symbol: options.wait_until, parameter
load_task) cannot prevent the fallback response from _options_load_response.
🪄 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: 5458c149-b9af-40eb-914d-88661452fb98

📥 Commits

Reviewing files that changed from the base of the PR and between 186148a and 71e2113.

📒 Files selected for processing (4)
  • src/chat_sdk/adapters/slack/adapter.py
  • src/chat_sdk/chat.py
  • src/chat_sdk/types.py
  • tests/test_options_load.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/chat_sdk/types.py
  • src/chat_sdk/chat.py

Comment thread src/chat_sdk/adapters/slack/adapter.py
Comment thread tests/test_options_load.py Fixed
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 the current code and only fix it if needed.

Inline comments:
In `@tests/test_options_load.py`:
- Around line 149-159: The test leaves a 5s background task running; shorten the
sleep inside _slow_handler (e.g., to ~0.1s) so it doesn't tie up teardown and
ensure the test waits for completion by awaiting slow_done.wait() in the test's
finally block; apply the same change to the other slow handler instance
referenced around lines 191-193 so both slow_done and _slow_handler are cleaned
up deterministically.
🪄 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: f0d53048-0a32-434c-8fed-df8120f49142

📥 Commits

Reviewing files that changed from the base of the PR and between 71e2113 and 0c8eb77.

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

Comment thread tests/test_options_load.py

# Patch the module-level timeout so the test runs quickly (real
# 2.5s budget is exercised in fidelity-level production code).
import chat_sdk.adapters.slack.adapter as slack_adapter_mod
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.

Duplicate of earlier import-style nit — deferred. Style opinion, not enforced by ruff/pyrefly; the suggested sys.modules workaround is strictly uglier.


options = WebhookOptions(wait_until=_wait_until)

import chat_sdk.adapters.slack.adapter as slack_adapter_mod
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.

Duplicate of earlier import-style nit — deferred. Style opinion, not enforced by ruff/pyrefly; the suggested sys.modules workaround is strictly uglier.


options = WebhookOptions(wait_until=_raising_wait_until)

import chat_sdk.adapters.slack.adapter as slack_adapter_mod
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.

Duplicate of earlier import-style nit — deferred. Style opinion, not enforced by ruff/pyrefly; the suggested sys.modules workaround is strictly uglier.

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.

♻️ Duplicate comments (1)
tests/test_options_load.py (1)

221-223: ⚠️ Potential issue | 🟡 Minor

Await cancelled timeout tasks for deterministic teardown.

Line 222 and Line 282 still use 5s sleeps, and cleanup at Line 264-267 and Line 327-329 only calls .cancel() without awaiting completion. That can leak pending tasks into adjacent async tests.

Proposed patch
@@
         async def _slow_handler(event: Any, options: Any = None):
-            await asyncio.sleep(5.0)
+            await asyncio.sleep(0.2)
             return []
@@
         finally:
             slack_adapter_mod.OPTIONS_LOAD_TIMEOUT_MS = original_timeout
             # Cancel the still-running slow task so it doesn't leak.
             if registered and not registered[0].done():
                 registered[0].cancel()
+                await asyncio.gather(registered[0], return_exceptions=True)
@@
         async def _slow_handler(event: Any, options: Any = None):
-            await asyncio.sleep(5.0)
+            await asyncio.sleep(0.2)
             return []
@@
         finally:
             slack_adapter_mod.OPTIONS_LOAD_TIMEOUT_MS = original_timeout
             if slow_task_ref and not slow_task_ref[0].done():
                 slow_task_ref[0].cancel()
+                await asyncio.gather(slow_task_ref[0], return_exceptions=True)

As per coding guidelines, **/*.py: “Every async call must be awaited.”

Also applies to: 264-267, 281-283, 327-329

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

In `@tests/test_options_load.py` around lines 221 - 223, The test defines a
long-running async helper _slow_handler (await asyncio.sleep(5.0)) and several
teardown blocks simply call .cancel() on running tasks which can leak pending
tasks; replace the long 5.0s sleeps with a much shorter sleep (e.g. 0.1s) for
speed and, in every place that calls task.cancel() (the cleanup code referenced
around the handlers), immediately await the cancelled task inside a try/except
catching asyncio.CancelledError (or use asyncio.wait_for) to ensure the
cancellation completes before test exit so no pending tasks leak into adjacent
async tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/test_options_load.py`:
- Around line 221-223: The test defines a long-running async helper
_slow_handler (await asyncio.sleep(5.0)) and several teardown blocks simply call
.cancel() on running tasks which can leak pending tasks; replace the long 5.0s
sleeps with a much shorter sleep (e.g. 0.1s) for speed and, in every place that
calls task.cancel() (the cleanup code referenced around the handlers),
immediately await the cancelled task inside a try/except catching
asyncio.CancelledError (or use asyncio.wait_for) to ensure the cancellation
completes before test exit so no pending tasks leak into adjacent async tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7a022fb-d1d6-45db-9537-6e1fca4b5206

📥 Commits

Reviewing files that changed from the base of the PR and between 0c8eb77 and 3f970af.

📒 Files selected for processing (1)
  • tests/test_options_load.py

@patrick-chinchill
Copy link
Copy Markdown
Collaborator Author

Review verdict: needs follow-up. Refreshed and reviewed latest head 4cfa184 against main. The options-load behavior now matches upstream parity, including explicit empty-list results short-circuiting catch-all handlers. Remaining issue: src/chat_sdk/adapters/slack/adapter.py line 1058 introduces asyncio.ensure_future inside the async block_suggestion path, which violates this repo async rule. Use asyncio.get_running_loop().create_task(self._chat.process_options_load(event, options)) instead. Verification: tests/test_options_load.py plus tests/test_chat_faithful.py passed (105 passed, 2 skipped), and ruff passed.

patrick-chinchill and others added 7 commits April 24, 2026 02:45
Adds `Chat.on_options_load(...)` + `Chat.process_options_load(...)`
mirroring upstream's external-select suggestion lookup, and wires the
Slack adapter to route `block_suggestion` payloads through it — returning
a Slack-shaped options JSON response and falling back to empty options
(with a shielded background task that still logs late errors) after the
2.5s `OPTIONS_LOAD_TIMEOUT_MS` budget.

Closes 4 [Options Load] fidelity gaps in chat.test.ts.
Refs #50.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- process_options_load: switch to `is not None` check so handlers
  returning [] short-circuit correctly (Port Rule #1 truthiness trap)
- Slack adapter: replace `or ""` fallbacks in OptionsLoadEvent
  construction with explicit None checks
- test helper: URL-encode payload JSON in signed-request body
- Narrow OptionsLoadHandler return type and typed Protocol signature
  for process_options_load (replace Any with OptionsLoadEvent)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_until

Addresses Codex P2 on PR #66. When the 2.5s options-load budget fires,
the shielded handler task kept running in-process (pinned against GC and
wired to a late-error logging callback) but was never handed off to
`WebhookOptions.wait_until`. On serverless runtimes that end request
work unless tasks are registered — the same pattern other handlers in
this adapter already use — the orphan could be terminated before its
late-error path ran, silently dropping the commit's stated late-error
logging guarantee.

Registers the pinned task with `options.wait_until` when the timeout
path is taken and adds a regression test that asserts the task handed
to `wait_until` is still live at return time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…returns fallback

wait_until is user/runtime-provided; if it raises, the timeout branch
would fail the whole webhook instead of returning the empty-options
HTTP 200 fallback. Wrap the call in try/except with a warn log matching
the adjacent late-error callback's logger pattern, and add a regression
test that supplies a raising wait_until and asserts the handler still
returns status=200 with parsed body == {"options": []}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… parity)

Docstring said "first non-empty result wins" but the code (and upstream
TS) actually short-circuits on any non-None result — including []. A
handler returning [] is an explicit "I handled this action, show no
options", and blocks catch-all handlers by design. Matches upstream
chat.ts:920-923 where if(options) is truthy for [].

No behavior change; docstring + PR body wording only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shortens the mock slow handler and awaits its task in finally so the
5s sleep doesn't linger and flake adjacent async tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-2 fixup over-applied Port Rule #1. Upstream JS uses || chain
intentionally — empty-string username falls through to name/user_id.
The is-not-None chain preserved "" and diverged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@patrick-chinchill patrick-chinchill merged commit b48dc75 into main Apr 24, 2026
10 checks passed
@patrick-chinchill
Copy link
Copy Markdown
Collaborator Author

Noted from comment 4311953884 — asyncio.ensure_future at slack/adapter.py:1058 should be get_running_loop().create_task(...) per repo async convention. PR already merged (b48dc75); will fix in a followup cleanup PR along with the release PR.

patrick-chinchill added a commit that referenced this pull request Apr 24, 2026
Parity catch-up release for upstream chat@4.26.0. Bundles 8 PRs (#64 #65 #66 #67 #73 #74 #75 #76) + small followup cleanups. See CHANGELOG.md for details.
patrick-chinchill pushed a commit that referenced this pull request May 7, 2026
Per gemini-code-assist review on PR #83. Without the repo prefix, GitHub
auto-links the upstream PR numbers to local PRs in chat-sdk-python, which
collides with the local refs (#64, #66, #67, #74, #82) elsewhere in the
file. Use vercel/chat#NNN so the upstream refs link correctly.

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.

1 participant