fix(discord): reserve closing-fence space on fence-closing lines (#95661)

`chunkDiscordText` reserved closing-fence space from the post-line fence state
(`nextOpenFence`), but a flush during a line's segment loop appends the closing
fence based on the still-open `openFence`, which is only advanced after the
line. On a line that closes a fence yet carries trailing text, `reserveChars`
was 0 while `flush()` still appended a `` ``` ``, producing a chunk of
`maxChars + 4` (e.g. 2004 > 2000) that Discord rejects with HTTP 400.

Reserve against `nextOpenFence ?? openFence` so whichever fence a flush can
close is accounted for, keeping a fence-closing line's chunk within `maxChars`.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ly-wang19
2026-06-23 09:28:14 +08:00
committed by GitHub
parent 1658fb6c14
commit d84a8b1506
2 changed files with 17 additions and 2 deletions
+11
View File
@@ -78,6 +78,17 @@ describe("chunkDiscordText", () => {
}
});
it("keeps chunks within maxChars when a closing fence line carries trailing text", () => {
// A line that both closes the fence and carries a long tail must still reserve closing-fence
// space; otherwise a mid-line flush appended "```" and overflowed maxChars (e.g. 2004 > 2000).
for (let pad = 1990; pad <= 2000; pad++) {
const text = "hi\n```lang\n```" + "z".repeat(pad);
for (const chunk of chunkDiscordText(text, { maxChars: 2000, maxLines: 100 })) {
expect(chunk.length).toBeLessThanOrEqual(2000);
}
}
});
it("preserves whitespace when splitting long lines", () => {
const text = Array.from({ length: 40 }, () => "word").join(" ");
const chunks = chunkDiscordText(text, { maxChars: 20, maxLines: 50 });
+6 -2
View File
@@ -207,8 +207,12 @@ export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}):
}
}
const reserveChars = nextOpenFence ? closeFenceLine(nextOpenFence).length + 1 : 0;
const reserveLines = nextOpenFence ? 1 : 0;
// A flush can fire mid-line, before `openFence` advances to `nextOpenFence` below, so it closes
// against the still-open `openFence`. A fence-closing line that also carries trailing text would
// otherwise reserve 0 yet still get a closing fence appended on flush, overflowing maxChars.
const fenceToReserve = nextOpenFence ?? openFence;
const reserveChars = fenceToReserve ? closeFenceLine(fenceToReserve).length + 1 : 0;
const reserveLines = fenceToReserve ? 1 : 0;
const effectiveMaxChars = maxChars - reserveChars;
const effectiveMaxLines = maxLines - reserveLines;
const charLimit = effectiveMaxChars > 0 ? effectiveMaxChars : maxChars;