Skip to content

Commit 1c01d51

Browse files
authored
fix(slack): correct auto-formatting of slack ids (#275)
1 parent edce943 commit 1c01d51

2 files changed

Lines changed: 121 additions & 2 deletions

File tree

packages/slack/src/message.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { formatMessage } from "./message";
3+
4+
describe("formatMessage", () => {
5+
describe("markdown to Slack formatting", () => {
6+
test("converts markdown links to Slack format", () => {
7+
expect(formatMessage("[click here](https://example.com)")).toBe(
8+
"<https://example.com|click here>"
9+
);
10+
});
11+
12+
test("converts double-star bold to single-star bold", () => {
13+
expect(formatMessage("**hello**")).toBe("*hello*");
14+
});
15+
});
16+
17+
describe("user ID mention wrapping", () => {
18+
test("wraps @-prefixed Slack user IDs", () => {
19+
expect(formatMessage("@U02UD2WE3HA")).toBe("<@U02UD2WE3HA>");
20+
});
21+
22+
test("wraps bare Slack user IDs", () => {
23+
expect(formatMessage("user U02UD2WE3HA said")).toBe(
24+
"user <@U02UD2WE3HA> said"
25+
);
26+
});
27+
28+
test("wraps workspace IDs starting with W", () => {
29+
expect(formatMessage("@W01AB2CD3EF")).toBe("<@W01AB2CD3EF>");
30+
});
31+
32+
test("does not double-wrap already bracketed IDs", () => {
33+
expect(formatMessage("<@U02UD2WE3HA>")).toBe("<@U02UD2WE3HA>");
34+
});
35+
36+
test("removes brackets from non-ID @handles", () => {
37+
expect(formatMessage("<@john.doe>")).toBe("@john.doe");
38+
});
39+
});
40+
41+
describe("false-positive prevention", () => {
42+
test("does not match pure-alpha words like WORKSPACE", () => {
43+
const input = "CODER_WORKSPACE_IS_PREBUILD_CLAIM=true";
44+
expect(formatMessage(input)).toBe(input);
45+
});
46+
47+
test("does not match UPPERCASE without digits", () => {
48+
expect(formatMessage("UNDEFINED")).toBe("UNDEFINED");
49+
expect(formatMessage("WORKSPACEID")).toBe("WORKSPACEID");
50+
});
51+
52+
test("does not match short IDs", () => {
53+
expect(formatMessage("U1234")).toBe("U1234");
54+
});
55+
});
56+
57+
describe("code block preservation", () => {
58+
test("does not format IDs inside inline code", () => {
59+
const input = "`U02UD2WE3HA`";
60+
expect(formatMessage(input)).toBe(input);
61+
});
62+
63+
test("does not format IDs inside code blocks", () => {
64+
const input = "```\nU02UD2WE3HA\n```";
65+
expect(formatMessage(input)).toBe(input);
66+
});
67+
68+
test("formats IDs outside code but preserves code content", () => {
69+
const input = "user U02UD2WE3HA said `U02UD2WE3HA`";
70+
expect(formatMessage(input)).toBe(
71+
"user <@U02UD2WE3HA> said `U02UD2WE3HA`"
72+
);
73+
});
74+
75+
test("preserves markdown links inside code blocks", () => {
76+
const input = "```\n[text](url)\n```";
77+
expect(formatMessage(input)).toBe(input);
78+
});
79+
80+
test("preserves bold syntax inside inline code", () => {
81+
const input = "`**not bold**`";
82+
expect(formatMessage(input)).toBe(input);
83+
});
84+
85+
test("handles multiple code blocks", () => {
86+
const input = "`U02UD2WE3HA` and U02UD2WE3HA and ```U02UD2WE3HA```";
87+
expect(formatMessage(input)).toBe(
88+
"`U02UD2WE3HA` and <@U02UD2WE3HA> and ```U02UD2WE3HA```"
89+
);
90+
});
91+
92+
test("does not format IDs inside code blocks with more than 3 backticks", () => {
93+
const input = "````\nU02UD2WE3HA\n````";
94+
expect(formatMessage(input)).toBe(input);
95+
});
96+
});
97+
98+
describe("truncation", () => {
99+
test("truncates text longer than 3000 characters", () => {
100+
const input = "a".repeat(4000);
101+
expect(formatMessage(input).length).toBe(3000);
102+
});
103+
});
104+
});

packages/slack/src/message.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ export function formatMessage(text: string): string {
3030
text = text.slice(0, maxLength);
3131
}
3232

33+
// Preserve code blocks and inline code from formatting
34+
const preserved: string[] = [];
35+
const placeholder = (i: number) => `\x00CODE${i}\x00`;
36+
text = text.replace(/```[\s\S]*?```|`[^`]+`/g, (match) => {
37+
preserved.push(match);
38+
return placeholder(preserved.length - 1);
39+
});
40+
3341
// Manual formatting fixes for Slack compatibility
3442
// Convert markdown links [text](url) to Slack format <url|text>
3543
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<$2|$1>");
@@ -39,15 +47,17 @@ export function formatMessage(text: string): string {
3947

4048
// Replace non-bracketed user IDs with Slack format <@user_id>
4149
// Only wrap when not already inside angle brackets
50+
// Require at least one digit to avoid matching pure-alpha words like WORKSPACE
4251
text = text.replace(
43-
/(?<!<)@(U|W)[A-Z0-9]{8,}(?!>)/g,
52+
/(?<!<)@(?:U|W)(?=[A-Z0-9]*\d)[A-Z0-9]{8,}(?!>)/g,
4453
(match) => `<${match}>`
4554
);
4655

4756
// Also handle bare user IDs that start with U or W (LLMs often omit @ and <>)
4857
// Ensure we don't match within a larger alphanumeric token and avoid already bracketed forms
58+
// Require at least one digit to avoid matching pure-alpha words like WORKSPACE
4959
text = text.replace(
50-
/(^|[^A-Z0-9<@])((?:U|W)[A-Z0-9]{8,})(?![A-Z0-9>])/g,
60+
/(^|[^A-Z0-9<@])((?:U|W)(?=[A-Z0-9]*\d)[A-Z0-9]{8,})(?![A-Z0-9>])/g,
5161
(m, prefix, id) => `${prefix}<@${id}>`
5262
);
5363

@@ -56,6 +66,11 @@ export function formatMessage(text: string): string {
5666
/^[UW][A-Z0-9]{8,}$/.test(u) ? m : `@${u}`
5767
);
5868

69+
// Restore preserved code blocks
70+
for (let i = 0; i < preserved.length; i++) {
71+
text = text.replace(placeholder(i), preserved[i] ?? "");
72+
}
73+
5974
return text;
6075
}
6176

0 commit comments

Comments
 (0)