ankurah-virtual-scroll 0.7.7

Platform-agnostic virtual scroll state machine with pagination for Ankurah
Documentation
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
//! Advanced ScrollManager tests
//!
//! Tests for:
//! - 1.5 Rapid scroll stress test
//! - 1.6 Intersection anchoring
//! - 1.7 Selection predicates
//! - 1.8 Live mode behavior
//! - 1.11 Concurrent operations

mod common;

use common::*;
use std::sync::Arc;

// ============================================================================
// 1.5 Rapid Scroll Stress Test
// ============================================================================

/// Test rapid alternating scrolls without triggering pagination.
/// Verifies no panics or inconsistent state during rapid direction changes.
///
/// Note: Mode transitions (Live ↔ Backward) DO trigger renders to update should_auto_scroll.
/// This test verifies pagination is NOT triggered, but mode-change renders are expected.
#[tokio::test]
async fn test_rapid_alternating_scrolls() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    // Initial: 30 items, offset=1000, Live mode
    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);
    assert_eq!(r.scroll_offset, 1000);

    // First scroll up exits Live mode - expect mode-change render
    // offset 1000→900, visible indices 18-27 → ts 1048-1057, items_below=2 → exits Live
    r.scroll_up_and_expect(
        100, 30, 1030..=1059, None,  // same items, no pagination
        true, false, false,          // has_more_preceding, has_more_following, should_auto_scroll=false
        1048, 1057, 900,             // visible range and offset
        None,                        // selection unchanged
    ).await?;
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Backward);

    // Now do rapid alternating scrolls in Backward mode
    // Stay away from both edges to avoid triggering mode changes or pagination
    // At offset 900, scroll within 800-900 range
    for _ in 0..10 {
        r.up_no_render(50, 1047, 1056).await;  // offset 850
        r.down_no_render(50, 1048, 1057).await; // offset 900
    }

    // Verify still in Backward mode (not re-entered Live)
    assert_eq!(r.scroll_offset, 900);
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Backward);

    // Verify items are still ordered correctly
    let (first, last, _, _) = r.visible_range();
    assert!(first < last, "Items should be ordered: first={} last={}", first, last);

    Ok(())
}

/// Test multiple scroll events that trigger pagination.
/// Verifies correct state after rapid pagination triggers.
#[tokio::test]
async fn test_rapid_pagination_triggers() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    // Initial state
    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);

    // Scroll up (400px): offset 600, items_above=12 > screen(10), exits Live mode but no pagination
    r.scroll_up_and_expect(
        400, 30, 1030..=1059, None,
        true, false, false, 1042, 1051, 600,
        None,
    ).await?;
    // Scroll up (100px): offset 500, items_above=10 = screen, triggers pagination
    let vs = r.scroll_up_and_expect(
        100, 50, 1010..=1059, Some(1049),
        true, true, false, 1040, 1049, 1500,
        Some("TRUE AND \"timestamp\" <= 1059 ORDER BY timestamp DESC LIMIT 51"),
    ).await?;
    assert!(!vs.should_auto_scroll, "should_auto_scroll should be false after exiting Live mode");

    // Continue scrolling - debounce prevents repeated pagination triggers
    // With 50 items already loaded (1010-1059), scrolling within range doesn't trigger new queries
    // After pagination: offset 1500, visible 1040-1049
    // up(100): offset 1400, visible [1400, 1900) → indices 28-37 → ts 1038-1047
    r.up_no_render(100, 1038, 1047).await;
    // up(400): offset 1000, visible [1000, 1500) → indices 20-29 → ts 1030-1039
    r.up_no_render(400, 1030, 1039).await;
    // up(100): offset 900, visible [900, 1400) → indices 18-27 → ts 1028-1037
    r.up_no_render(100, 1028, 1037).await;

    // Verify mode and item ordering
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Backward);
    let (first, last, _, _) = r.visible_range();
    assert!(first < last, "Items should be ordered");

    Ok(())
}

// ============================================================================
// 1.6 Intersection Anchoring Tests
// ============================================================================

