From aa82e90aec06509c0b837eacc1b2096edf855518 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:33:54 +0000 Subject: [PATCH] fix(mrkdwn): convert Slack angle-bracket links to Markdown for markdown block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The body is delivered as a Slack `markdown` block, which expects standard Markdown links ([label](url)). Using Slack mrkdwn angle-bracket syntax () collides with bold markers and produces malformed output like ** where the closing ** is absorbed inside the angle brackets and Slack parses https://url** as the URL. Normalize in three passes: 1. Repair malformed bold-wrapped angle autolinks: ** → [URL](URL) 2. Convert Slack labeled links: → [label](URL) 3. Convert remaining angle autolinks: → [URL](URL) Refs: https://sentry.slack.com/archives/C03USURCFBJ/p1780496988280239 Co-authored-by: David Cramer --- packages/junior/src/chat/slack/mrkdwn.ts | 19 ++++++++ .../junior/tests/unit/misc/output.test.ts | 46 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/junior/src/chat/slack/mrkdwn.ts b/packages/junior/src/chat/slack/mrkdwn.ts index be155b4d1..85af040df 100644 --- a/packages/junior/src/chat/slack/mrkdwn.ts +++ b/packages/junior/src/chat/slack/mrkdwn.ts @@ -58,6 +58,25 @@ export function ensureBlockSpacing(text: string): string { export function renderSlackMrkdwn(text: string): string { let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, ""); normalized = ensureBlockSpacing(normalized); + // The body is sent as a Slack `markdown` block, which expects standard + // Markdown links ([label](url)), not Slack mrkdwn angle-bracket syntax + // ( / ). Convert to explicit Markdown links so the block + // renders them correctly and bold markers (**) can never collide with the + // angle brackets and corrupt the URL. + // + // Pass 1: repair malformed bold-wrapped angle autolinks produced by the + // model: ** → [https://url](https://url) + normalized = normalized.replace( + /\*\*<(https?:\/\/[^*>\s]+)\*\*>/g, + "[$1]($1)", + ); + // Pass 2: Slack labeled links → Markdown: → [label](https://url) + normalized = normalized.replace( + /<(https?:\/\/[^|>\s]+)\|([^>]+)>/g, + "[$2]($1)", + ); + // Pass 3: remaining angle autolinks → explicit Markdown links: → [https://url](https://url) + normalized = normalized.replace(/<(https?:\/\/[^>\s]+)>/g, "[$1]($1)"); return normalized.replace(/\n{3,}/g, "\n\n").trim(); } diff --git a/packages/junior/tests/unit/misc/output.test.ts b/packages/junior/tests/unit/misc/output.test.ts index 507aacd8e..221ed2025 100644 --- a/packages/junior/tests/unit/misc/output.test.ts +++ b/packages/junior/tests/unit/misc/output.test.ts @@ -15,6 +15,52 @@ describe("renderSlackMrkdwn", () => { "one\n\n- item a\n- item b\n\ntwo", ); }); + + it("converts Slack angle-bracket URL to explicit Markdown link", () => { + expect( + renderSlackMrkdwn("See for details."), + ).toBe("See [https://example.com](https://example.com) for details."); + }); + + it("converts Slack labeled links to Markdown format", () => { + expect( + renderSlackMrkdwn(""), + ).toBe("[PR #1](https://github.com/org/repo/pull/1)"); + }); + + it("resolves bold+angle-bracket collision that broke GitHub PR links", () => { + // The model generated ** — closing ** absorbed inside the + // angle brackets. Pass 1 catches this and converts to [url](url). + expect( + renderSlackMrkdwn( + "**", + ), + ).toBe( + "[https://github.com/getsentry/sentry/pull/116765](https://github.com/getsentry/sentry/pull/116765)", + ); + }); + + it("converts multiple angle-bracket URLs in one message", () => { + expect( + renderSlackMrkdwn( + "PR1: and PR2: ", + ), + ).toBe( + "PR1: [https://github.com/org/repo/pull/1](https://github.com/org/repo/pull/1) and PR2: [https://github.com/org/repo/pull/2](https://github.com/org/repo/pull/2)", + ); + }); + + it("leaves Markdown links unchanged", () => { + expect( + renderSlackMrkdwn("**[PR #123](https://github.com/org/repo/pull/123)**"), + ).toBe("**[PR #123](https://github.com/org/repo/pull/123)**"); + }); + + it("leaves plain bold text unchanged", () => { + expect(renderSlackMrkdwn("**Root cause** is here.")).toBe( + "**Root cause** is here.", + ); + }); }); describe("buildSlackOutputMessage", () => {