Skip to content

fix(slack): render_postable uses AST path for PostableMarkdown (closes #81)#82

Merged
patrick-chinchill merged 5 commits into
mainfrom
claude/review-pr-1003-placement-pmA7S
May 7, 2026
Merged

fix(slack): render_postable uses AST path for PostableMarkdown (closes #81)#82
patrick-chinchill merged 5 commits into
mainfrom
claude/review-pr-1003-placement-pmA7S

Conversation

@patrick-chinchill
Copy link
Copy Markdown
Collaborator

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

Summary

  • SlackFormatConverter.render_postable was routing PostableMarkdown and {"markdown": ...} dict inputs through a private regex helper (_markdown_to_mrkdwn) instead of the AST-based from_markdown path
  • The upstream TS SDK routes these through fromAst(parseMarkdown(text)) — this was an undocumented divergence
  • The regex path truncates URLs that contain parentheses (e.g. Wikipedia article links), producing broken <url|text output
  • Fix: both markdown branches in render_postable now call from_markdown; str and raw branches are unaffected

Root cause

# before
if "markdown" in message:
    return self._markdown_to_mrkdwn(message["markdown"])   # regex, stops at first ')'
if hasattr(message, "markdown"):
    return self._markdown_to_mrkdwn(message.markdown)

# after
if "markdown" in message:
    return self.from_markdown(message["markdown"])          # AST via parse_markdown
if hasattr(message, "markdown"):
    return self.from_markdown(message.markdown)

Additional changes in this PR

  • Deleted _markdown_to_mrkdwn — the method had no call sites after the fix; the TS SDK has no equivalent. Removes an undocumented divergence.
  • Added card + object-with-ast support to render_postable{"card": ...} dict, {"type": "card", ...} CardElement dict, {"ast": ...} dict, .card / .ast attribute branches, and str(message) fallback for unrecognized types.

Connection to chinchill-api

This is the SDK-side root cause of the PostableMarkdown streaming round-trip bug that caused chinchill-api to abandon PostableMarkdown in favour of PostableRaw + a local _SLACK_MARKDOWN_LINK_RE regex workaround (chinchill-api#1002, chinchill-api#948). Fixing render_postable here makes the SDK correct end-to-end for that use case.

Test plan

  • TestRenderPostable (7 tests): link conversion, dict markdown, bold, mixed, query-string URL, str passthrough, PostableRaw passthrough
  • TestRenderPostableAdditionalBranches (3 tests): {"raw": ...} dict, CardElement dict, .ast attribute, arbitrary object fallback, multiple simultaneous mentions
  • TestRenderPostableRemainingBranches (5 tests): same coverage, integrated into the existing test class
  • TestNodeRendering (6 tests): heading → bold, blockquote, thematic break, image with/without alt
  • TestExtractPlainTextAdditional (5 tests): strikethrough, bare URL, channel mentions
  • TestToBlocksWithTableAdditional (3 tests): non-dict AST, standalone table, column alignment
  • All 66 tests in test_slack_format.py pass; CI green on 3.10 / 3.11 / 3.12 / 3.13

Release

Bumps to 0.4.26.3 (Python-only patch; no upstream version change). Changelog updated.

https://claude.ai/code/session_01XZeM3apTXAHZmjndAqeKPw

Summary by CodeRabbit

  • New Features

    • Expanded Slack message handling to accept more input shapes: markdown payloads, AST-based content, card payloads, and dataclass-style message objects.
  • Bug Fixes

    • Improved fallback behavior for unrecognized message shapes to return meaningful text instead of empty output; fixed URL/formatting edge cases in markdown→Slack conversion.
  • Tests

    • Added extensive tests covering markdown, AST paths, card fallbacks, mentions, tables, and plain-text extraction.

…81)

SlackFormatConverter.render_postable was calling _markdown_to_mrkdwn (a
simple regex) for PostableMarkdown and {"markdown": ...} dict inputs.
The upstream TS SDK routes these through fromAst(parseMarkdown(text)),
which correctly handles all markdown constructs — including links — via
AST node handlers.

The regex had two failure modes the AST path avoids: (a) its link pattern
[^)]+ stops at the first ')' so URLs with parentheses were truncated, and
(b) it had no equivalent of the link-node branch in _node_to_mrkdwn, making
edge-case formatting harder to extend safely.

Change both markdown branches in render_postable to call from_markdown
instead. str and raw branches are unaffected — they correctly stay on
_convert_mentions_to_slack since those inputs are already mrkdwn.

Adds TestRenderPostable to test_slack_format.py covering the previously
untested render_postable(PostableMarkdown(...)) code path, including link
conversion, bold, mixed formatting, query-string URLs, and the str/raw
passthrough cases.

Closes #81

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

coderabbitai Bot commented Apr 30, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

SlackFormatConverter.render_postable now routes markdown, AST, and card-typed message inputs through their respective conversion methods (from_markdown, from_ast, card_to_fallback_text), supports dict and dataclass-style inputs with markdown/ast/card attributes, and falls back to str(message) for unsupported types instead of an empty string.

Changes

Slack Message Format Routing

Layer / File(s) Summary
Data Shape / Docstring
src/chat_sdk/adapters/slack/format_converter.py
Docstring updated to list additional supported input shapes (dict/dataclass with markdown, ast, card, raw, etc.).
Core Implementation
src/chat_sdk/adapters/slack/format_converter.py
render_postable expanded: dicts with markdown now use from_markdown, dicts with ast use from_ast, dicts/objects with card or type == "card" use card_to_fallback_text; dataclass-style .markdown, .ast, .card attributes are handled; unmatched dicts now return str(message) instead of "".
Tests
tests/test_slack_format.py
Added TestRenderPostable, TestRenderPostableRemainingBranches, TestNodeRendering, TestExtractPlainTextAdditional, and TestToBlocksWithTableAdditional to cover markdown via AST, raw passthrough, ast attribute handling, card fallback text extraction, node rendering, plain-text extraction edge cases, and table-to-blocks behaviors.
Metadata / Release Notes
pyproject.toml, CHANGELOG.md
Version bumped to 0.4.26.3 and changelog entry added describing the Slack markdown rendering change and expanded test coverage.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰
Hopping through code with a snug little twitch,
Markdown and AST find the right switch.
Cards and raw now travel their way,
No more lost links in parentheses' sway.
Tiny paws clap — tests light the pitch.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.90% 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 clearly describes the main fix: routing PostableMarkdown through AST path in render_postable, which directly addresses the primary changeset objective.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/review-pr-1003-placement-pmA7S

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

❤️ Share

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

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request transitions the SlackFormatConverter to use AST-based markdown conversion via from_markdown to ensure consistency with the TypeScript SDK, addressing issue #81. It also adds a comprehensive set of regression tests for the render_postable method. Feedback suggests further improving render_postable by adding support for card types and refining the fallback logic to prevent silent failures when handling unexpected message formats.

Comment on lines 84 to 95
if "raw" in message:
return self._convert_mentions_to_slack(message["raw"])
if "markdown" in message:
return self._markdown_to_mrkdwn(message["markdown"])
return self.from_markdown(message["markdown"])
if "ast" in message:
return self.from_ast(message["ast"])
# Dataclass-style objects
if hasattr(message, "markdown"):
return self._markdown_to_mrkdwn(message.markdown)
return self.from_markdown(message.markdown)
if hasattr(message, "ast"):
return self.from_ast(message.ast)
return ""
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 render_postable implementation in SlackFormatConverter is missing support for PostableCard and CardElement types, which are handled in the base class. Additionally, the fallback behavior returns an empty string instead of str(message), which can lead to silent failures for unexpected message shapes.

Consider aligning this implementation with BaseFormatConverter.render_postable to ensure parity and robustness. Also, when checking for the fallback value, use is not None to avoid silently ignoring falsy but valid values like 0.

Suggested change
if "raw" in message:
return self._convert_mentions_to_slack(message["raw"])
if "markdown" in message:
return self._markdown_to_mrkdwn(message["markdown"])
return self.from_markdown(message["markdown"])
if "ast" in message:
return self.from_ast(message["ast"])
# Dataclass-style objects
if hasattr(message, "markdown"):
return self._markdown_to_mrkdwn(message.markdown)
return self.from_markdown(message.markdown)
if hasattr(message, "ast"):
return self.from_ast(message.ast)
return ""
if "raw" in message:
return self._convert_mentions_to_slack(message["raw"])
if "markdown" in message:
return self.from_markdown(message["markdown"])
if "ast" in message:
return self.from_ast(message["ast"])
if "card" in message or message.get("type") == "card":
from chat_sdk.cards import card_to_fallback_text
return card_to_fallback_text(message.get("card") or message)
# Dataclass-style objects
if hasattr(message, "markdown"):
return self.from_markdown(message.markdown)
if hasattr(message, "ast"):
return self.from_ast(message.ast)
if hasattr(message, "card"):
from chat_sdk.cards import card_to_fallback_text
return card_to_fallback_text(message.card)
return str(message) if message is not None else ""
References
  1. When checking for optional values that can be falsy but valid (e.g., 0, empty string, empty list), use is not None instead of a truthiness check to avoid silently ignoring them.

Two pre-existing gaps in SlackFormatConverter.render_postable flagged in
review:

1. Card types (PostableCard / CardElement dict) were unhandled; the
   override returned "" instead of routing through card_to_fallback_text
   like the base class does.

2. The fallback for unknown message shapes was "" (silent failure)
   rather than str(message), which matches BaseFormatConverter and is
   safer for unexpected inputs.

Mirrors the base class structure for both cases. No new tests needed:
these branches are identical to the base class and covered by its suite.

https://claude.ai/code/session_01XZeM3apTXAHZmjndAqeKPw
@patrick-chinchill patrick-chinchill marked this pull request as ready for review May 6, 2026 22:47
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.

🧹 Nitpick comments (2)
tests/test_slack_format.py (1)

54-107: ⚡ Quick win

No test coverage for the new card and ast-dict branches added in render_postable.

TestRenderPostable guards the "markdown" / PostableMarkdown fix well, but the card handling code added in the same PR (lines 91–111 of format_converter.py) is entirely uncovered:

  • {"card": <card_payload>}card_to_fallback_text(message["card"])
  • {"type": "card", ...} + is_card_elementcard_to_fallback_text(message)
  • Object with .card attribute → card_to_fallback_text(message.card)
  • {"ast": <root>} dict → from_ast(message["ast"])

Any regression in those branches would go undetected. Adding a test per branch (even mocking card_to_fallback_text) costs very little.

🤖 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_format.py` around lines 54 - 107, Test coverage is missing
for the new card and ast-dict branches in render_postable; add unit tests in
TestRenderPostable that exercise: 1) dict message with "card" key -> ensure
render_postable calls card_to_fallback_text(message["card"]) (mock
card_to_fallback_text); 2) dict message with "type":"card" and where
is_card_element(message) is true -> mock card_to_fallback_text(message) and
assert output; 3) object with a .card attribute -> provide a simple object with
.card and mock card_to_fallback_text(message.card); and 4) dict with "ast" key
-> mock from_ast(message["ast"]) and assert render_postable returns the from_ast
result; use the existing TestRenderPostable setup and
SlackFormatConverter.render_postable to locate code paths.
src/chat_sdk/adapters/slack/format_converter.py (1)

205-221: ⚡ Quick win

Remove _markdown_to_mrkdwn — it is unreachable dead code.

This private method has no call sites in the codebase. The test suite confirms that render_postable should use the AST-based from_markdown conversion path, not the regex-based helper.

🤖 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/format_converter.py` around lines 205 - 221,
Remove the unreachable regex-based helper _markdown_to_mrkdwn from the Slack
format converter: delete the private method _markdown_to_mrkdwn (and any purely
internal references if present) so the module relies solely on the AST-based
from_markdown path; ensure no external code references this function remain and
run tests to confirm nothing else imports or uses _markdown_to_mrkdwn.
🤖 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.

Nitpick comments:
In `@src/chat_sdk/adapters/slack/format_converter.py`:
- Around line 205-221: Remove the unreachable regex-based helper
_markdown_to_mrkdwn from the Slack format converter: delete the private method
_markdown_to_mrkdwn (and any purely internal references if present) so the
module relies solely on the AST-based from_markdown path; ensure no external
code references this function remain and run tests to confirm nothing else
imports or uses _markdown_to_mrkdwn.

In `@tests/test_slack_format.py`:
- Around line 54-107: Test coverage is missing for the new card and ast-dict
branches in render_postable; add unit tests in TestRenderPostable that exercise:
1) dict message with "card" key -> ensure render_postable calls
card_to_fallback_text(message["card"]) (mock card_to_fallback_text); 2) dict
message with "type":"card" and where is_card_element(message) is true -> mock
card_to_fallback_text(message) and assert output; 3) object with a .card
attribute -> provide a simple object with .card and mock
card_to_fallback_text(message.card); and 4) dict with "ast" key -> mock
from_ast(message["ast"]) and assert render_postable returns the from_ast result;
use the existing TestRenderPostable setup and
SlackFormatConverter.render_postable to locate code paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3000bf21-7b1b-49b3-b13c-8d85ed10cac5

