test(mobile): lock the history-position hold's release path

The tail-follow positive control never drove a paging cycle, so a hold that
latched on — chat permanently unable to follow the agent, the worst failure
mode of this design — passed the whole suite. Drive the real
loadingEarlier -> prepend -> settled sequence before the append.

Also record why onContentSizeChange reads the render snapshot instead of the
ref: effects flush before native layout, so the ref is already cleared there.
This commit is contained in:
Brennan Benson
2026-08-04 02:23:31 -07:00
parent 7bd313a1fd
commit 19a28542d8
2 changed files with 8 additions and 1 deletions
@@ -222,7 +222,12 @@ describe('MobileNativeChatView', () => {
it('still follows the tail once the history page has settled', async () => {
vi.useFakeTimers()
try {
await render({ messages: [older, message], hasMore: false })
await render({ messages: [message], hasMore: true })
await act(async () => vi.runAllTimersAsync())
// Drive a real paging cycle first: a hold that never releases would leave
// the chat permanently unable to follow the agent.
await update({ messages: [message], hasMore: true, loadingEarlier: true })
await update({ messages: [older, message], hasMore: false, loadingEarlier: false })
await act(async () => vi.runAllTimersAsync())
scrollToEnd.mockClear()
@@ -154,6 +154,8 @@ export function MobileNativeChatView({
// A short transcript leaves the reader both at the top and `atBottom`, so the
// tail-follow paths would scroll away the history that just paged in.
const keepHistoryPositionRef = useRef(Boolean(loadingEarlier))
// Snapshot, not a live ref read: effects flush before native layout, so by the
// time `onContentSizeChange` runs the ref is already cleared.
const keepHistoryPosition = keepHistoryPositionRef.current
const { fontScale, pinchGesture } = useMobileNativeChatPinchGesture()
useEffect(