From 93660e6f1d925a0400a495e3352e8d7944fc207a Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 24 Feb 2026 10:54:14 +0100 Subject: [PATCH] fix(slack): correct auto-formatting of slack ids --- packages/slack/src/message.test.ts | 104 +++++++++++++++++++++++++++++ packages/slack/src/message.ts | 19 +++++- 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 packages/slack/src/message.test.ts diff --git a/packages/slack/src/message.test.ts b/packages/slack/src/message.test.ts new file mode 100644 index 00000000..04ad6f6a --- /dev/null +++ b/packages/slack/src/message.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from "bun:test"; +import { formatMessage } from "./message"; + +describe("formatMessage", () => { + describe("markdown to Slack formatting", () => { + test("converts markdown links to Slack format", () => { + expect(formatMessage("[click here](https://example.com)")).toBe( + "" + ); + }); + + test("converts double-star bold to single-star bold", () => { + expect(formatMessage("**hello**")).toBe("*hello*"); + }); + }); + + describe("user ID mention wrapping", () => { + test("wraps @-prefixed Slack user IDs", () => { + expect(formatMessage("@U02UD2WE3HA")).toBe("<@U02UD2WE3HA>"); + }); + + test("wraps bare Slack user IDs", () => { + expect(formatMessage("user U02UD2WE3HA said")).toBe( + "user <@U02UD2WE3HA> said" + ); + }); + + test("wraps workspace IDs starting with W", () => { + expect(formatMessage("@W01AB2CD3EF")).toBe("<@W01AB2CD3EF>"); + }); + + test("does not double-wrap already bracketed IDs", () => { + expect(formatMessage("<@U02UD2WE3HA>")).toBe("<@U02UD2WE3HA>"); + }); + + test("removes brackets from non-ID @handles", () => { + expect(formatMessage("<@john.doe>")).toBe("@john.doe"); + }); + }); + + describe("false-positive prevention", () => { + test("does not match pure-alpha words like WORKSPACE", () => { + const input = "CODER_WORKSPACE_IS_PREBUILD_CLAIM=true"; + expect(formatMessage(input)).toBe(input); + }); + + test("does not match UPPERCASE without digits", () => { + expect(formatMessage("UNDEFINED")).toBe("UNDEFINED"); + expect(formatMessage("WORKSPACEID")).toBe("WORKSPACEID"); + }); + + test("does not match short IDs", () => { + expect(formatMessage("U1234")).toBe("U1234"); + }); + }); + + describe("code block preservation", () => { + test("does not format IDs inside inline code", () => { + const input = "`U02UD2WE3HA`"; + expect(formatMessage(input)).toBe(input); + }); + + test("does not format IDs inside code blocks", () => { + const input = "```\nU02UD2WE3HA\n```"; + expect(formatMessage(input)).toBe(input); + }); + + test("formats IDs outside code but preserves code content", () => { + const input = "user U02UD2WE3HA said `U02UD2WE3HA`"; + expect(formatMessage(input)).toBe( + "user <@U02UD2WE3HA> said `U02UD2WE3HA`" + ); + }); + + test("preserves markdown links inside code blocks", () => { + const input = "```\n[text](url)\n```"; + expect(formatMessage(input)).toBe(input); + }); + + test("preserves bold syntax inside inline code", () => { + const input = "`**not bold**`"; + expect(formatMessage(input)).toBe(input); + }); + + test("handles multiple code blocks", () => { + const input = "`U02UD2WE3HA` and U02UD2WE3HA and ```U02UD2WE3HA```"; + expect(formatMessage(input)).toBe( + "`U02UD2WE3HA` and <@U02UD2WE3HA> and ```U02UD2WE3HA```" + ); + }); + + test("does not format IDs inside code blocks with more than 3 backticks", () => { + const input = "````\nU02UD2WE3HA\n````"; + expect(formatMessage(input)).toBe(input); + }); + }); + + describe("truncation", () => { + test("truncates text longer than 3000 characters", () => { + const input = "a".repeat(4000); + expect(formatMessage(input).length).toBe(3000); + }); + }); +}); diff --git a/packages/slack/src/message.ts b/packages/slack/src/message.ts index 0a278d1a..19d8d0e1 100644 --- a/packages/slack/src/message.ts +++ b/packages/slack/src/message.ts @@ -30,6 +30,14 @@ export function formatMessage(text: string): string { text = text.slice(0, maxLength); } + // Preserve code blocks and inline code from formatting + const preserved: string[] = []; + const placeholder = (i: number) => `\x00CODE${i}\x00`; + text = text.replace(/```[\s\S]*?```|`[^`]+`/g, (match) => { + preserved.push(match); + return placeholder(preserved.length - 1); + }); + // Manual formatting fixes for Slack compatibility // Convert markdown links [text](url) to Slack format text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<$2|$1>"); @@ -39,15 +47,17 @@ export function formatMessage(text: string): string { // Replace non-bracketed user IDs with Slack format <@user_id> // Only wrap when not already inside angle brackets + // Require at least one digit to avoid matching pure-alpha words like WORKSPACE text = text.replace( - /(?)/g, + /(?)/g, (match) => `<${match}>` ); // Also handle bare user IDs that start with U or W (LLMs often omit @ and <>) // Ensure we don't match within a larger alphanumeric token and avoid already bracketed forms + // Require at least one digit to avoid matching pure-alpha words like WORKSPACE text = text.replace( - /(^|[^A-Z0-9<@])((?:U|W)[A-Z0-9]{8,})(?![A-Z0-9>])/g, + /(^|[^A-Z0-9<@])((?:U|W)(?=[A-Z0-9]*\d)[A-Z0-9]{8,})(?![A-Z0-9>])/g, (m, prefix, id) => `${prefix}<@${id}>` ); @@ -56,6 +66,11 @@ export function formatMessage(text: string): string { /^[UW][A-Z0-9]{8,}$/.test(u) ? m : `@${u}` ); + // Restore preserved code blocks + for (let i = 0; i < preserved.length; i++) { + text = text.replace(placeholder(i), preserved[i] ?? ""); + } + return text; }