From c502643bc10c11857bbf8670c5ac6c93c560a59e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:53:55 -0700 Subject: [PATCH] test(mobile): pin replay paging-metadata and base-snapshot authority Two more branches of the replay logic were unpinned. Adopting a replay's `beforeOffset` when it starts partway into paged-in history would make the next loadEarlier re-fetch on-screen rows and prepend duplicates; treating a post-replacement snapshot as a replay would retain a row the authoritative window dropped. Both mutants now fail exactly one test. --- .../mobile-native-chat-stream-frame.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/mobile/src/session/mobile-native-chat-stream-frame.test.ts b/mobile/src/session/mobile-native-chat-stream-frame.test.ts index ef94c83a7f1..4fa0b924b43 100644 --- a/mobile/src/session/mobile-native-chat-stream-frame.test.ts +++ b/mobile/src/session/mobile-native-chat-stream-frame.test.ts @@ -103,6 +103,27 @@ describe('applyMobileNativeChatStreamFrame', () => { }) }) + it('ignores replay paging metadata that describes a row other than our oldest', () => { + const merger = createNativeChatMerger() + replaceList(merger, ['p1', 'p2', 'a', 'b'].map(message)) + + // `beforeOffset` here points before 'a', not before 'p1'. Adopting it would + // make the next loadEarlier re-fetch 'p1'/'p2' and prepend them twice. + expect( + applyMobileNativeChatStreamFrame({ + merger, + frame: { + type: 'snapshot', + messages: [message('a'), message('b')], + hasMore: true, + beforeOffset: 500 + }, + limit: 100, + replaceSnapshot: false + }) + ).toEqual({ kind: 'messages', messages: ['p1', 'p2', 'a', 'b'].map(message) }) + }) + it('replaces the window when a reconnect replay is disjoint from history', () => { const merger = createNativeChatMerger() replaceList(merger, [message('old-1'), message('old-2')]) @@ -249,6 +270,33 @@ describe('applyMobileNativeChatStreamFrame', () => { }) }) + it('still treats the base snapshot as authoritative after a replacement frame', () => { + const merger = createNativeChatMerger() + replaceList(merger, ['a', 'b'].map(message)) + + // A replacement is not this subscription's base snapshot, so the first real + // snapshot still replaces — merging would keep 'a', which it does not carry. + expect( + applyMobileNativeChatStreamFrame({ + merger, + frame: { + type: 'snapshot', + messages: [message('b'), message('c')], + hasMore: true, + beforeOffset: 77 + }, + limit: 100, + replaceSnapshot: true + }) + ).toEqual({ + kind: 'messages', + messages: [message('b'), message('c')], + hasMore: true, + beforeOffset: 77, + windowReplaced: true + }) + }) + it('surfaces snapshot errors and ignores unrelated frames', () => { const merger = createNativeChatMerger() expect(