Files
paseo/packages/app/src/components/assistant-message-render-limit.test.ts
T
Mohamed Boudra 06fae60809 Prevent oversized assistant messages from crashing the timeline (#4166)
* fix(app): cap oversized assistant messages

A single assistant message can expand into enough rendered Markdown nodes to crash the renderer. Limit the Markdown input while preserving the complete timeline value and report its full byte size.

* fix(app): bound oversized stream processing

Cap the render input before paced reveal and defer full-message byte counting until completion. Reuse the existing grapheme boundary policy so capped text never ends in a partial glyph.

* fix(app): preserve capped text without segmentation

Hermes lacks Intl.Segmenter, while the shared reveal boundary intentionally returns zero without it. Keep that contract and fall back locally to a surrogate-safe 32K prefix.
2026-09-01 18:06:25 +02:00

55 lines
1.8 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import {
ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT,
capAssistantMessageForRender,
getUtf8ByteLength,
} from "./assistant-message-render-limit";
describe("assistant message render limit", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.resetModules();
});
it("leaves messages at the limit unchanged", () => {
const message = "a".repeat(ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT);
expect(capAssistantMessageForRender(message)).toEqual({ text: message, capped: false });
});
it("caps oversized messages before markdown rendering", () => {
const message = "a".repeat(ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT + 1);
expect(capAssistantMessageForRender(message)).toEqual({
text: "a".repeat(ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT),
capped: true,
});
});
it("does not split a grapheme cluster at the boundary", () => {
const prefix = "a".repeat(ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT - 1);
expect(capAssistantMessageForRender(`${prefix}étail`)).toEqual({
text: prefix,
capped: true,
});
});
it("counts the complete message size in UTF-8 bytes", () => {
expect(getUtf8ByteLength("aé😀")).toBe(7);
});
it("keeps a bounded prefix when grapheme segmentation is unavailable", async () => {
vi.resetModules();
vi.stubGlobal("Intl", { ...Intl, Segmenter: undefined });
const { capAssistantMessageForRender: capWithoutSegmenter } =
await import("./assistant-message-render-limit");
const message = "a".repeat(ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT + 1);
expect(capWithoutSegmenter(message)).toEqual({
text: "a".repeat(ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT),
capped: true,
});
});
});