/// Test that intersection item exists in both old and new windows.
/// Backward pagination: intersection at newest visible (bottom of viewport).
#[tokio::test]
async fn test_intersection_anchoring_backward() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);

    // Scroll up - first exits Live mode (mode-change render)
    // offset 1000→600, visible indices 12-21 → ts 1042-1051, items_below=8 → exits Live
    r.scroll_up_and_expect(
        400, 30, 1030..=1059, None,  // same items, no pagination yet
        true, false, false,          // mode changed, should_auto_scroll=false
        1042, 1051, 600,
        None,
    ).await?;

    // Before pagination: visible range is 1042-1051
    // Intersection should be at newest visible (1049) for backward
    // After: 50 items (1010-1059), anchor at index 39 (ts 1049), offset = 2000 - 500 = 1500
    let vs = r.scroll_up_and_expect(
        100, 50, 1010..=1059, Some(1049),
        true, true, false, 1040, 1049, 1500,
        Some("TRUE AND \"timestamp\" <= 1059 ORDER BY timestamp DESC LIMIT 51"),
    ).await?;

    // Verify intersection exists in new window
    let intersection = vs.intersection.as_ref().expect("Should have intersection");
    let ts = timestamps(&vs);
    let intersection_ts = ts[intersection.index];

    // Intersection item should be in the range 1020..=1059
    assert!(intersection_ts >= 1020 && intersection_ts <= 1059,
        "Intersection {} should be in new window", intersection_ts);

    // For backward, intersection is anchored at viewport bottom
    assert_eq!(intersection.direction, ankurah_virtual_scroll::LoadDirection::Backward);

    Ok(())
}

/// Test forward pagination: intersection at oldest visible (top of viewport).
#[tokio::test]
async fn test_intersection_anchoring_forward() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    // Initial render: 30 items, Live mode
    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);

    // Scroll backward to trigger pagination (deterministic - collect all renders)
    let renders = r.scroll_up_collect(500).await;
    assert!(!renders.is_empty(), "scroll_up should produce at least one render");
    let last = renders.last().unwrap();
    // After backward pagination, should have 50 items
    assert_eq!(last.items.len(), 50, "should have 50 items after backward pagination");
    assert!(!last.should_auto_scroll, "should_auto_scroll should be false");

    // Scroll to oldest edge (collect all, pagination might trigger)
    let _ = r.scroll_up_collect(500).await;
    let _ = r.scroll_up_collect(500).await;
    let _ = r.scroll_up_collect(500).await;

    // Now scroll forward toward newest
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;

    // Trigger forward pagination at newest edge
    let _renders = r.scroll_down_collect(500).await;

    // Verify we're still in Backward mode (haven't necessarily triggered forward pagination
    // or re-entered Live mode - depends on exact scroll positions)
    // The key assertion is that the test ran deterministically without timing issues
    let final_mode = sm.mode();
    assert!(
        final_mode == ankurah_virtual_scroll::ScrollMode::Backward
            || final_mode == ankurah_virtual_scroll::ScrollMode::Forward
            || final_mode == ankurah_virtual_scroll::ScrollMode::Live,
        "should be in a valid mode: {:?}",
        final_mode
    );

    Ok(())
}

// ============================================================================
// 1.7 Selection Predicate Tests
// ============================================================================

/// Test that selection predicates are correctly formed.
#[tokio::test]
async fn test_selection_predicates() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    // Initial: Live mode selection
    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);

    // Live mode: ORDER BY DESC LIMIT live_window
    let selection = sm.current_selection();
    assert!(selection.contains("ORDER BY timestamp DESC"),
        "Live mode should order DESC: {}", selection);
    assert!(selection.contains("LIMIT 30"),
        "Live mode should limit to live_window: {}", selection);

    // Trigger backward pagination - first scroll exits Live mode
    r.scroll_up_and_expect(
        400, 30, 1030..=1059, None,  // same items, no pagination yet
        true, false, false,          // mode changed, should_auto_scroll=false
        1042, 1051, 600,
        None,
    ).await?;
    r.scroll_up_and_expect(
        100, 50, 1010..=1059, Some(1049),
        true, true, false, 1040, 1049, 1500,
        Some("TRUE AND \"timestamp\" <= 1059 ORDER BY timestamp DESC LIMIT 51"),
    ).await?;

    // Backward: timestamp <= cursor ORDER BY DESC
    let selection = sm.current_selection();
    assert!(selection.contains("\"timestamp\" <= 1059"),
        "Backward should have cursor constraint: {}", selection);
    assert!(selection.contains("ORDER BY timestamp DESC"),
        "Backward should order DESC: {}", selection);
    assert!(selection.contains("LIMIT 51"),
        "Backward limit should be full_window+1: {}", selection);

    Ok(())
}