📥 Commits

Reviewing files that changed from the base of the PR and between c36c8aa and 4736baa.

📒 Files selected for processing (2)
  • src/chat_sdk/adapters/slack/format_converter.py
  • tests/test_slack_format.py

claude added 3 commits May 6, 2026 22:55
…nch tests

Remove _markdown_to_mrkdwn — a regex-based private method with no call sites
after render_postable was switched to the AST path. Deleting it restores
structural parity with the TS SDK (which has no equivalent method). Also adds
test coverage for the card dict branch, CardElement-style dict branch, .card
attribute branch, and the {"ast": ...} dict branch in render_postable.

https://claude.ai/code/session_01XZeM3apTXAHZmjndAqeKPw
…ranches

Add 19 new tests covering previously untested paths:

- render_postable: {"raw": ...} dict, CardElement dict, .ast attribute,
  arbitrary object fallback, multiple simultaneous @mentions
- _node_to_mrkdwn: heading (h1/h2 → bold), blockquote, thematic break,
  image with alt, image without alt
- extract_plain_text: strikethrough, bare URL, channel mention with name,
  bare channel, named user mention
- to_blocks_with_table: non-dict AST returns None, standalone table emits
  no extra section blocks, column alignment produces column_settings

https://claude.ai/code/session_01XZeM3apTXAHZmjndAqeKPw
@patrick-chinchill patrick-chinchill merged commit 04c0658 into main May 7, 2026
9 of 10 checks passed
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.

2 participants