From 58486bb58b95fb6210180a8f31ac50bdf80045ce Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:50:06 +1100 Subject: [PATCH] fix(server): PR watch wakes no longer split an emoji in a long comment Co-Authored-By: Claude Opus 5.5 --- .../orchestration-v2/pullRequestWatch.test.ts | 17 +++++++++++++++++ .../src/orchestration-v2/pullRequestWatch.ts | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts index 0b6070f364662..adf1e9776ce02 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts @@ -8,6 +8,7 @@ import { assert, describe, it } from "@effect/vitest"; import { PULL_REQUEST_WATCH_WAKE_LIMIT, evaluatePullRequestWatch as evaluate, + pullRequestWatchMessage, } from "./pullRequestWatch.ts"; // These cases read the head once or at one moment, so the no-checks notice never comes due. @@ -335,6 +336,22 @@ describe("evaluatePullRequestWatch", () => { assert.deepEqual(evaluatePullRequestWatch(report.next, detail(), comments).changes, []); }); + it("cuts a long remark in the wake message without splitting an emoji", () => { + const body = `${"x".repeat(196)}🚀 and more`; + const report = evaluatePullRequestWatch(watch(), detail(), [ + remark("reviewer", "2026-10-02T12:06:00Z", body), + ]); + const { text } = pullRequestWatchMessage({ + number: 1, + url: "https://github.com/o/r/pull/1", + baseBranch: "main", + headSha: null, + report, + }); + assert.isTrue(text.isWellFormed()); + assert.include(text, `"${"x".repeat(196)}..."`); + }); + it("reports a conflict once, until the branch is clean again", () => { const conflicting = detail({ mergeability: "conflicting" }); const first = evaluatePullRequestWatch(watch(), conflicting, noRemarks); diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.ts b/apps/server/src/orchestration-v2/pullRequestWatch.ts index 033125a6d6c9d..c4f227f0520ef 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.ts @@ -160,7 +160,11 @@ function snippet(body: string): string { .replaceAll(//g, " ") .replaceAll(/\s+/g, " ") .trim(); - return text.length <= SNIPPET_LENGTH ? text : `${text.slice(0, SNIPPET_LENGTH - 3)}...`; + if (text.length <= SNIPPET_LENGTH) return text; + // Half an emoji serializes to a lone surrogate escape, which provider JSON parsers reject. + const last = text.charCodeAt(SNIPPET_LENGTH - 4); + const end = last >= 0xd800 && last <= 0xdbff ? SNIPPET_LENGTH - 4 : SNIPPET_LENGTH - 3; + return `${text.slice(0, end)}...`; } function listed(items: ReadonlyArray, line: (item: T) => string): Array {