/// Test forward selection predicate at oldest edge.
/// Uses deterministic scroll_collect helpers to handle variable render timing.
#[tokio::test]
async fn test_selection_predicate_forward() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Live);

    // Navigate to oldest edge using deterministic helpers
    let _ = r.scroll_up_collect(500).await;
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Backward);

    // Continue scrolling toward oldest
    let _ = r.scroll_up_collect(500).await;
    let _ = r.scroll_up_collect(500).await;
    let _ = r.scroll_up_collect(500).await;

    // Should have paginated backward to reach oldest items
    let selection = sm.current_selection();
    assert!(selection.contains("ORDER BY timestamp DESC"),
        "Backward should order DESC: {}", selection);

    // Now scroll toward newest to trigger forward pagination
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;

    // Once we've scrolled far enough toward newest, mode should change to Forward
    // and selection should reflect forward pagination
    let final_selection = sm.current_selection();

    // Either we're in Forward mode with ASC ordering, or still paginating backward
    let mode = sm.mode();
    if mode == ankurah_virtual_scroll::ScrollMode::Forward {
        assert!(final_selection.contains("ORDER BY timestamp ASC"),
            "Forward should order ASC: {}", final_selection);
    }

    Ok(())
}

// ============================================================================
// 1.8 Live Mode Behavior
// ============================================================================

/// Test initial Live mode with should_auto_scroll.
#[tokio::test]
async fn test_live_mode_initial() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    // Initial render should be in Live mode with auto-scroll
    let vs = r.next_render().await?;

    assert!(vs.should_auto_scroll, "Initial render should have should_auto_scroll=true");
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Live);

    // Should be scrolled to bottom
    assert_eq!(r.scroll_offset, 1000); // 30*50 - 500 = 1000

    Ok(())
}

/// Test that scrolling up exits Live mode.
#[tokio::test]
async fn test_live_mode_exit_on_scroll_up() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Live);

    // Scroll up to trigger backward pagination - first scroll exits Live mode
    r.scroll_up_and_expect(
        400, 30, 1030..=1059, None,  // same items, no pagination yet
        true, false, false,          // mode changed, should_auto_scroll=false
        1042, 1051, 600,
        None,
    ).await?;
    r.scroll_up_and_expect(
        100, 50, 1010..=1059, Some(1049),
        true, true, false, 1040, 1049, 1500,
        Some("TRUE AND \"timestamp\" <= 1059 ORDER BY timestamp DESC LIMIT 51"),
    ).await?;

    // Should now be in Backward mode
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Backward);

    Ok(())
}

/// Test returning to Live mode when scrolling back to bottom.
#[tokio::test]
async fn test_live_mode_reentry() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    // Initial: Live mode
    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Live);

    // Scroll backward (deterministic - collect all renders)
    let _ = r.scroll_up_collect(500).await;
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Backward);

    // Continue scrolling toward oldest edge
    let _ = r.scroll_up_collect(500).await;
    let _ = r.scroll_up_collect(500).await;
    let _ = r.scroll_up_collect(500).await;

    // Scroll forward toward newest
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;
    let _ = r.scroll_down_collect(500).await;

    // Check final mode - might be Live, Forward, or Backward depending on exact positions
    let final_mode = sm.mode();
    assert!(
        final_mode == ankurah_virtual_scroll::ScrollMode::Backward
            || final_mode == ankurah_virtual_scroll::ScrollMode::Forward
            || final_mode == ankurah_virtual_scroll::ScrollMode::Live,
        "should be in a valid mode after scrolling: {:?}",
        final_mode
    );

    Ok(())
}

