Skip to content

fix(slack): NestBot async UX, Slack-safe AI replies, and deferred path hardening#4319

Draft
rajnisk wants to merge 3 commits intoOWASP:feature/nestbot-ai-assistantfrom
rajnisk:feature/nestbot-ai-interaction-ux-pr3
Draft

fix(slack): NestBot async UX, Slack-safe AI replies, and deferred path hardening#4319
rajnisk wants to merge 3 commits intoOWASP:feature/nestbot-ai-assistantfrom
rajnisk:feature/nestbot-ai-interaction-ux-pr3

Conversation

@rajnisk
Copy link

@rajnisk rajnisk commented Mar 19, 2026

Proposed change

Base: feature/nestbot-ai-assistant
Resolves: #3693
Delivers NestBot Slack interaction UX (async work, clear event boundaries, reaction / “Thinking…” lifecycle) and posting / formatting hardening so long answers and edge-case model output don’t break chat.postMessage or double-invoke the AI on deferred replies.

Interaction & async UX

  • App mentions and /ai: enqueue process_ai_query_async on the ai queue; empty /ai gets an ephemeral usage hint; enqueue failure surfaces an error and cleans up reactions where applicable.
  • Event boundaries: message_posted skips messages where the bot is @mentioned so app_mention owns mentions (avoids duplicate handling).
  • Reactions & “Thinking…”: 👀 when starting; remove 👀 on success, no-answer, and errors; 🤷 on no usable answer; temporary “Thinking…” removed in finally.
  • /ai replies: DM via conversations_open + chat_postMessage (private); failures fall back to ephemeral error when DM post fails.
  • Images: pass lightweight slack_image_files metadata into the worker; download / base64 in the worker (with sensible file count / size limits).

Slack posting & deferred auto-reply

  • No second LLM on answers: deferred path formats pre-rendered model text (does not call get_blocks/pipeline again on the answer).
  • markdown_blocks: split mrkdwn so each section stays under Slack’s 3000-character limit; cap blocks per message with a safe truncation path.
  • chat.postMessage text: fallback / notification text uses formatted body + truncate_for_slack_fallback (aligned with blocks, within API limits).
  • format_ai_response_for_slack: normalizes fences / backticks only; markdown links converted once when building blocks (no double format_links_for_slack).
  • Guards: ignore bare yes/no pipeline output; treat whitespace-only / empty-after-format as no-answer (🤷 + user message / error blocks where appropriate).

Tests

  • Blocks splitting, links, truncation budget, message_auto_reply / ai handler flows, YES/NO and empty-format cases.

Related

  • Supersedes #4320 (deferred reply formatting / no second model call / block limits)—same behavior included here.

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

Use async processing for mentions and /ai commands, tighten event boundaries, and stabilize reaction/thinking cleanup across success, no-answer, and error paths.

Made-with: Cursor
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • AI commands and mentions now process asynchronously for faster response times
    • Enhanced image handling and vision support for AI queries
    • Slash commands can now send responses via direct message
    • Improved error messages for failed AI generations
  • Bug Fixes

    • Better error recovery for Slack API failures
    • Improved bot mention detection in messages
    • Enhanced handling of long AI responses with proper formatting

Walkthrough

Enqueues Slack AI queries to an RQ background job across slash command, app_mention, and message-posted handlers; converts synchronous block rendering and image-downloads into async processing; adds Slack-sized markdown block splitting, fallback truncation, reaction/error handling, and extensive corresponding test updates.

Changes

