Files
life-echo/app-expo/tests/features/conversation/message-split.test.ts
Kevin 3921c5ec24 fix(app-expo): allow read-aloud on other split segments while TTS paused
Match playback refs to the correct assistant segment so the interrupt overlay
does not block other bubbles, and preempt local playback when switching segments.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 14:55:10 +08:00

107 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
assistantSegmentMessageId,
lastSegmentPreview,
normalizeAssistantContentForSplit,
parseAssistantSplitListKey,
playbackListKeyMatchesBubble,
splitMessageParts,
splitStreamingSegments,
} from '@/features/conversation/message-split';
describe('message-split', () => {
it('splitMessageParts is case-insensitive on delimiter', () => {
expect(splitMessageParts('a [SPLIT] b')).toEqual(['a', 'b']);
expect(splitMessageParts('a [split] b')).toEqual(['a', 'b']);
expect(splitMessageParts('a [Split] b')).toEqual(['a', 'b']);
});
it('splitMessageParts handles spaces inside brackets and fullwidth brackets', () => {
expect(splitMessageParts('a [ SPLIT ] b')).toEqual(['a', 'b']);
expect(splitMessageParts('aSPLITb')).toEqual(['a', 'b']);
expect(splitMessageParts('a【SPLIT】b')).toEqual(['a', 'b']);
});
it('splitMessageParts trims and drops empty segments', () => {
expect(splitMessageParts(' x [SPLIT] y ')).toEqual(['x', 'y']);
expect(splitMessageParts('[SPLIT]only')).toEqual(['only']);
});
it('splitMessageParts splits multi-segment content as persisted / WS-joined', () => {
expect(splitMessageParts('第一段[SPLIT]第二段')).toEqual([
'第一段',
'第二段',
]);
});
it('splitMessageParts falls back to double-newline paragraphs (no [SPLIT] in DB)', () => {
const a = '太为你高兴了!在上海大剧院的舞台绽放,聚光灯下的你。';
const b =
'说到舞台,我忽然想起你黄浦江边的童年。从看着江水流淌,到在舞台上演绎别人的悲欢。';
expect(splitMessageParts(`${a}\n\n${b}`)).toEqual([a, b]);
});
it('splitStreamingSegments keeps empty tail after delimiter', () => {
/**
* 流式上下文(!isComplete下保留尾部空段让 UI 能在分隔符已出现、第二段尚未到字时
* 渲染「上一段已完成气泡 + 空流式气泡」。`StreamingBubbles` 在 isComplete=true 时
* 会过滤掉这只空尾段(见 conversation/[id].tsx 与对应注释),所以底部不会再永久挂一只
* 假装的「Replying…」气泡。
*/
expect(splitStreamingSegments('first [SPLIT]')).toEqual(['first', '']);
});
it('splitStreamingSegments handles lowercase / fullwidth split markers', () => {
expect(splitStreamingSegments('a [split] b')).toEqual(['a', 'b']);
expect(splitStreamingSegments('a【SPLIT】b')).toEqual(['a', 'b']);
expect(splitStreamingSegments('a [ SPLIT ] b')).toEqual(['a', 'b']);
});
it('splitMessageParts accepts spaced / lowercase delimiters', () => {
expect(splitMessageParts('first [ SPLIT ] second')).toEqual([
'first',
'second',
]);
expect(splitMessageParts('first [split] second')).toEqual([
'first',
'second',
]);
});
it('lastSegmentPreview uses last non-empty part', () => {
expect(lastSegmentPreview('a [SPLIT] b', 10)).toBe('b');
expect(lastSegmentPreview('hello', 3)).toBe('hel');
});
it('assistantSegmentMessageId matches WS / TTS segment binding', () => {
expect(assistantSegmentMessageId('uuid-a', 0)).toBe('uuid-a_seg_0');
expect(assistantSegmentMessageId('uuid-a', 1)).toBe('uuid-a_seg_1');
});
it('playbackListKeyMatchesBubble aligns seg playback with part listKey', () => {
const uuid = '78b32c06-d2f9-453b-9cc4-354e68fbcb2d';
expect(
playbackListKeyMatchesBubble(
`${uuid}_seg_1`,
`${uuid}_part_1`,
uuid,
),
).toBe(true);
expect(
playbackListKeyMatchesBubble(
`${uuid}_seg_0`,
`${uuid}_part_1`,
uuid,
),
).toBe(false);
expect(parseAssistantSplitListKey(`${uuid}_part_0`)).toEqual({
messageId: uuid,
segmentIndex: 0,
});
});
it('normalizeAssistantContentForSplit maps fullwidth brackets', () => {
expect(normalizeAssistantContentForSplit('x')).toBe('[x]');
expect(normalizeAssistantContentForSplit('【x】')).toBe('[x]');
});
});