atagen

macltok

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/history_repair.rsRust729 lines23.6 KB
1
mod support;
2
3
use std::time::Duration;
4
5
#[cfg(feature = "mirror")]
6
use macltok::Mirror;
7
use macltok::{
8
    ClientHandle, Engine, EngineConfig, EngineError, Event, EventStream, HistoryGap,
9
    HistoryRepairPhase, HistoryRepairReport, HistoryRepairStop, RecentHistoryState, Termination,
10
    TimelineDiff, TimelineId, TimelineItem,
11
};
12
use matrix_sdk::{
13
    Client,
14
    ruma::{EventId, RoomId, api::client::sync::sync_events::v5, event_id, room_id},
15
    test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
16
};
17
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
18
use support::shut_down;
19
use wiremock::ResponseTemplate;
20
21
struct TestRoom {
22
    server: MatrixMockServer,
23
    sync_client: Client,
24
    handle: ClientHandle,
25
    events: EventStream,
26
    running: tokio::task::JoinHandle<Result<Termination, EngineError>>,
27
    /// the open room's timeline, kept current by every helper that reads events
28
    items: imbl::Vector<TimelineItem>,
29
    opened: bool,
30
}
31
32
async fn start_room(room_id: &RoomId) -> TestRoom {
33
    let server = MatrixMockServer::new().await;
34
    server
35
        .mock_sliding_sync()
36
        .ok(v5::Response::new("1".to_owned()))
37
        .mount()
38
        .await;
39
    let client = server.client_builder().build().await;
40
    server.mock_room_state_encryption().plain().mount().await;
41
    server.sync_joined_room(&client, room_id).await;
42
    let sync_client = client.clone();
43
    let config = EngineConfig::default()
44
        .with_initial_history_limit(0)
45
        .with_search_backfill_pages_per_room(0);
46
    let (engine, handle, events) = Engine::from_client(client, config).await.unwrap();
47
    let running = tokio::spawn(engine.run());
48
    TestRoom {
49
        server,
50
        sync_client,
51
        handle,
52
        events,
53
        running,
54
        items: imbl::Vector::new(),
55
        opened: false,
56
    }
57
}
58
59
/// the next event that satisfies `predicate`. only the open itself may reset the room
60
/// timeline, history after it has to arrive as diffs
61
async fn next_until(room: &mut TestRoom, mut predicate: impl FnMut(&Event) -> bool) -> Event {
62
    tokio::time::timeout(Duration::from_secs(5), async {
63
        loop {
64
            let event = room.events.recv().await.expect("event stream closed");
65
            match &event {
66
                Event::TimelineReset {
67
                    timeline_id: TimelineId::Room(_),
68
                    items,
69
                } => {
70
                    assert!(!room.opened, "history reset the room timeline");
71
                    room.opened = true;
72
                    room.items = items.clone();
73
                }
74
                Event::TimelineChanged {
75
                    timeline_id: TimelineId::Room(_),
76
                    diffs,
77
                } => {
78
                    for diff in diffs.clone() {
79
                        assert!(
80
                            !matches!(diff, TimelineDiff::Reset { .. }),
81
                            "history replaced the room timeline"
82
                        );
83
                        diff.try_apply(&mut room.items)
84
                            .expect("diff did not fit the timeline");
85
                    }
86
                }
87
                _ => {}
88
            }
89
            if predicate(&event) {
90
                return event;
91
            }
92
        }
93
    })
94
    .await
95
    .expect("expected event did not arrive")
96
}
97
98
async fn timeline_until(
99
    room: &mut TestRoom,
100
    predicate: impl Fn(&imbl::Vector<TimelineItem>) -> bool,
101
) {
102
    tokio::time::timeout(Duration::from_secs(5), async {
103
        while !predicate(&room.items) {
104
            next_until(room, |_| true).await;
105
        }
106
    })
107
    .await
108
    .expect("the timeline never got there");
109
}
110
111
async fn open_room(room: &mut TestRoom, room_id: &RoomId) {
112
    room.handle.open_room(room_id.to_owned()).await.unwrap();
113
    next_until(room, |event| {
114
        matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)
115
    })
116
    .await;
