Skip to content

Commit 8c25a76

Browse files
committed
[slack] [bug] resolve structured streaming team context from interactive payloads
1 parent 4c24c94 commit 8c25a76

5 files changed

Lines changed: 454 additions & 9 deletions

File tree

packages/adapter-slack/src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2296,6 +2296,7 @@ interface MockableClient {
22962296
update: ReturnType<typeof vi.fn>;
22972297
delete: ReturnType<typeof vi.fn>;
22982298
};
2299+
chatStream: ReturnType<typeof vi.fn>;
22992300
conversations: {
23002301
open: ReturnType<typeof vi.fn>;
23012302
replies: ReturnType<typeof vi.fn>;

packages/chat/src/thread.test.ts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,15 +595,33 @@ describe("ThreadImpl", () => {
595595
}
596596
});
597597

598-
it("should pass stream options from current message context", async () => {
598+
it.each([
599+
{
600+
expectedTeamId: "T123",
601+
label: "team_id",
602+
raw: { team_id: "T123", type: "app_mention" },
603+
},
604+
{
605+
expectedTeamId: "T234",
606+
label: "team string",
607+
raw: { team: "T234", type: "message" },
608+
},
609+
{
610+
expectedTeamId: "T345",
611+
label: "team.id",
612+
raw: { team: { id: "T345" }, type: "block_actions" },
613+
},
614+
])("should pass stream options from Slack current message context via $label", async ({
615+
raw,
616+
expectedTeamId,
617+
}) => {
599618
const mockStream = vi.fn().mockResolvedValue({
600619
id: "msg-stream",
601620
threadId: "t1",
602621
raw: "Hello",
603622
});
604623
mockAdapter.stream = mockStream;
605624

606-
// Create thread with current message context
607625
const threadWithContext = new ThreadImpl({
608626
id: "slack:C123:1234.5678",
609627
adapter: mockAdapter,
@@ -614,7 +632,7 @@ describe("ThreadImpl", () => {
614632
threadId: "slack:C123:1234.5678",
615633
text: "test",
616634
formatted: { type: "root", children: [] },
617-
raw: { team_id: "T123" },
635+
raw,
618636
author: {
619637
userId: "U456",
620638
userName: "user",
@@ -630,6 +648,65 @@ describe("ThreadImpl", () => {
630648
const textStream = createTextStream(["Hello"]);
631649
await threadWithContext.post(textStream);
632650

651+
expect(mockStream).toHaveBeenCalledWith(
652+
"slack:C123:1234.5678",
653+
expect.any(Object),
654+
expect.objectContaining({
655+
recipientUserId: "U456",
656+
recipientTeamId: expectedTeamId,
657+
})
658+
);
659+
});
660+
661+
it("should derive recipientTeamId from Slack block_actions payloads for structured streams", async () => {
662+
const mockStream = vi.fn().mockResolvedValue({
663+
id: "msg-stream",
664+
threadId: "t1",
665+
raw: "Hello",
666+
});
667+
mockAdapter.stream = mockStream;
668+
669+
const threadWithActionContext = new ThreadImpl({
670+
id: "slack:C123:1234.5678",
671+
adapter: mockAdapter,
672+
channelId: "C123",
673+
stateAdapter: mockState,
674+
currentMessage: {
675+
id: "action-msg",
676+
threadId: "slack:C123:1234.5678",
677+
text: "",
678+
formatted: { type: "root", children: [] },
679+
raw: {
680+
actions: [
681+
{ action_id: "select-option", selected_option: { value: "option-a" } },
682+
],
683+
team: { domain: "workspace", id: "T123" },
684+
type: "block_actions",
685+
},
686+
author: {
687+
userId: "U456",
688+
userName: "user",
689+
fullName: "Test User",
690+
isBot: false,
691+
isMe: false,
692+
},
693+
metadata: { dateSent: new Date(), edited: false },
694+
attachments: [],
695+
},
696+
});
697+
698+
async function* structuredStream(): AsyncIterable<string | StreamChunk> {
699+
yield "Picking option...";
700+
yield {
701+
id: "task-1",
702+
status: "pending",
703+
title: "Thinking",
704+
type: "task_update",
705+
};
706+
}
707+
708+
await threadWithActionContext.post(structuredStream());
709+
633710
expect(mockStream).toHaveBeenCalledWith(
634711
"slack:C123:1234.5678",
635712
expect.any(Object),

packages/chat/src/thread.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,62 @@ function isAsyncIterable(
110110
);
111111
}
112112

113+
/**
114+
* Slack payloads carry the workspace ID in a few different shapes depending on
115+
* the webhook type. Message events usually expose `team_id`, while interactive
116+
* payloads commonly expose `team.id`.
117+
*/
118+
function extractSlackRecipientTeamId(raw: unknown): string | undefined {
119+
if (!raw || typeof raw !== "object") {
120+
return undefined;
121+
}
122+
123+
const payload = raw as {
124+
authorizations?: Array<{ team_id?: unknown }>;
125+
team?: { id?: unknown; team_id?: unknown } | string;
126+
team_id?: unknown;
127+
user?: { team_id?: unknown };
128+
};
129+
130+
if (typeof payload.team_id === "string" && payload.team_id.length > 0) {
131+
return payload.team_id;
132+
}
133+
134+
if (typeof payload.team === "string" && payload.team.length > 0) {
135+
return payload.team;
136+
}
137+
138+
if (payload.team && typeof payload.team === "object") {
139+
if (typeof payload.team.id === "string" && payload.team.id.length > 0) {
140+
return payload.team.id;
141+
}
142+
143+
if (
144+
typeof payload.team.team_id === "string" &&
145+
payload.team.team_id.length > 0
146+
) {
147+
return payload.team.team_id;
148+
}
149+
}
150+
151+
if (
152+
typeof payload.user?.team_id === "string" &&
153+
payload.user.team_id.length > 0
154+
) {
155+
return payload.user.team_id;
156+
}
157+
158+
const authorizationTeamId = payload.authorizations?.find(
159+
(authorization) =>
160+
typeof authorization.team_id === "string" &&
161+
authorization.team_id.length > 0
162+
)?.team_id;
163+
164+
return typeof authorizationTeamId === "string"
165+
? authorizationTeamId
166+
: undefined;
167+
}
168+
113169
export class ThreadImpl<TState = Record<string, unknown>>
114170
implements Thread<TState>
115171
{
@@ -574,12 +630,9 @@ export class ThreadImpl<TState = Record<string, unknown>>
574630
const options: StreamOptions = { ...callerOptions };
575631
if (this._currentMessage) {
576632
options.recipientUserId = this._currentMessage.author.userId;
577-
// Extract teamId from raw Slack payload
578-
const raw = this._currentMessage.raw as {
579-
team_id?: string;
580-
team?: string;
581-
};
582-
options.recipientTeamId = raw?.team_id ?? raw?.team;
633+
options.recipientTeamId = extractSlackRecipientTeamId(
634+
this._currentMessage.raw
635+
);
583636
}
584637

585638
// Use native streaming if adapter supports it

0 commit comments

Comments
 (0)