@@ -210,4 +210,58 @@ public async Task WhenApiReturnsEvents_CallsUpdateExactlyOnce()
210210
211211 await _repository . Received ( 1 ) . UpdateLastEventIdAsync ( Arg . Any < long > ( ) , Arg . Any < CancellationToken > ( ) ) ;
212212 }
213+
214+ [ Fact ( Timeout = 5000 ) ]
215+ public async Task WhenApiReturnsOutOfOrderEvents_SortsDefensivelyAndAdvancesCorrectly ( )
216+ {
217+ var cts = new CancellationTokenSource ( ) ;
218+ // Deliberately out of order: 10 first, then 5
219+ var events = new List < ScanEvent > { MakeScanEvent ( 10 ) , MakeScanEvent ( 5 ) } ;
220+
221+ _ = _repository . GetLastEventIdAsync ( Arg . Any < CancellationToken > ( ) ) . Returns ( 0L ) ;
222+ _ = _apiClient . GetScanEventsAsync ( Arg . Any < long > ( ) , Arg . Any < int > ( ) , Arg . Any < CancellationToken > ( ) )
223+ . Returns ( Result < IReadOnlyList < ScanEvent > > . Success ( events ) ) ;
224+ _ = _repository . UpdateLastEventIdAsync ( Arg . Any < long > ( ) , Arg . Any < CancellationToken > ( ) )
225+ . Returns ( callInfo =>
226+ {
227+ cts . Cancel ( ) ;
228+ return Task . CompletedTask ;
229+ } ) ;
230+
231+ ApiPollerWorker worker = CreateWorker ( ) ;
232+ await worker . StartAsync ( cts . Token ) ;
233+ await worker . ExecuteTask ! ;
234+ await worker . StopAsync ( CancellationToken . None ) ;
235+
236+ // After sort [5, 10], events[^1] is EventId=10 — the correct max
237+ await _repository . Received ( 1 ) . UpdateLastEventIdAsync ( 10L , Arg . Any < CancellationToken > ( ) ) ;
238+ }
239+
240+ [ Fact ( Timeout = 5000 ) ]
241+ public async Task WhenApiReturnsStaleEvents_ContinuesProcessingNormally ( )
242+ {
243+ var cts = new CancellationTokenSource ( ) ;
244+ // Both events are older than lastEventId=20
245+ var events = new List < ScanEvent > { MakeScanEvent ( 5 ) , MakeScanEvent ( 10 ) } ;
246+
247+ _ = _repository . GetLastEventIdAsync ( Arg . Any < CancellationToken > ( ) ) . Returns ( 20L ) ;
248+ _ = _apiClient . GetScanEventsAsync ( Arg . Any < long > ( ) , Arg . Any < int > ( ) , Arg . Any < CancellationToken > ( ) )
249+ . Returns ( Result < IReadOnlyList < ScanEvent > > . Success ( events ) ) ;
250+ _ = _repository . UpdateLastEventIdAsync ( Arg . Any < long > ( ) , Arg . Any < CancellationToken > ( ) )
251+ . Returns ( callInfo =>
252+ {
253+ cts . Cancel ( ) ;
254+ return Task . CompletedTask ;
255+ } ) ;
256+
257+ ApiPollerWorker worker = CreateWorker ( ) ;
258+ await worker . StartAsync ( cts . Token ) ;
259+ await worker . ExecuteTask ! ;
260+ await worker . StopAsync ( CancellationToken . None ) ;
261+
262+ // Stale events are processed normally — idempotent MERGE handles dedup.
263+ // lastEventId must not regress: advance marker stays at 20, not events[^1]=10.
264+ await _repository . Received ( 1 ) . UpdateLastEventIdAsync ( 20L , Arg . Any < CancellationToken > ( ) ) ;
265+ await _queue . Received ( 2 ) . SendAsync ( Arg . Any < ScanEvent > ( ) , Arg . Any < CancellationToken > ( ) ) ;
266+ }
213267}
0 commit comments