117
}
118
119
async fn cache_event_before_open(room: &TestRoom, room_id: &RoomId, id: &EventId, body: &str) {
120
    room.server
121
        .sync_room(
122
            &room.sync_client,
123
            JoinedRoomBuilder::new(room_id).add_timeline_event(
124
                EventFactory::new()
125
                    .text_msg(body)
126
                    .event_id(id)
127
                    .sender(&ALICE),
128
            ),
129
        )
130
        .await;
131
}
132
133
fn history_event(
134
    room_id: &RoomId,
135
    id: &EventId,
136
    body: &str,
137
) -> matrix_sdk::ruma::serde::Raw<matrix_sdk::ruma::events::AnyTimelineEvent> {
138
    EventFactory::new()
139
        .room(room_id)
140
        .text_msg(body)
141
        .event_id(id)
142
        .sender(&ALICE)
143
        .into_raw_timeline()
144
}
145
146
async fn wait_for_recent(room: &mut TestRoom, room_id: &RoomId) -> RecentHistoryState {
147
    let event = next_until(room, |event| {
148
        matches!(event, Event::RecentHistoryChanged { room_id: id, state, .. }
149
            if id == room_id && !matches!(state, RecentHistoryState::Loading))
150
    })
151
    .await;
152
    match event {
153
        Event::RecentHistoryChanged { state, .. } => state,
154
        _ => unreachable!(),
155
    }
156
}
157
158
async fn wait_for_settled_fill(room: &mut TestRoom) -> HistoryRepairReport {
159
    match next_until(room, |event| {
160
        matches!(event, Event::HistoryRepairChanged(report)
161
            if report.phase != HistoryRepairPhase::Bridging)
162
    })
163
    .await
164
    {
165
        Event::HistoryRepairChanged(report) => report,
166
        _ => unreachable!(),
167
    }
168
}
169
170
fn has_event(items: &imbl::Vector<TimelineItem>, id: &EventId) -> bool {
171
    items.iter().any(
172
        |item| matches!(item, TimelineItem::Event(event) if event.event_id.as_deref() == Some(id)),
173
    )
174
}
175
176
fn gap_in(items: &imbl::Vector<TimelineItem>) -> Option<HistoryGap> {
177
    items.iter().find_map(|item| match item {
178
        TimelineItem::HistoryGap(gap) => Some(gap.clone()),
179
        _ => None,
180
    })
181
}
182
183
#[tokio::test]
184
async fn a_recent_load_is_bounded_and_keeps_cached_history_behind_a_gap() {
185
    let room_id = room_id!("!recent-repair:localhost");
186
    let old_id = event_id!("$old:localhost");
187
    let new_ids = [
188
        event_id!("$fresh-a:localhost"),
189
        event_id!("$fresh-b:localhost"),
190
        event_id!("$fresh-c:localhost"),
191
        event_id!("$fresh-d:localhost"),
192
    ];
193
    let mut room = start_room(room_id).await;
194
    for (index, id) in new_ids.iter().enumerate() {
195
        let token = format!("recent-{index}");
196
        let response = RoomMessagesResponseTemplate::default()
197
            .end_token(token.clone())
198
            .events(vec![history_event(room_id, id, "recent event")]);
199
        if index > 0 {
200
            let previous = format!("recent-{}", index - 1);
201
            room.server
202
                .mock_room_messages()
203
                .match_from(&previous)
204
                .ok(response)
205
                .mock_once()
206
                .mount()
207
                .await;
208
        } else {
209
            room.server
210
                .mock_room_messages()
211
                .ok(response)
212
                .mock_once()
213
                .mount()
214
                .await;
215
        }
216
    }
217
    cache_event_before_open(&room, room_id, old_id, "retained old event").await;
218
    open_room(&mut room, room_id).await;
219
    room.handle
220
        .retry_recent_history(room_id.to_owned())
221
        .await
222
        .unwrap();
223
    timeline_until(&mut room, |items| {
224
        gap_in(items).is_some()
225
            && has_event(items, old_id)
226
            && new_ids.iter().all(|id| has_event(items, id))
227
    })
228
    .await;
229
    assert_eq!(
230
        wait_for_recent(&mut room, room_id).await,
231
        RecentHistoryState::Loaded
232
    );
233
    let requests = room.server.received_requests().await.unwrap();
234
    let messages = requests
235
        .iter()
236
        .filter(|request| request.url.path().ends_with("/messages"))
237
        .collect::<Vec<_>>();
238
    assert_eq!(messages.len(), 4);
239
    assert!(!messages[0].url.query_pairs().any(|(key, _)| key == "from"));
240
    shut_down(&room.handle, &mut room.events, room.running).await;
241
}
242
243
#[tokio::test]
244
async fn explicit_recent_start_coalesces_with_the_automatic_load() {
245
    let room_id = room_id!("!recent-coalesce:localhost");
246
    let fresh_id = event_id!("$coalesced:localhost");
247
    let mut room = start_room(room_id).await;
248
    room.server
249
        .mock_room_messages()
250
        .ok(RoomMessagesResponseTemplate::default()
251
            .with_delay(Duration::from_millis(300))
252
            .events(vec![history_event(room_id, fresh_id, "shared result")]))
253
        .mock_once()
254
        .mount()
255
        .await;
256
    open_room(&mut room, room_id).await;
257
    tokio::time::timeout(Duration::from_secs(5), async {
258
        loop {
259
            let requested = room
260
                .server
261
                .received_requests()
262
                .await
263
                .unwrap()
264
                .iter()
265
                .any(|request| request.url.path().ends_with("/messages"));
266
            if requested {
267
                break;
268
            }
269
            tokio::task::yield_now().await;
270
        }
271
    })
272
    .await
273
    .expect("automatic recent request did not start");
274
275
    room.handle
276
        .retry_recent_history(room_id.to_owned())
277
        .await
278
        .unwrap();
279
    assert_eq!(
280
        wait_for_recent(&mut room, room_id).await,
281
        RecentHistoryState::Loaded
282
    );
283
    let requests = room.server.received_requests().await.unwrap();
284
    assert_eq!(
285
        requests
286
            .iter()
287
            .filter(|request| request.url.path().ends_with("/messages"))
288
            .count(),
289
        1
290
    );
291
    shut_down(&room.handle, &mut room.events, room.running).await;
292
}
293
294
#[cfg(feature = "mirror")]
295
#[tokio::test]
296
async fn fill_gap_returns_after_the_mirror_contains_the_committed_rows() {
297
    let server = MatrixMockServer::new().await;
298
    server
299
        .mock_sliding_sync()
300
        .ok(v5::Response::new("1".to_owned()))
301
        .mount()
302
        .await;
303
    let client = server.client_builder().build().await;
304
    let room_id = room_id!("!fill-gap-mirror:localhost");
305
    server.mock_room_state_encryption().plain().mount().await;
306
    server.sync_joined_room(&client, room_id).await;
307
    let sync_client = client.clone();
308
    let old_id = event_id!("$gap-old:localhost");
309
    let fresh_id = event_id!("$gap-fresh:localhost");
310
    let middle_id = event_id!("$gap-middle:localhost");
311
    let (engine, handle, events) = Engine::from_client(
312
        client,
313
        EngineConfig::default()
314
            .with_initial_history_limit(0)
315
            .with_search_backfill_pages_per_room(0),
316
    )
317
    .await
318
    .unwrap();
319
    let running = tokio::spawn(engine.run());
320
    let (mirror, mirror_handle) = Mirror::new(handle.clone(), events);
321
    let mirror_running = tokio::spawn(mirror.run());
322
    server
323
        .mock_room_messages()
324
        .ok(RoomMessagesResponseTemplate::default()
325
            .end_token("recent-gap")
326
            .events(vec![history_event(room_id, fresh_id, "new window")]))
327
        .mock_once()
328
        .mount()
329
        .await;
330
    server
331
        .mock_room_messages()
332
        .match_from("recent-gap")
333
        .ok(RoomMessagesResponseTemplate::default().end_token("recent-gap"))
334
        .mock_once()
335
        .mount()
336
        .await;
337
    server
338
        .sync_room(
339
            &sync_client,
340
            JoinedRoomBuilder::new(room_id).add_timeline_event(
341
                EventFactory::new()
342
                    .text_msg("old retained")
343
                    .event_id(old_id)
344
                    .sender(&ALICE),
345
            ),
346
        )
347
        .await;
348
    handle.open_room(room_id.to_owned()).await.unwrap();
349
    handle
350
        .retry_recent_history(room_id.to_owned())
351
        .await
352
        .unwrap();
353
    let timeline_id = TimelineId::from(room_id.to_owned());
354
    let gap = tokio::time::timeout(Duration::from_secs(5), async {
355
        loop {
356
            if let Some(gap) = mirror_handle
357
                .snapshot()
358
                .timelines
359
                .get(&timeline_id)
360
                .and_then(|timeline| gap_in(&timeline.items))
361
            {
362
                break gap;
363
            }
364
            tokio::task::yield_now().await;
365
        }
366
    })
367
    .await
368
    .expect("the recent history gap did not reach the mirror");
369
    tokio::time::timeout(Duration::from_secs(5), async {
370
        loop {
371
            if mirror_handle.snapshot().recent_history.get(room_id)
372
                == Some(&RecentHistoryState::Loaded)
373
            {
374
                break;
375
            }
376
            tokio::task::yield_now().await;
377
        }
378
    })
379
    .await
380
    .expect("automatic recent loading did not finish");
381
    server
382
        .mock_room_messages()
383
        .match_from("recent-gap")
384
        .ok(RoomMessagesResponseTemplate::default()
385
            .with_delay(Duration::from_millis(300))
386
            .events(vec![
387
                history_event(room_id, middle_id, "middle page"),
388
                history_event(room_id, old_id, "old retained overlap"),
389
            ]))
390
        .mock_once()
391
        .mount()
392
        .await;
393
394
    let fill_handle = handle.clone();
395
    let target_gap_id = gap.id.clone();
396
    let fill_gap_id = target_gap_id.clone();
397
    let fill = tokio::spawn(async move {
398
        fill_handle
399
            .fill_history_gap(room_id.to_owned(), fill_gap_id)
400
            .await
401
    });
402
    tokio::time::timeout(Duration::from_secs(5), async {
403
        loop {
404
            let snapshot = mirror_handle.snapshot();
405
            let active_gap = snapshot.repairs.get(room_id).is_some_and(|report| {
406
                report.gap_id == target_gap_id && report.phase == HistoryRepairPhase::Bridging
407
            });
408
            if active_gap {
409
                break;
410
            }
411
            tokio::task::yield_now().await;
412
        }
413
    })
414
    .await
415
    .expect("the active repair report did not identify its target gap");
416
    fill.await.unwrap().unwrap();
417
    let snapshot = mirror_handle.snapshot();
418
    let timeline = snapshot
419
        .timelines
420
        .get(&timeline_id)
421
        .expect("the room timeline disappeared");
422
    let event_ids = timeline
423
        .items
424
        .iter()
425
        .filter_map(|item| match item {
426
            TimelineItem::Event(event) => event.event_id.as_deref(),
427
            _ => None,
428
        })
429
        .collect::<Vec<_>>();
430
    assert!(event_ids.contains(&fresh_id));
431
    assert!(event_ids.contains(&middle_id));
432
    assert!(event_ids.contains(&old_id));
433
434
    handle.shutdown().await.unwrap();
435
    assert_eq!(running.await.unwrap().unwrap(), Termination::ShutDown);
436
    mirror_running.await.unwrap();
437
}
438
439
#[tokio::test]
440
async fn cancelling_a_gap_fill_keeps_committed_progress_for_retry() {
441
    let room_id = room_id!("!cancel-gap-fill:localhost");
442
    let old_id = event_id!("$cancel-gap-old:localhost");
443
    let fresh_id = event_id!("$cancel-gap-fresh:localhost");
444
    let middle_id = event_id!("$cancel-gap-middle:localhost");
445
    let mut room = start_room(room_id).await;
446
    room.server
447
        .mock_room_messages()
448
        .ok(RoomMessagesResponseTemplate::default()
449
            .end_token("gap-cursor-1")
450
            .events(vec![history_event(room_id, fresh_id, "recent")]))
451
        .mock_once()
452
        .mount()
453
        .await;
454
    room.server
455
        .mock_room_messages()
456
        .match_from("gap-cursor-1")
457
        .ok(RoomMessagesResponseTemplate::default().end_token("gap-cursor-1"))
458
        .mock_once()
459
        .mount()
460
        .await;
461
    cache_event_before_open(&room, room_id, old_id, "old cached").await;
462
    open_room(&mut room, room_id).await;
463
    room.handle
464
        .retry_recent_history(room_id.to_owned())
465
        .await
466
        .unwrap();
467
    timeline_until(&mut room, |items| gap_in(items).is_some()).await;
468
    let gap = gap_in(&room.items).unwrap();
469
    assert_eq!(
470
        wait_for_recent(&mut room, room_id).await,
471
        RecentHistoryState::Loaded
472
    );
473
    room.server
474
        .mock_room_messages()
475
        .match_from("gap-cursor-1")
476
        .ok(RoomMessagesResponseTemplate::default()
477
            .end_token("gap-cursor-2")
478
            .events(vec![history_event(
479
                room_id,
480
                middle_id,
481
                "committed progress",
482
            )]))
483
        .mock_once()
484
        .mount()
485
        .await;
486
    room.server
487
        .mock_room_messages()
488
        .match_from("gap-cursor-2")
489
        .ok(RoomMessagesResponseTemplate::default()
490
            .with_delay(Duration::from_secs(5))
491
            .events(vec![history_event(room_id, old_id, "old overlap")]))
492
        .mock_once()
493
        .mount()
494
        .await;
495
    let fill_handle = room.handle.clone();
496
    let gap_id = gap.id.clone();
497
    let fill = tokio::spawn(async move {
498
        fill_handle
499
            .fill_history_gap(room_id.to_owned(), gap_id)
500
            .await
501
    });
502
    tokio::time::timeout(Duration::from_secs(5), async {
503
        loop {
504
            let requested = room
505
                .server
506
                .received_requests()
507
                .await
508
                .unwrap()
509
                .iter()
510
                .any(|request| {
511
                    request.url.path().ends_with("/messages")
512
                        && request
513
                            .url
514
                            .query_pairs()
515
                            .any(|(key, value)| key == "from" && value == "gap-cursor-2")
516
                });
517
            if requested {
518
                break;
519
            }
520
            tokio::task::yield_now().await;
521
        }
522
    })
523
    .await
524
    .expect("the second gap page did not start");
525
    room.handle
526
        .cancel_history_repair(room_id.to_owned())
527
        .await
528
        .unwrap();
529
    let error = fill.await.unwrap().unwrap_err();
530
    assert_eq!(error.kind, macltok::ErrorKind::Interrupted);
531
    let report = wait_for_settled_fill(&mut room).await;
532
    assert_eq!(report.stop, Some(HistoryRepairStop::Cancelled));
533
    assert!(report.failure.is_none());
534
535
    room.server
536
        .mock_room_messages()
537
        .match_from("gap-cursor-2")
538
        .ok(
539
            RoomMessagesResponseTemplate::default().events(vec![history_event(
540
                room_id,
541
                old_id,
542
                "old overlap",
543
            )]),
544
        )
545
        .mock_once()
546
        .mount()
547
        .await;
548
    room.handle
549
        .fill_history_gap(room_id.to_owned(), gap.id)
550
        .await
551
        .unwrap();
552
    timeline_until(&mut room, |items| {
553
        [fresh_id, middle_id, old_id]
554
            .iter()
555
            .all(|id| has_event(items, id))
556
            && gap_in(items).is_none()
557
    })
558
    .await;
559
    shut_down(&room.handle, &mut room.events, room.running).await;
560
}
561
562
#[tokio::test]
563
async fn failed_recent_load_is_public_and_keeps_the_cached_timeline() {
564
    let room_id = room_id!("!failed-recent:localhost");
565
    let old_id = event_id!("$failed-old:localhost");
566
    let mut room = start_room(room_id).await;
567
    room.server
568
        .mock_room_messages()
569
        .respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({
570
            "errcode": "M_FORBIDDEN",
571
            "error": "history is unavailable",
572
        })))