Cohort / File(s) Summary
Command Handler
backend/apps/slack/commands/ai.py
Replaced synchronous render_blocks with handler(ack, command, client) that acks, validates/normalizes query, posts ephemeral usage hints on empty input, and enqueues process_ai_query_async to the ai RQ queue; logs truncated query hash on enqueue errors and suppresses Slack API errors when posting ephemerals.
App mention events
backend/apps/slack/events/app_mention.py
Stop inline image downloads/base64 encoding and direct replies; add reaction handling without payload inspection; enqueue process_ai_query_async with slack_image_files metadata; on enqueue failure attempt reaction removal and post fallback thread message, then re-raise.
Message posted events
backend/apps/slack/events/message_posted.py
Add per-team bot-user-id cache; improve mention detection (text + block parsing); handle auth_test()/users_info() Slack errors gracefully; replace exception-based thread-update checks with affected-row checks; defer AI import until job enqueue.
Auto-reply service & async job
backend/apps/slack/services/message_auto_reply.py
Introduce process_ai_query_async RQ job and refactor generate_ai_reply_if_unanswered to use markdown_blocks, standardized error constants/messages, "Thinking..." message lifecycle, reaction management (add/remove), Slack-file→data-URI conversion with caps, and routing for slash-command DMs vs in-channel posts.
Slack blocks & formatting utils
backend/apps/slack/blocks.py, backend/apps/slack/common/handlers/ai.py, backend/apps/slack/utils.py
Add markdown_blocks with per-section and per-message caps, _split_mrkdwn_text, and constants; switch handlers to use markdown_blocks; add truncate_for_slack_fallback and tighten format_ai_response_for_slack to strip/backtick-normalize without link formatting.
Tests
backend/tests/apps/slack/...
backend/tests/apps/slack/blocks_test.py, .../common/handlers/ai_test.py, .../services/message_auto_reply_test.py, .../commands/ai_test.py, .../events/*_test.py, .../utils_test.py
Update tests to assert enqueue behavior instead of synchronous replies, mock markdown_blocks/formatting/truncation helpers, cover empty/bare-YES/NO outputs, image metadata propagation, reaction error cases, enqueue failure handling, and new blocks/truncation behaviors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Agentic rag #2432: touches backend/apps/slack/common/handlers/ai.py and alters AI query processing and agent invocation; closely related to handler/formatting changes.
  • Add Image Extraction Support to NestBot #3925: modifies Slack image-handling pipeline and file-download interfaces; relates to slack_image_files metadata and image processing changes.
  • Nestbot MVP #2113: implements async RQ-based Slack AI processing across commands/events/services; overlaps substantially with enqueuing/job changes here.

Suggested labels

backend, Nestbot-ai-assistant

Suggested reviewers

  • kasya
  • arkid15r
  • rudransh-shrivastava
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(slack): NestBot async UX, Slack-safe AI replies, and deferred path hardening' accurately and specifically summarizes the main changes: async processing for Slack interactions, safe AI response formatting, and hardening of the deferred reply path.
Description check ✅ Passed The description thoroughly explains the proposed changes, including async UX improvements, event boundaries, reaction lifecycle, Slack posting hardening, and tests. It directly relates to the changeset and links to the resolved issue.
Linked Issues check ✅ Passed The PR addresses key objectives from #3693: reaction management (👀 added at start, removed on success/error, 🤷 for no-answer, no ✅), event boundaries for mentions, async processing with worker-side image handling, and formatting safety. It does not fully implement RAG search improvements and greeting routing, which are mentioned in the issue but partially out of scope.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to async UX, reaction/message lifecycle, Slack block formatting, and worker-side image handling. No unrelated refactoring, dependencies, or out-of-scope modifications are present.
Docstring Coverage ✅ Passed Docstring coverage is 95.60% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 4 files

Confidence score: 3/5

  • There is a concrete regression risk in backend/apps/slack/events/message_posted.py: returning on auth_test failure can skip question detection, message persistence, and enqueueing, so non-mention OWASP questions may be missed during transient Slack auth failures.
  • backend/apps/slack/events/app_mention.py now only rescues SlackApiError around reactions_add; other SDK/transport exceptions can bubble up and abort handle_event, which can block app-mention enqueueing.
  • Given the medium severity and fairly confident, user-facing processing impact in event handlers, this looks mergeable only with some caution rather than low-risk.
  • Pay close attention to backend/apps/slack/events/message_posted.py and backend/apps/slack/events/app_mention.py - exception handling changes may drop or abort Slack event processing paths.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/apps/slack/events/app_mention.py">

<violation number="1" location="backend/apps/slack/events/app_mention.py:73">
P2: `reactions_add` now only catches SlackApiError; other Slack SDK client/transport exceptions can bubble out and abort `handle_event`, preventing the enqueue of the app-mention processing. This is a regression from the previous broad exception guard and can drop mentions on transient request errors.</violation>
</file>

<file name="backend/apps/slack/events/message_posted.py">

<violation number="1" location="backend/apps/slack/events/message_posted.py:78">
P2: Returning on auth_test failure drops all subsequent message processing (question detection, message persistence, enqueueing), causing non-mention OWASP questions to be missed whenever Slack API auth_test transiently fails.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/apps/slack/commands/ai.py`:
- Around line 18-25: The custom handler method in ai.py overrides
CommandBase.handler and bypasses the global Slack commands gate; restore that
gate by either calling super().handler(ack, command, client) at the start of
ai.Command.handler or by explicitly checking settings.SLACK_COMMANDS_ENABLED
(the same logic used in CommandBase.handler) before ack/enqueuing work; ensure
the check prevents further handling when disabled and keep the existing
channel_id/user_id/query extraction after the gate passes.

In `@backend/apps/slack/events/app_mention.py`:
- Around line 96-103: The handler is synchronously downloading/base64-encoding
attachments and enqueuing large blobs as image_uris; change the enqueue call
that invokes django_rq.get_queue("ai").enqueue(process_ai_query_async, ...) to
pass lightweight references (e.g., slack file IDs or signed URLs) instead of the
full base64 image_uris, and move the download + base64-encoding logic into
process_ai_query_async so the worker fetches and encodes attachments when
running the job; update the parameter name (e.g., images -> image_refs) in both
the enqueue call and the process_ai_query_async signature to match.

In `@backend/apps/slack/events/message_posted.py`:
- Around line 59-78: The handler currently aborts on SlackApiError after calling
client.auth_test(), which causes normal messages to be dropped; change the
except SlackApiError block in MessagePosted (around client.auth_test() usage) to
not return—log the error with details (channel_id, team_id) and fall back to a
best-effort behavior by leaving bot_user_id unset (or setting a sentinel) so the
rest of the message handling proceeds and uses an alternative mention check
instead of relying on auth_test(); update any logic that uses
MessagePosted._bot_user_id_by_team and bot_user_id to tolerate a missing
bot_user_id and perform the approximate mention suppression accordingly.

In `@backend/apps/slack/services/message_auto_reply.py`:
- Around line 219-241: The success branch always posts publicly via
client.chat_postMessage; change it to mirror the no-answer branch routing so
`/ai` slash-command responses stay private: after building blocks =
_format_response_blocks(ai_response_text), if is_app_mention send
client.chat_postMessage to channel_id with thread_ts or message_ts, elif user_id
send client.chat_postEphemeral to channel_id for user_id (with text/blocks),
otherwise fallback to client.chat_postMessage; wrap API calls in
contextlib.suppress(SlackApiError) as done in the error path to match existing
behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: acf9bbaa-1fae-4028-9e9b-caf39cdca709

📥 Commits

Reviewing files that changed from the base of the PR and between d6b2e85 and 6b141ff.

📒 Files selected for processing (4)
  • backend/apps/slack/commands/ai.py
  • backend/apps/slack/events/app_mention.py
  • backend/apps/slack/events/message_posted.py
  • backend/apps/slack/services/message_auto_reply.py

- Async mentions/slash: reactions, thinking message cleanup, DM for /ai
- Defer image download to worker; pass slack_image_files metadata
- message_posted: skip when bot mentioned; auth_test failure continues
- Split AI mrkdwn into ≤3000-char blocks; truncate chat.postMessage fallback text
- Tests for markdown_blocks and message_auto_reply flows

Made-with: Cursor
@rajnisk rajnisk marked this pull request as draft March 19, 2026 22:40
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

7 issues found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/apps/slack/events/app_mention.py">

<violation number="1" location="backend/apps/slack/events/app_mention.py:71">
P2: `already_reacted` is no longer treated as a benign Slack idempotency case, so expected duplicates/retries are now logged as exceptions.</violation>
</file>

<file name="backend/apps/slack/events/message_posted.py">

<violation number="1" location="backend/apps/slack/events/message_posted.py:78">
P2: On `auth_test` failure, message handler now continues as non-mention, so mention messages may bypass mention guard and be processed in addition to `app_mention`, causing duplicate AI handling.</violation>
</file>

<file name="backend/tests/apps/slack/services/message_auto_reply_test.py">

<violation number="1" location="backend/tests/apps/slack/services/message_auto_reply_test.py:98">
P2: Success-path test no longer verifies the final AI reply channel, allowing channel-routing regressions to pass undetected.</violation>

<violation number="2" location="backend/tests/apps/slack/services/message_auto_reply_test.py:184">
P2: The Slack API error test assertion is overly permissive (`>= 1`) and can miss regressions where only the temporary "Thinking..." message is posted but the final AI response is not.</violation>
</file>

<file name="backend/apps/slack/utils.py">

<violation number="1" location="backend/apps/slack/utils.py:88">
P2: `truncate_for_slack_fallback` can exceed `max_len` when `max_len` is smaller than the truncation suffix, breaking truncation correctness.</violation>
</file>

<file name="backend/apps/slack/blocks.py">

<violation number="1" location="backend/apps/slack/blocks.py:85">
P2: `markdown_blocks` can raise `IndexError` when `max_blocks` is 0 (or negative) because it slices `parts` to empty and then accesses `parts[-1]`.</violation>

<violation number="2" location="backend/apps/slack/blocks.py:94">
P2: Truncation appends ellipsis without reserving space, so final section text can exceed the 3000-character limit.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

assert len(block_posts) == 1
assert block_posts[0].kwargs["blocks"] == response_blocks
assert block_posts[0].kwargs["text"] == truncate_for_slack_fallback(ai_text)
assert block_posts[0].kwargs["thread_ts"] == mock_message.slack_message_id
Copy link
Contributor

Choose a reason for hiding this comment

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

P2: Success-path test no longer verifies the final AI reply channel, allowing channel-routing regressions to pass undetected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/tests/apps/slack/services/message_auto_reply_test.py, line 98:

<comment>Success-path test no longer verifies the final AI reply channel, allowing channel-routing regressions to pass undetected.</comment>

<file context>
@@ -80,24 +86,16 @@ def test_generate_ai_reply_success(
+        assert len(block_posts) == 1
+        assert block_posts[0].kwargs["blocks"] == response_blocks
+        assert block_posts[0].kwargs["text"] == truncate_for_slack_fallback(ai_text)
+        assert block_posts[0].kwargs["thread_ts"] == mock_message.slack_message_id
 
     @patch("apps.slack.services.message_auto_reply.Message.objects.get")
</file context>
Suggested change
assert block_posts[0].kwargs["thread_ts"] == mock_message.slack_message_id
assert block_posts[0].kwargs["channel"] == mock_message.conversation.slack_channel_id
assert block_posts[0].kwargs["thread_ts"] == mock_message.slack_message_id

Copy link
Contributor

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/apps/slack/blocks.py`:
- Around line 87-95: The truncation for the last block doesn't reserve space for
the three-dot ellipsis so the final string can exceed
SLACK_SECTION_MRKDWN_MAX_CHARS; when computing room in the max_blocks branch
(variables: parts, max_blocks, suffix, SLACK_SECTION_MRKDWN_MAX_CHARS), subtract
the length of the ellipsis as well (or otherwise ensure the final concatenation
"..."+suffix fits), update the room check to compare against that reduced budget
and adjust the fallback when room < 1 accordingly so parts[-1] never produces a
section longer than SLACK_SECTION_MRKDWN_MAX_CHARS.

In `@backend/apps/slack/common/handlers/ai.py`:
- Around line 36-40: The current flow checks ai_response before formatting, but
format_ai_response_for_slack(ai_response) can normalize a non-empty input to an
empty string, causing markdown_blocks(formatted_response) to return an empty
payload; change the logic to format first and then check formatted_response for
truthiness: call format_ai_response_for_slack(ai_response), store to
formatted_response, and if formatted_response is empty/falsey return
get_error_blocks(), otherwise return markdown_blocks(formatted_response);
reference the existing symbols ai_response, format_ai_response_for_slack,
formatted_response, markdown_blocks, and get_error_blocks when making this
change.

In `@backend/apps/slack/services/message_auto_reply.py`:
- Around line 50-78: _post_slash_command_dm currently swallows SlackApiError and
returns None so callers (e.g., process_ai_query_async) can’t detect delivery
failures; change _post_slash_command_dm to return an explicit success/failure
signal (bool) or re-raise the caught SlackApiError so the caller can act on it —
update callers (like process_ai_query_async) to handle the boolean/exception and
implement the proposed fallback path (e.g., send ephemeral message or log and
surface error to user) when delivery fails; refer to function name
_post_slash_command_dm and the caller process_ai_query_async to locate and
update the code paths.
- Around line 31-33: The current code returns formatted blocks directly from
_format_response_blocks(text) without treating outputs that normalize to empty
as "no answer"; call format_ai_response_for_slack(text) first, assign to a
normalized_text variable, strip surrounding whitespace and code-fence markers
(as format_ai_response_for_slack does), then if normalized_text is empty treat
it as no-answer (trigger the 🤷 fallback) instead of returning blank blocks;
update _format_response_blocks to accept/return only when normalized_text is
non-empty and apply the same normalized_text check inside process_ai_query_async
(and the other similar flows referenced) so any model output that becomes empty
after normalization is handled as "no answer".
- Around line 36-47: The helper _slack_files_to_image_uris downloads and
base64-encodes entire Slack files in memory; to prevent worker OOM during
process_ai_query(), add a strict size guard: introduce a constant like
MAX_SLACK_IMAGE_BYTES and for each file use download_file in streaming/chunked
mode (or check Content-Length before fetching) to abort if the file exceeds the
limit, log/skipping oversized files, and only base64-encode files that completed
within the size cap; update references to download_file usage in
_slack_files_to_image_uris to support a max_bytes parameter or streaming API and
ensure the function still returns uris or None.

In `@backend/tests/apps/slack/blocks_test.py`:
- Around line 52-58: The test test_markdown_blocks_max_blocks_truncation_notice
currently only checks for the "truncated" notice; update it to also assert the
truncated block's text length respects Slack's 3000-character cap by adding an
assertion that len(blocks[-1]["text"]["text"]) <= 3000 (or equivalent
safe-length check) after calling markdown_blocks; this ensures the truncation
logic in markdown_blocks (see the truncation code path around the truncation
logic) prevents overflow.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b9a9b85f-0905-4e7c-8b45-2d03f59109ef

📥 Commits

Reviewing files that changed from the base of the PR and between 6b141ff and 5c07aad.

📒 Files selected for processing (10)
  • backend/apps/slack/blocks.py
  • backend/apps/slack/commands/ai.py
  • backend/apps/slack/common/handlers/ai.py
  • backend/apps/slack/events/app_mention.py
  • backend/apps/slack/events/message_posted.py
  • backend/apps/slack/services/message_auto_reply.py
  • backend/apps/slack/utils.py
  • backend/tests/apps/slack/blocks_test.py
  • backend/tests/apps/slack/common/handlers/ai_test.py
  • backend/tests/apps/slack/services/message_auto_reply_test.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/apps/slack/events/message_posted.py
  • backend/apps/slack/events/app_mention.py

@rajnisk rajnisk changed the title fix(ai): improve Slack interaction UX flow fix(slack): NestBot async UX, Slack-safe AI replies, and deferred path hardening Mar 19, 2026
- Enqueue process_ai_query_async for app mentions and /ai; reactions and worker-side images
- Split mrkdwn into Slack-sized blocks; truncate fallback text; format pipeline guards
- message_posted: mention boundary and conversation fetch hardening
- Update Slack tests (events, commands, auto-reply, blocks, utils, handlers)

Made-with: Cursor
@sonarqubecloud
Copy link

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 13 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/tests/apps/slack/events/app_mention_test.py">

<violation number="1" location="backend/tests/apps/slack/events/app_mention_test.py:19">
P2: Assertion helper uses `kwargs.get()` so expected `None` values don’t verify key presence, allowing missing enqueue kwargs to go undetected.</violation>
</file>

<file name="backend/apps/slack/services/message_auto_reply.py">

<violation number="1" location="backend/apps/slack/services/message_auto_reply.py:98">
P2: Missing/invalid Slack file size metadata is treated as 0, allowing oversized images to be fully downloaded before rejection.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

args, kwargs = mock_queue.enqueue.call_args
assert args[0] is process_ai_query_async
for key, value in expected.items():
assert kwargs.get(key) == value
Copy link
Contributor

Choose a reason for hiding this comment

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

P2: Assertion helper uses kwargs.get() so expected None values don’t verify key presence, allowing missing enqueue kwargs to go undetected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/tests/apps/slack/events/app_mention_test.py, line 19:

<comment>Assertion helper uses `kwargs.get()` so expected `None` values don’t verify key presence, allowing missing enqueue kwargs to go undetected.</comment>

<file context>
@@ -3,10 +3,20 @@
+    args, kwargs = mock_queue.enqueue.call_args
+    assert args[0] is process_ai_query_async
+    for key, value in expected.items():
+        assert kwargs.get(key) == value
 
 
</file context>
Suggested change
assert kwargs.get(key) == value
assert key in kwargs
assert kwargs[key] == value

try:
size = int(file.get("size") or 0)
except (TypeError, ValueError):
size = 0
Copy link
Contributor

Choose a reason for hiding this comment

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

P2: Missing/invalid Slack file size metadata is treated as 0, allowing oversized images to be fully downloaded before rejection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/apps/slack/services/message_auto_reply.py, line 98:

<comment>Missing/invalid Slack file size metadata is treated as 0, allowing oversized images to be fully downloaded before rejection.</comment>

<file context>
@@ -28,20 +31,86 @@
+        try:
+            size = int(file.get("size") or 0)
+        except (TypeError, ValueError):
+            size = 0
+        if not url or not mime.startswith("image/"):
+            continue
</file context>

Copy link
Contributor

@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 (3)
backend/apps/slack/services/message_auto_reply.py (2)

264-280: Consider using distinct error messages for ValueError vs SlackApiError.

Currently, both exceptions post ERROR_POSTING_RESPONSE, but a ValueError from process_ai_query is a processing error, not a posting failure. Consider catching them separately:

♻️ Suggested improvement
-    except (ValueError, SlackApiError):
+    except SlackApiError:
         logger.exception(
             "Error posting AI response",
             extra={"channel_id": channel_id, "message_ts": message_ts},
         )
         with contextlib.suppress(SlackApiError):
             client.chat_postMessage(
                 channel=channel_id,
                 text=ERROR_POSTING_RESPONSE,
                 thread_ts=message_ts,
             )
         with contextlib.suppress(SlackApiError):
             client.reactions_remove(
                 channel=channel_id,
                 timestamp=message_ts,
                 name="eyes",
             )
+    except ValueError:
+        logger.exception(
+            "Error processing AI query",
+            extra={"channel_id": channel_id, "message_ts": message_ts},
+        )
+        with contextlib.suppress(SlackApiError):
+            client.chat_postMessage(
+                channel=channel_id,
+                text=ERROR_UNEXPECTED_PROCESSING,
+                thread_ts=message_ts,
+            )
+        with contextlib.suppress(SlackApiError):
+            client.reactions_remove(
+                channel=channel_id,
+                timestamp=message_ts,
+                name="eyes",
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/slack/services/message_auto_reply.py` around lines 264 - 280,
Split the combined except block into two distinct handlers: one catching
ValueError (raised by process_ai_query) and one catching SlackApiError (raised
by client.chat_postMessage or client.reactions_remove). For the ValueError
handler, log the exception with context (channel_id, message_ts) and send a
different user-facing message (e.g., ERROR_PROCESSING_RESPONSE) via
client.chat_postMessage in the thread; for the SlackApiError handler, keep the
current logger.exception and continue to send ERROR_POSTING_RESPONSE and attempt
reactions_remove inside contextlib.suppress(SlackApiError). Update the exception
logging messages to reflect the specific failure (processing vs posting) and
reference process_ai_query, client.chat_postMessage, client.reactions_remove,
ERROR_POSTING_RESPONSE and the new ERROR_PROCESSING_RESPONSE constant/name.

324-326: Document precedence when both images and slack_image_files are provided.

If both parameters are supplied, images is silently discarded. While this may not happen in practice (app_mention provides slack_image_files, slash commands provide neither), clarifying the precedence in the docstring would prevent confusion.

📝 Suggested docstring update
 def process_ai_query_async(
     ...
 ):
-    """Process an AI query asynchronously (app mention or slash command)."""
+    """Process an AI query asynchronously (app mention or slash command).
+
+    If both `images` (pre-encoded data URIs) and `slack_image_files` (metadata requiring
+    download) are provided, `slack_image_files` takes precedence and `images` is ignored.
+    """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/slack/services/message_auto_reply.py` around lines 324 - 326,
The function that accepts the parameters images and slack_image_files currently
sets image_uris = images and then overrides it when slack_image_files is present
(via _slack_files_to_image_uris), silently discarding images; update the
function's docstring to explicitly state the precedence: when both images and
slack_image_files are provided, slack_image_files wins and images will be
ignored (and that image_uris will be derived from slack_image_files via
_slack_files_to_image_uris). Mention the parameter names images and
slack_image_files and the image_uris behavior so callers understand the
override.
backend/apps/slack/events/message_posted.py (1)

21-21: Consider adding TTL or eviction to the bot user ID cache.

The class-level _bot_user_id_by_team cache persists for the process lifetime. Per the Workspace model context, there's no signal-based invalidation when bot tokens rotate or workspaces change. While bot user IDs rarely change and process restarts clear this, a TTL-based approach (e.g., using @lru_cache with maxsize or a timestamped dict) would be more resilient for long-running workers.

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

In `@backend/apps/slack/events/message_posted.py` at line 21, The module-level
cache _bot_user_id_by_team currently lives for the process lifetime; replace it
with an eviction-capable cache to avoid stale bot IDs (e.g., use
functools.lru_cache with a bounded maxsize or a TTL cache such as
cachetools.TTLCache) or implement per-entry timestamps and expire entries after
a short TTL; update the code paths that read/write _bot_user_id_by_team (the
functions that fetch and store bot user IDs in this module) to use the new cache
API so entries are evicted/refreshed when tokens rotate or after the TTL
elapses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/apps/slack/common/handlers/ai.py`:
- Around line 51-63: The current gate inspects str(result) but returns the
original result, allowing wrapped sentinels or non-str types to slip through;
change the logic in this block (variables: result, text, stripped, channel_id,
logger) to normalize to a string up-front (e.g., coerce result to str, then
strip and casefold for checks) and if it passes, return the normalized stripped
string instead of the original result (or return None when empty or a bare
"yes"/"no"); ensure all subsequent code receives a plain str not the original
object.

In `@backend/apps/slack/utils.py`:
- Around line 101-113: The fence-stripping regexes in utils.py currently use
[\w]* and thus leave non-word language tags (e.g., "c++", "objective-c",
"python3.12") behind; update the logic that handles text starting/ending with
triple backticks and the code_block_pattern so the info string consumes all
characters up to the line break (no backticks or newlines). Replace the patterns
used when trimming the opening fence (currently in the if text.startswith("```")
and text.endswith("```") branch) and the code_block_pattern (used with
replace_code_block) to use a pattern like [^`\r\n]*(?:\r?\n)? for the opening
fence and (?:\r?\n) before the capture group so the full info string is removed
for functions/variables text, code_block_pattern, and replace_code_block.

---

Nitpick comments:
In `@backend/apps/slack/events/message_posted.py`:
- Line 21: The module-level cache _bot_user_id_by_team currently lives for the
process lifetime; replace it with an eviction-capable cache to avoid stale bot
IDs (e.g., use functools.lru_cache with a bounded maxsize or a TTL cache such as
cachetools.TTLCache) or implement per-entry timestamps and expire entries after
a short TTL; update the code paths that read/write _bot_user_id_by_team (the
functions that fetch and store bot user IDs in this module) to use the new cache
API so entries are evicted/refreshed when tokens rotate or after the TTL
elapses.

In `@backend/apps/slack/services/message_auto_reply.py`:
- Around line 264-280: Split the combined except block into two distinct
handlers: one catching ValueError (raised by process_ai_query) and one catching
SlackApiError (raised by client.chat_postMessage or client.reactions_remove).
For the ValueError handler, log the exception with context (channel_id,
message_ts) and send a different user-facing message (e.g.,
ERROR_PROCESSING_RESPONSE) via client.chat_postMessage in the thread; for the
SlackApiError handler, keep the current logger.exception and continue to send
ERROR_POSTING_RESPONSE and attempt reactions_remove inside
contextlib.suppress(SlackApiError). Update the exception logging messages to
reflect the specific failure (processing vs posting) and reference
process_ai_query, client.chat_postMessage, client.reactions_remove,
ERROR_POSTING_RESPONSE and the new ERROR_PROCESSING_RESPONSE constant/name.
- Around line 324-326: The function that accepts the parameters images and
slack_image_files currently sets image_uris = images and then overrides it when
slack_image_files is present (via _slack_files_to_image_uris), silently
discarding images; update the function's docstring to explicitly state the
precedence: when both images and slack_image_files are provided,
slack_image_files wins and images will be ignored (and that image_uris will be
derived from slack_image_files via _slack_files_to_image_uris). Mention the
parameter names images and slack_image_files and the image_uris behavior so
callers understand the override.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 63f38e4b-7b55-4a6b-971d-48b5aa17d416

📥 Commits

Reviewing files that changed from the base of the PR and between 5c07aad and 705484e.

📒 Files selected for processing (13)
  • backend/apps/slack/blocks.py
  • backend/apps/slack/common/handlers/ai.py
  • backend/apps/slack/events/app_mention.py
  • backend/apps/slack/events/message_posted.py
  • backend/apps/slack/services/message_auto_reply.py
  • backend/apps/slack/utils.py
  • backend/tests/apps/slack/blocks_test.py
  • backend/tests/apps/slack/commands/ai_test.py
  • backend/tests/apps/slack/common/handlers/ai_test.py
  • backend/tests/apps/slack/events/app_mention_test.py
  • backend/tests/apps/slack/events/message_posted_test.py
  • backend/tests/apps/slack/services/message_auto_reply_test.py
  • backend/tests/apps/slack/utils_test.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/tests/apps/slack/blocks_test.py
  • backend/apps/slack/events/app_mention.py
  • backend/tests/apps/slack/services/message_auto_reply_test.py

Comment on lines +51 to +63
if result is None:
return None
text = str(result)
stripped = text.strip()
if not stripped:
return None
if stripped.casefold() in {"yes", "no"}:
logger.info(
"Ignoring bare YES/NO pipeline output",
extra={"channel_id": channel_id},
)
]
return None
return result
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Base the no-answer gate on normalized text, and don’t return raw result.

Right now this branch classifies str(result) but returns the original object. That lets wrapped sentinels like `YES` / NO slip through, and it also means a non-str pipeline value can still leak into the later string-only Slack path.

Proposed fix
-    text = str(result)
-    stripped = text.strip()
-    if not stripped:
+    text = str(result)
+    normalized = format_ai_response_for_slack(text).strip()
+    if not normalized:
         return None
-    if stripped.casefold() in {"yes", "no"}:
+    if normalized.casefold() in {"yes", "no"}:
         logger.info(
             "Ignoring bare YES/NO pipeline output",
             extra={"channel_id": channel_id},
         )
         return None
-    return result
+    return text
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/slack/common/handlers/ai.py` around lines 51 - 63, The current
gate inspects str(result) but returns the original result, allowing wrapped
sentinels or non-str types to slip through; change the logic in this block
(variables: result, text, stripped, channel_id, logger) to normalize to a string
up-front (e.g., coerce result to str, then strip and casefold for checks) and if
it passes, return the normalized stripped string instead of the original result
(or return None when empty or a bare "yes"/"no"); ensure all subsequent code
receives a plain str not the original object.

Comment on lines 101 to +113
if text.startswith("```") and text.endswith("```"):
# Extract content from code block wrapper
# Remove first ``` and optional language identifier
text = re.sub(r"^```[\w]*\n?", "", text, count=1)
# Remove trailing ```
text = re.sub(r"\n?```$", "", text)
text = text.strip()

# Remove markdown code blocks (```language\ncode\n```) and convert to plain text
# This regex matches code blocks with optional language identifier
# Pattern: ```optional_lang\ncontent\n```
code_block_pattern = re.compile(r"```[\w]*\n(.*?)```", re.DOTALL)

def replace_code_block(match):
# Convert code block content to plain text
# This prevents Slack from rendering it as a code block
# Preserve Slack channel links that might be inside code blocks
return match.group(1).strip()

text = code_block_pattern.sub(replace_code_block, text)

# Remove any remaining triple backticks that might have been missed
# (handles edge cases where regex didn't match)
# Also handle cases where backticks are on separate lines
text = re.sub(r"```+", "", text)

# Remove single backticks that might wrap inline code
# But preserve Slack channel/user links (format: <#...|...> or <@...|...>)
# Pattern: `text` but not part of Slack link syntax
text = re.sub(r"`([^`<]+)`", r"\1", text)

# Preserve Slack channel links (format: <#channel_id|channel_name>)
# These should not be modified by format_links_for_slack
# Convert markdown links to Slack format (but preserve existing Slack links)
return format_links_for_slack(text)


# Import get_news_data and get_staff_data from owasp utils
return re.sub(r"`([^`<]+)`", r"\1", text)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fence stripping should consume the full info string, not just \w.

LLM outputs frequently use fence labels like c++, objective-c, or python3.12. With [\w]*, those cases leave the language tag behind, so an “empty” fenced response can end up posting c++/objective-c instead of falling back.

Proposed fix
-    if text.startswith("```") and text.endswith("```"):
-        text = re.sub(r"^```[\w]*\n?", "", text, count=1)
-        text = re.sub(r"\n?```$", "", text)
+    if text.startswith("```") and text.endswith("```"):
+        text = re.sub(r"^```[^`\r\n]*(?:\r?\n)?", "", text, count=1)
+        text = re.sub(r"(?:\r?\n)?```$", "", text)
         text = text.strip()

-    code_block_pattern = re.compile(r"```[\w]*\n(.*?)```", re.DOTALL)
+    code_block_pattern = re.compile(r"```[^`\r\n]*(?:\r?\n)(.*?)```", re.DOTALL)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/slack/utils.py` around lines 101 - 113, The fence-stripping
regexes in utils.py currently use [\w]* and thus leave non-word language tags
(e.g., "c++", "objective-c", "python3.12") behind; update the logic that handles
text starting/ending with triple backticks and the code_block_pattern so the
info string consumes all characters up to the line break (no backticks or
newlines). Replace the patterns used when trimming the opening fence (currently
in the if text.startswith("```") and text.endswith("```") branch) and the
code_block_pattern (used with replace_code_block) to use a pattern like
[^`\r\n]*(?:\r?\n)? for the opening fence and (?:\r?\n) before the capture group
so the full info string is removed for functions/variables text,
code_block_pattern, and replace_code_block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant