Skip to content
Open
41 changes: 41 additions & 0 deletions apps/server/src/orchestration-v2/pullRequestWatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,47 @@ describe("evaluatePullRequestWatch", () => {
assert.include(text, `"${"x".repeat(196)}..."`);
});

it("quotes a remark's text instead of its leading HTML, and keeps tags in prose and code", () => {
const badge =
'<a href="https://app.coderabbit.ai/change-stack/o/r/pull/1?cs_source=review_comment"><picture><source media="(prefers-color-scheme: dark)" srcset="https://coderabbit.ai/badge-dark.svg"><img alt="Severity > `Minor`" src="https://coderabbit.ai/badge.svg"></picture></a>';
const report = evaluatePullRequestWatch(watch(), detail(), [
remark(
"coderabbitai",
"2026-10-02T12:06:00Z",
`${badge}\n\n_⚠️ Potential issue_ | _🟠 Major_\n\n**Guard the empty list.**`,
),
remark(
"reviewer",
"2026-10-02T12:07:00Z",
"Return Promise<void>, not Promise<A> or Promise<a>.",
),
remark(
"designer",
"2026-10-02T12:08:00Z",
"Replace `<div>` with ``<span>``:\n~~~html\n<p>Done</p>\n~~~",
),
remark("maintainer", "2026-10-02T12:09:00Z", "<p>Guard this</p>, and `<!--` stays escaped."),
remark(
"summary-bot",
"2026-10-02T12:10:00Z",
`<!-- ${"x".repeat(3_000)} -->\nCheck the retry.\n <!-- GitHub hides this unclosed block`,
),
]);
const { text } = pullRequestWatchMessage({
number: 1,
url: "https://github.com/o/r/pull/1",
baseBranch: "main",
headSha: null,
report,
});
assert.include(text, `"_⚠️ Potential issue_ | _🟠 Major_ **Guard the empty list.**"`);
assert.include(text, `"Return Promise<void>, not Promise<A> or Promise<a>."`);
assert.include(text, '"Replace `<div>` with ``<span>``: ~~~html <p>Done</p> ~~~"');
assert.include(text, 'maintainer on src/index.ts: "Guard this , and `<!--` stays escaped."');
assert.include(text, '"Check the retry."');
assert.notInclude(text, "<picture");
});

it("reports a conflict once, until the branch is clean again", () => {
const conflicting = detail({ mergeability: "conflicting" });
const first = evaluatePullRequestWatch(watch(), conflicting, noRemarks);
Expand Down
33 changes: 31 additions & 2 deletions apps/server/src/orchestration-v2/pullRequestWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ export const PULL_REQUEST_WATCH_WAKE_LIMIT = 10;
export const PULL_REQUEST_WATCH_NO_CHECKS_MS = 15 * 60_000;
const LISTED_ITEMS = 10;
const SNIPPET_LENGTH = 200;
// Remark bodies are untrusted and run to 64 KB, and unclosed openers make the markup pass
// backtrack, so it only reads far enough to fill an excerpt once HTML comments are gone.
const SNIPPET_SCAN_LENGTH = 2_000;
// One left-to-right pass keeps code spans' tags and reads attribute backticks as part of the tag.
// A lookahead pins each delimiter run at full length, so a long run can't backtrack cubically.
// Element names are lowercase. A one-letter element needs attributes or a later closing tag,
// so Promise<a> stays.
const MARKUP =
/(?<code>(?<!`)(?=(?<ticks>`+))\k<ticks>[\s\S]*?(?<!`)\k<ticks>(?!`)|(?<!~)(?=(?<tildes>~{3,}))\k<tildes>[\s\S]*?\k<tildes>)|<(?<one>[abip])>(?=[\s\S]*?<\/\k<one>>)|<\/(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)\s*>|<(?:blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|img|li|ol|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>|<(?:a|b|i|p)\s(?:[^>"']|"[^"]*"|'[^']*')*>/g;

export type PullRequestWatchChange =
| { readonly kind: "checks-failed"; readonly failed: ReadonlyArray<PullRequestCheck> }
Expand Down Expand Up @@ -158,9 +167,29 @@ export function evaluatePullRequestWatch(
};
}

// indexOf keeps this linear on unclosed openers, so it can read the whole body.
function withoutHtmlComments(body: string): string {
let text = "";
let from = 0;
for (let open = body.indexOf("<!--"); open !== -1; open = body.indexOf("<!--", from)) {
const close = body.indexOf("-->", open + 4);
if (close === -1) {
// Like GitHub: an unclosed opener starting a line hides the rest, but one inside a paragraph is text.
const lineStart = body.lastIndexOf("\n", open - 1) + 1;
const hidesRest = /^ {0,3}$/.test(body.slice(lineStart, open));
return text + body.slice(from, hidesRest ? open : undefined);
}
text += `${body.slice(from, open)} `;
from = close + 3;
}
return text + body.slice(from);
}

function snippet(body: string): string {
const text = body
.replaceAll(/<!--[\s\S]*?-->/g, " ")
const text = withoutHtmlComments(body)
.slice(0, SNIPPET_SCAN_LENGTH)
Comment thread
onlydole marked this conversation as resolved.
Resolved
.toWellFormed()
.replaceAll(MARKUP, (match, code: string | undefined) => (code === undefined ? " " : match))
.replaceAll(/\s+/g, " ")
.trim();
if (text.length <= SNIPPET_LENGTH) return text;
Expand Down
Loading