From 8f7642e7edf6ff406372cae3a958bef033702282 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:26:33 -0700 Subject: [PATCH] test(mobile): pin the older-page fence against a cursor-re-cutting replay The snapshot arm of `if (applied.windowReplaced || frame.type === 'snapshot')` was unpinned: deleting it kept the suite green. It is load-bearing. A replay that merges cleanly can still carry a new `beforeOffset`, which the hook adopts via `replayStillStartsAtOldest`. The page already in flight was addressed with the old offset, so without the fence it lands and writes its own stale cursor back over the fresh one, leaving the next `loadEarlier` addressed from a byte offset that no longer describes the file. Row order alone stays correct, which is why the ordering-only reasoning missed this. Sole failure under the mutation. No source change. --- .../use-mobile-native-chat-session.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/mobile/src/session/use-mobile-native-chat-session.test.ts b/mobile/src/session/use-mobile-native-chat-session.test.ts index 710e370c0d3..844c311b827 100644 --- a/mobile/src/session/use-mobile-native-chat-session.test.ts +++ b/mobile/src/session/use-mobile-native-chat-session.test.ts @@ -333,6 +333,38 @@ describe('useMobileNativeChatSession', () => { expect(state?.hasMore).toBe(false) }) + it('fences an in-flight older page when a merging replay re-cuts the byte cursor', async () => { + let resolveEarlier: (response: unknown) => void = () => {} + const sendRequest = vi.fn( + () => new Promise((resolve) => (resolveEarlier = resolve)) + ) as unknown as RpcClient['sendRequest'] + const retained = Array.from({ length: 40 }, (_unused, index) => message(`win-${index}`)) + const subscribe: RpcClient['subscribe'] = vi.fn((_method, _params, onData) => { + emit = onData + onData({ type: 'snapshot', messages: retained, hasMore: true, beforeOffset: 100 }) + return () => {} + }) + await mount({ sendRequest, subscribe } as unknown as RpcClient) + act(() => state?.loadEarlier()) + + // The replay merges (same rows, no trim), but the host re-cut the file, so + // it carries a new cursor. The page already in flight was addressed with the + // old offset and would write that stale cursor back over the fresh one. + await act(async () => + emit({ type: 'snapshot', messages: retained, hasMore: true, beforeOffset: 250 }) + ) + await act(async () => { + resolveEarlier({ + ok: true, + result: { messages: [message('stale-page')], hasMore: true, beforeOffset: 40 } + }) + await Promise.resolve() + }) + + expect(state?.messages.some((entry) => entry.id === 'stale-page')).toBe(false) + expect(state?.loadingEarlier).toBe(false) + }) + it('keeps the base snapshot authoritative when a live append arrives first', async () => { const sendRequest = vi.fn() const subscribe: RpcClient['subscribe'] = vi.fn((_method, _params, onData) => {