-
-
Notifications
You must be signed in to change notification settings - Fork 268
Expand file tree
/
Copy pathToolAccordionStreaming.test.tsx
More file actions
86 lines (76 loc) · 3.66 KB
/
Copy pathToolAccordionStreaming.test.tsx
File metadata and controls
86 lines (76 loc) · 3.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* BUG #37 regression — the tool-call accordion must be tappable and stay open while a
* sibling message is ACTIVELY streaming (the whole chat subtree re-renders every token).
*
* cf40369b made the expanded flag survive the streaming→finalized remount (accordionStore).
* The REMAINING bug: during ACTIVE streaming the tool-result row re-rendered every token
* and its onPress was a fresh closure each render, so the TouchableOpacity press target
* churned mid-gesture and a tap landing during streaming was dropped — the accordion
* never opened until generation finished.
*
* Fix: `useAccordionExpanded` returns a referentially STABLE toggle, and the accordion
* rows (ToolResultBubble / ToolsSentCollapsible) are memoized so token churn on a
* streaming sibling can't re-render them.
*
* Two guards below:
* 1. `useAccordionExpanded` returns the SAME toggle instance across re-renders
* (deterministic fails-before / passes-after — a fresh `() => toggle(key)` per render
* failed this).
* 2. The memoized row does NOT re-render when a streaming sibling churns, and the
* accordion opens on tap and stays open across the churn.
*/
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react-native';
import { renderHook } from '@testing-library/react-native';
import { ChatMessage } from '../../../src/components/ChatMessage';
import { createToolResultMessage } from '../../utils/factories';
import { useAccordionStore, useAccordionExpanded } from '../../../src/stores/accordionStore';
jest.mock('../../../src/utils/messageContent', () => ({
...jest.requireActual('../../../src/utils/messageContent'),
stripControlTokens: (content: string) => content,
}));
describe('BUG #37 — tool accordion is tappable during active streaming', () => {
beforeEach(() => {
useAccordionStore.setState({ expanded: {} });
});
it('useAccordionExpanded returns a referentially stable toggle across re-renders', () => {
const { result, rerender } = renderHook(() => useAccordionExpanded('tool-result:call-abc'));
const firstToggle = result.current[1];
// Re-render several times (== token churn re-rendering the accordion's owner).
rerender({});
rerender({});
rerender({});
const laterToggle = result.current[1];
// A stable handler keeps the TouchableOpacity press target intact across churn.
expect(laterToggle).toBe(firstToggle);
});
it('opens on tap and stays open while a sibling streams (rapid re-renders)', () => {
const toolMessage = createToolResultMessage(
'web_search',
'Detailed search results the accordion reveals when expanded.',
{ id: 'tool-1', toolCallId: 'call-xyz' },
);
const StreamingHost: React.FC<{ tick: number }> = ({ tick }) => (
<>
<ChatMessage message={toolMessage} isStreaming={false} />
<ChatMessage
message={{ id: 'streaming', role: 'assistant', content: 'x'.repeat(tick), timestamp: 0, isStreaming: true }}
isStreaming
/>
</>
);
const { getByTestId, queryByText, rerender } = render(<StreamingHost tick={0} />);
for (let t = 1; t <= 5; t++) {
act(() => rerender(<StreamingHost tick={t} />));
}
expect(queryByText('Detailed search results the accordion reveals when expanded.')).toBeNull();
act(() => {
fireEvent.press(getByTestId('tool-result-label-web_search'));
});
for (let t = 6; t <= 12; t++) {
act(() => rerender(<StreamingHost tick={t} />));
}
expect(queryByText('Detailed search results the accordion reveals when expanded.')).not.toBeNull();
expect(useAccordionStore.getState().expanded['tool-result:call-xyz']).toBe(true);
});
});