Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/decode-slack-self-mention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chat-adapter/slack": minor
---

Decode the bot's own mention in incoming Slack messages. The adapter now resolves `<@U_BOT>` to the bot's display name (`@<DisplayName>`) the same way it resolves every other user mention, instead of leaving the raw user-ID markup in place, and sets `isMention` on the parsed message by detecting the bot's ID in the raw event text. This keeps `message.text` self-describing for downstream consumers (LLM prompts, classifiers) while preserving mention detection, which previously depended on the raw ID markup surviving in the text.
155 changes: 140 additions & 15 deletions packages/adapter-slack/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7135,10 +7135,7 @@ describe("resolveInlineMentions", () => {
callback: () => Promise<T>
): Promise<T>;
};
resolveInlineMentions(
text: string,
skipSelfMention: boolean
): Promise<string>;
resolveInlineMentions(text: string): Promise<string>;
}

it("resolves user mentions in incoming messages via webhook", async () => {
Expand Down Expand Up @@ -7200,7 +7197,7 @@ describe("resolveInlineMentions", () => {
expect(message.text).toContain("@John");
});

it("skips self-mention resolution in incoming webhooks", async () => {
it("resolves the bot's own mention and flags it in incoming webhooks", async () => {
const state = createMockState();
const chatInstance = createMockChatInstance({ state });
chatInstance.processMessage = vi.fn();
Expand All @@ -7212,11 +7209,13 @@ describe("resolveInlineMentions", () => {
botUserId: "U_BOT",
});

// users.info should NOT be called for the bot's own mention
const usersInfoMock = vi.fn().mockResolvedValue({
const usersInfoMock = vi.fn().mockImplementation(async (_id: string) => ({
ok: true,
user: { name: "sender", profile: { display_name: "Sender" } },
});
user: {
name: "user",
profile: { display_name: "Test User", real_name: "Test User" },
},
}));
mockClientMethod(adapter, "users.info", usersInfoMock);

await adapter.initialize(chatInstance);
Expand All @@ -7239,11 +7238,138 @@ describe("resolveInlineMentions", () => {
.mock.calls[0][2];
const message = await factory();

// Bot mention should NOT be resolved (kept as-is for mention detection)
expect(message.text).toContain("@U_BOT");
// The bot's own mention is decoded to its display name like any other
// mention, and the message is flagged as a mention from the raw event
// text (the ID markup no longer exists after resolution).
expect(message.text).toBe("@Test User help me");
expect(message.isMention).toBe(true);
});

it("flags the bot's own mention in rich text table cells", async () => {
const state = createMockState();
const chatInstance = createMockChatInstance({ state });
chatInstance.processMessage = vi.fn();

const adapter = createSlackAdapter({
botToken: "xoxb-test-token",
signingSecret: secret,
logger: mockLogger,
botUserId: "U_BOT",
});

mockClientMethod(
adapter,
"users.info",
vi.fn().mockImplementation(async ({ user }: { user: string }) => ({
ok: true,
user: {
name: user === "U_BOT" ? "testbot" : "sender",
profile: {
display_name: user === "U_BOT" ? "Test Bot" : "Sender",
},
},
}))
);

await adapter.initialize(chatInstance);

const body = JSON.stringify({
type: "event_callback",
team_id: "T123",
event: {
type: "message",
user: "U_SENDER",
channel: "C456",
text: "",
ts: "1234567890.676767",
blocks: [
{
type: "table",
rows: [
[
{
type: "rich_text",
elements: [
{
type: "rich_text_section",
elements: [{ type: "user", user_id: "U_BOT" }],
},
],
},
],
],
},
],
},
});
const request = createWebhookRequest(body, secret);
await adapter.handleWebhook(request);

const factory = (chatInstance.processMessage as ReturnType<typeof vi.fn>)
.mock.calls[0][2];
const message = await factory();

expect(message.text).toBe("@Test Bot");
expect(message.isMention).toBe(true);
});

it("falls back to the bot's user ID when users.info fails for its own mention", async () => {
const state = createMockState();
const chatInstance = createMockChatInstance({ state });
chatInstance.processMessage = vi.fn();

const adapter = createSlackAdapter({
botToken: "xoxb-test-token",
signingSecret: secret,
logger: mockLogger,
botUserId: "U_BOT",
});

const usersInfoMock = vi
.fn()
.mockImplementation(async ({ user }: { user: string }) => {
if (user === "U_BOT") {
// The Slack SDK surfaces API failures as error-like objects; lookupUser
// catches any rejection, so the shape is irrelevant to the fallback.
throw new Error("rate_limited");
}
return {
ok: true,
user: {
name: "user",
profile: { display_name: "Test User", real_name: "Test User" },
},
};
});
mockClientMethod(adapter, "users.info", usersInfoMock);

await adapter.initialize(chatInstance);

const body = JSON.stringify({
type: "event_callback",
team_id: "T123",
event: {
type: "app_mention",
user: "U_SENDER",
channel: "C456",
text: "<@U_BOT> help me",
ts: "1234567890.777777",
},
});
const request = createWebhookRequest(body, secret);
await adapter.handleWebhook(request);

const factory = (chatInstance.processMessage as ReturnType<typeof vi.fn>)
.mock.calls[0][2];
const message = await factory();

// Lookup failure degrades to the raw user ID with a mention prefix —
// the raw angle-bracket markup never resurfaces in the rendered text.
expect(message.text).toBe("@U_BOT help me");
expect(message.isMention).toBe(true);
});

it("skips request-scoped self mention resolution in multi-workspace mode", async () => {
it("resolves request-scoped self mention in multi-workspace mode", async () => {
const state = createMockState();
const chatInstance = createMockChatInstance({ state });
const adapter = createSlackAdapter({
Expand Down Expand Up @@ -7271,11 +7397,10 @@ describe("resolveInlineMentions", () => {
token: "xoxb-multi-token",
botUserId: "U_BOT_MULTI",
},
() => mentionAdapter.resolveInlineMentions("<@U_BOT_MULTI> help me", true)
() => mentionAdapter.resolveInlineMentions("<@U_BOT_MULTI> help me")
);

expect(result).toBe("<@U_BOT_MULTI> help me");
expect(usersInfoMock).not.toHaveBeenCalled();
expect(result).toBe("<@U_BOT_MULTI|Workspace Bot> help me");
});

it("resolves bare channel mentions in incoming messages", async () => {
Expand Down
Loading