// ============================================================================
// 1.11 Concurrent Operations
// ============================================================================

/// Test that scroll events during pending pagination don't cause issues.
/// This is a basic concurrency test - the MockRenderer serializes events,
/// but we verify the system handles rapid state changes gracefully.
#[tokio::test]
async fn test_concurrent_scroll_events() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..60).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    let vs = r.next_render().await?;
    r.assert(&vs, 30, 1030..=1059, None, true, false, true, 1050, 1059);

    // Rapidly fire scroll events without waiting for renders
    // This simulates rapid user scrolling
    r.scroll_offset = 600; // Jump to offset 600
    let (first_idx, last_idx) = (12, 21); // Approximate visible at offset 600
    let first_id = vs.items[first_idx].entity().id();
    let last_id = vs.items[last_idx].entity().id();
    sm.on_scroll(first_id, last_id, true);

    // Immediately scroll more without waiting
    r.scroll_offset = 400;
    let (first_idx, last_idx) = (8, 17);
    let first_id = vs.items[first_idx].entity().id();
    let last_id = vs.items[last_idx].entity().id();
    sm.on_scroll(first_id, last_id, true);

    // Now wait for any pending render
    let vs = match tokio::time::timeout(
        std::time::Duration::from_millis(500),
        r.next_render()
    ).await {
        Ok(result) => result?,
        Err(_) => {
            // No render triggered is also valid if we stayed in buffer
            return Ok(());
        }
    };

    // Verify the result is valid regardless of which scroll "won"
    assert!(vs.items.len() >= 30, "Should have at least live_window items");
    let ts = timestamps(&vs);
    // Verify items are sorted
    for i in 1..ts.len() {
        assert!(ts[i-1] < ts[i], "Items should be sorted: {} >= {}", ts[i-1], ts[i]);
    }

    Ok(())
}

/// Test multiple pagination triggers in sequence.
/// Uses deterministic scroll_collect helpers to handle variable render timing.
#[tokio::test]
async fn test_sequential_paginations() -> Result<(), anyhow::Error> {
    let ctx = durable_sled_setup().await?;
    create_messages(&ctx, (0..100).map(|i| (1000 + i, 50))).await?;

    let sm = Arc::new(ScrollManager::<TestMessageView>::new(
        &ctx,
        "true",
        "timestamp DESC",
        50,
        2.0,
        500,
    )?);

    let mut r = MockRenderer::new(sm.clone(), 500);
    tokio::spawn({
        let sm = sm.clone();
        async move { sm.start().await }
    });

    // Initial: 30 items from 100
    let vs = r.next_render().await?;
    assert_eq!(vs.items.len(), 30);
    let initial_ts = timestamps(&vs);
    assert_eq!(initial_ts[0], 1070); // 100-30 = 70
    assert_eq!(*initial_ts.last().unwrap(), 1099);
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Live);

    // Trigger multiple backward paginations in sequence
    // Each should correctly extend the window

    // First backward scroll - exits Live mode and may trigger pagination
    let renders = r.scroll_up_collect(500).await;
    assert!(!renders.is_empty(), "First backward scroll should produce renders");
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Backward);

    // Second backward scroll
    let _ = r.scroll_up_collect(500).await;

    // Third backward scroll
    let _ = r.scroll_up_collect(500).await;

    // Fourth backward scroll - should have paginated multiple times by now
    let _ = r.scroll_up_collect(500).await;

    // Verify final state
    assert_eq!(sm.mode(), ankurah_virtual_scroll::ScrollMode::Backward);

    // The selection should reflect backward pagination with cursor constraint
    let selection = sm.current_selection();
    assert!(selection.contains("ORDER BY timestamp DESC"),
        "Backward mode should order DESC: {}", selection);
    assert!(selection.contains("LIMIT"),
        "Should have a LIMIT clause: {}", selection);

    Ok(())
}