573
        .mock_once()
574
        .mount()
575
        .await;
576
    cache_event_before_open(&room, room_id, old_id, "kept event").await;
577
    open_room(&mut room, room_id).await;
578
    assert!(
579
        room.handle
580
            .retry_recent_history(room_id.to_owned())
581
            .await
582
            .is_err()
583
    );
584
    let state = wait_for_recent(&mut room, room_id).await;
585
    assert!(matches!(state, RecentHistoryState::Failed(_)));
586
    shut_down(&room.handle, &mut room.events, room.running).await;
587
}
588
589
#[tokio::test]
590
async fn closing_during_a_recent_load_settles_idle_not_failed() {
591
    let room_id = room_id!("!close-recent:localhost");
592
    let mut room = start_room(room_id).await;
593
    room.server
594
        .mock_room_messages()
595
        .ok(RoomMessagesResponseTemplate::default()
596
            .with_delay(Duration::from_secs(5))
597
            .events(vec![history_event(
598
                room_id,
599
                event_id!("$never:localhost"),
600
                "too late",
601
            )]))
602
        .mock_once()
603
        .mount()
604
        .await;
605
    open_room(&mut room, room_id).await;
606
    tokio::time::timeout(Duration::from_secs(5), async {
607
        while !room
608
            .server
609
            .received_requests()
610
            .await
611
            .unwrap()
612
            .iter()
613
            .any(|request| request.url.path().ends_with("/messages"))
614
        {
615
            tokio::task::yield_now().await;
616
        }
617
    })
618
    .await
619
    .expect("the recent load never asked for messages");
620
    room.handle.close_room(room_id.to_owned()).await.unwrap();
621
    let mut states = Vec::new();
622
    next_until(&mut room, |event| {
623
        if let Event::RecentHistoryChanged { state, .. } = event {
624
            states.push(state.clone());
625
        }
626
        matches!(event, Event::TimelineClosed { timeline_id } if timeline_id.room_id() == room_id)
627
    })
628
    .await;
629
    assert_eq!(
630
        states,
631
        [RecentHistoryState::Loading, RecentHistoryState::Idle]
632
    );
633
    shut_down(&room.handle, &mut room.events, room.running).await;
634
}
635
636
#[tokio::test]
637
async fn a_reply_whose_target_is_not_loaded_fetches_it() {
638
    let room_id = room_id!("!replies:localhost");
639
    let target_id = event_id!("$target:localhost");
640
    let reply_id = event_id!("$reply:localhost");
641
    let mut room = start_room(room_id).await;
642
    room.server
643
        .mock_room_messages()
644
        .ok(RoomMessagesResponseTemplate::default())
645
        .mount()
646
        .await;
647
    room.server
648
        .mock_room_event()
649
        .match_event_id()
650
        .ok(EventFactory::new()
651
            .room(room_id)
652
            .text_msg("target")
653
            .event_id(target_id)
654
            .sender(&ALICE)
655
            .into_event())
656
        .mount()
657
        .await;
658
    room.server
659
        .sync_room(
660
            &room.sync_client,
661
            JoinedRoomBuilder::new(room_id).add_timeline_event(
662
                EventFactory::new()
663
                    .text_msg("reply")
664
                    .reply_to(target_id)
665
                    .event_id(reply_id)
666
                    .sender(&ALICE),
667
            ),
668
        )
669
        .await;
670
    open_room(&mut room, room_id).await;
671
    timeline_until(&mut room, |items| {
672
        items.iter().any(|item| {
673
            matches!(item, TimelineItem::Event(event)
674
            if event.event_id.as_deref() == Some(reply_id)
675
                && event.reply_to.as_ref().is_some_and(|reply| {
676
                    matches!(reply.details, macltok::ReplyDetails::Ready { .. })
677
                }))
678
        })
679
    })
680
    .await;
681
    shut_down(&room.handle, &mut room.events, room.running).await;
682
}
683
684
#[tokio::test]
685
async fn opening_a_retained_room_only_probes_its_newest_gap() {
686
    let room_id = room_id!("!probe:localhost");
687
    let old_id = event_id!("$probe-old:localhost");
688
    let live_id = event_id!("$probe-live:localhost");
689
    let mut room = start_room(room_id).await;
690
    cache_event_before_open(&room, room_id, old_id, "retained").await;
691
    room.server
692
        .sync_room(
693
            &room.sync_client,
694
            JoinedRoomBuilder::new(room_id)
695
                .add_timeline_event(
696
                    EventFactory::new()
697
                        .text_msg("live")
698
                        .event_id(live_id)
699
                        .sender(&ALICE),
700
                )
701
                .set_timeline_limited()
702
                .set_timeline_prev_batch("sync-edge"),
703
        )
704
        .await;
705
    room.server
706
        .mock_room_messages()
707
        .match_from("sync-edge")
708
        .ok(RoomMessagesResponseTemplate::default()
709
            .end_token("further")
710
            .events(vec![history_event(room_id, old_id, "retained")]))
711
        .mock_once()
712
        .mount()
713
        .await;
714
    open_room(&mut room, room_id).await;
715
    let report = wait_for_settled_fill(&mut room).await;
716
    assert_eq!(report.pages, 1);
717
    assert_eq!(report.stop, None);
718
    timeline_until(&mut room, |items| {
719
        gap_in(items).is_none() && has_event(items, old_id) && has_event(items, live_id)
720
    })
721
    .await;
722
    let requests = room.server.received_requests().await.unwrap();
723
    let messages = requests
724
        .iter()
725
        .filter(|request| request.url.path().ends_with("/messages"))
726
        .count();
727
    assert_eq!(messages, 1);
728
    shut_down(&room.handle, &mut room.events, room.running).await;
729
}