Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions agentex/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2563,10 +2563,7 @@ paths:
description: Successful Response
content:
application/json:
schema:
additionalProperties: true
type: object
title: Response Slack Commands Slack Commands Post
schema: {}
/slack/interactions:
post:
tags:
Expand All @@ -2578,10 +2575,7 @@ paths:
description: Successful Response
content:
application/json:
schema:
additionalProperties: true
type: object
title: Response Slack Interactions Slack Interactions Post
schema: {}
/tracker/{tracker_id}:
get:
tags:
Expand Down
25 changes: 19 additions & 6 deletions agentex/src/api/routes/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@
secrets microservice. Delegates all logic to SlackGatewayUseCase.
"""

from fastapi import APIRouter, BackgroundTasks, Request
from fastapi import APIRouter, BackgroundTasks, Request, Response
from fastapi.responses import JSONResponse

from src.domain.use_cases.slack_gateway_use_case import DSlackGatewayUseCase

router = APIRouter(prefix="/slack", tags=["Slack"])


def _slack_ack(result: dict) -> Response:
"""Serialize a slash-command / interaction response for Slack. An ack-only result
(empty dict) becomes a truly EMPTY 200 body — Slack renders a bare ``{}`` JSON body
as a stray message, so "show nothing" must send no body, not ``{}``. A non-empty
result (ephemeral message / response_action) is sent as JSON."""
return JSONResponse(result) if result else Response(status_code=200)


@router.post("/events", summary="Slack Events API ingress for the @agent app")
async def slack_events(
request: Request,
Expand All @@ -31,26 +40,30 @@ async def slack_events(
async def slack_commands(
request: Request,
use_case: DSlackGatewayUseCase,
) -> dict:
) -> Response:
# Slash commands are application/x-www-form-urlencoded, not JSON. Read the raw
# body first (needed for signature verification), then parse the form.
body = await request.body()
form = dict(await request.form())
headers = {k.lower(): v for k, v in request.headers.items()}
return await use_case.handle_slash_command(body=body, headers=headers, form=form)
return _slack_ack(
await use_case.handle_slash_command(body=body, headers=headers, form=form)
)


@router.post("/interactions", summary="Slack interactivity ingress (modals, shortcuts)")
async def slack_interactions(
request: Request,
background: BackgroundTasks,
use_case: DSlackGatewayUseCase,
) -> dict:
) -> Response:
# Interactions are form-encoded with a JSON `payload` field. Raw body first (for
# signature verification), then the form. The turn runs out-of-band like events.
body = await request.body()
form = dict(await request.form())
headers = {k.lower(): v for k, v in request.headers.items()}
return await use_case.handle_interaction(
body=body, headers=headers, form=form, background=background
return _slack_ack(
await use_case.handle_interaction(
body=body, headers=headers, form=form, background=background
)
)
2 changes: 1 addition & 1 deletion agentex/src/domain/use_cases/slack_gateway_use_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ async def _submit_agents_modal(
"elements": [
{
"type": "mrkdwn",
"text": f"via <@{user}> through *{agent}*",
"text": f"<@{user}> asked *{agent}*",
}
],
},
Expand Down
23 changes: 23 additions & 0 deletions agentex/tests/unit/api/test_slack_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Slack route ack helper: an ack-only result (empty dict) must be a TRULY EMPTY 200
body — Slack renders a bare ``{}`` JSON body as a stray message, so "show nothing" has
to send no body, not ``{}``. A non-empty result is sent as JSON."""

import json

import pytest
from fastapi.responses import JSONResponse
from src.api.routes.slack import _slack_ack


@pytest.mark.unit
class TestSlackAck:
def test_empty_result_is_empty_200_body(self):
resp = _slack_ack({})
assert resp.status_code == 200
assert resp.body == b"" # not b"{}" — nothing for Slack to render

def test_non_empty_result_is_json(self):
payload = {"response_type": "ephemeral", "text": "hi"}
resp = _slack_ack(payload)
assert isinstance(resp, JSONResponse)
assert json.loads(resp.body) == payload
Loading