atagen

macltok

public
Star0
Watch0
2006a11ce142bf4e8a1e5ea7fdc6f96f79c4a603
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: tests/state.rs

31ce2453c82fatagen2026-09-24 13:07 UTC1
mod support;
31ce2453c82fatagen2026-09-24 13:07 UTC2
96fdf90aacd6atagen2026-09-08 08:24 UTC3
use std::time::Duration;
96fdf90aacd6atagen2026-09-08 08:24 UTC4
fe4133dbcd47atagen2026-09-26 09:05 UTC5
use macltok::{
17e042fbc331atagen2026-10-08 12:39 UTC6
    ConnectionState, Engine, EngineConfig, EngineError, Event, PackUsage, Retain, RoomMembership,
97fb5720d881atagen2026-10-25 14:15 UTC7
    Termination, ThreadListEntry, ThreadUnread,
fe4133dbcd47atagen2026-09-26 09:05 UTC8
};
96fdf90aacd6atagen2026-09-08 08:24 UTC9
use matrix_sdk::{
21b9227729a4atagen2026-10-25 11:03 UTC10
    ThreadingSupport,
e5c6427adb35atagen2026-10-25 10:23 UTC11
    ruma::{
97fb5720d881atagen2026-10-25 14:15 UTC12
        EventId, MilliSecondsSinceUnixEpoch, RoomId,
e5c6427adb35atagen2026-10-25 10:23 UTC13
        api::client::sync::sync_events::v5,
e5c6427adb35atagen2026-10-25 10:23 UTC14
        event_id,
e5c6427adb35atagen2026-10-25 10:23 UTC15
        events::{
e7c1b037154datagen2026-10-25 13:50 UTC16
            AnySyncStateEvent, AnyTimelineEvent, Mentions,
e5c6427adb35atagen2026-10-25 10:23 UTC17
            receipt::{ReceiptThread, ReceiptType},
e5c6427adb35atagen2026-10-25 10:23 UTC18
        },
e5c6427adb35atagen2026-10-25 10:23 UTC19
        room_id,
e5c6427adb35atagen2026-10-25 10:23 UTC20
        serde::Raw,
e5c6427adb35atagen2026-10-25 10:23 UTC21
    },
96fdf90aacd6atagen2026-09-08 08:24 UTC22
    test_utils::mocks::MatrixMockServer,
96fdf90aacd6atagen2026-09-08 08:24 UTC23
};
e5c6427adb35atagen2026-10-25 10:23 UTC24
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
17e042fbc331atagen2026-10-08 12:39 UTC25
use serde_json::json;
96fdf90aacd6atagen2026-09-08 08:24 UTC26
use tokio::sync::watch;
96fdf90aacd6atagen2026-09-08 08:24 UTC27
96fdf90aacd6atagen2026-09-08 08:24 UTC28
#[tokio::test]
96fdf90aacd6atagen2026-09-08 08:24 UTC29
async fn rooms_changed_invalidates_room_summaries() {
96fdf90aacd6atagen2026-09-08 08:24 UTC30
    let server = MatrixMockServer::new().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC31
    server
96fdf90aacd6atagen2026-09-08 08:24 UTC32
        .mock_sliding_sync()
96fdf90aacd6atagen2026-09-08 08:24 UTC33
        .ok(v5::Response::new("1".to_owned()))
96fdf90aacd6atagen2026-09-08 08:24 UTC34
        .mount()
96fdf90aacd6atagen2026-09-08 08:24 UTC35
        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC36
    let client = server.client_builder().build().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC37
    let room_id = room_id!("!state:localhost");
96fdf90aacd6atagen2026-09-08 08:24 UTC38
    server.mock_room_state_encryption().plain().mount().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC39
    server.sync_joined_room(&client, room_id).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC40
    let config = EngineConfig::default().with_initial_history_limit(0);
96fdf90aacd6atagen2026-09-08 08:24 UTC41
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
96fdf90aacd6atagen2026-09-08 08:24 UTC42
    let running = tokio::spawn(engine.run());
96fdf90aacd6atagen2026-09-08 08:24 UTC43
31ce2453c82fatagen2026-09-24 13:07 UTC44
    support::receive_until(&mut events, |event| matches!(event, Event::RoomsChanged)).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC45
    let rooms = handle.rooms().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC46
    let summary = rooms
96fdf90aacd6atagen2026-09-08 08:24 UTC47
        .iter()
96fdf90aacd6atagen2026-09-08 08:24 UTC48
        .find(|summary| summary.room_id == room_id)
96fdf90aacd6atagen2026-09-08 08:24 UTC49
        .expect("synced room was absent from the room summaries");
96fdf90aacd6atagen2026-09-08 08:24 UTC50
    assert_eq!(summary.membership, RoomMembership::Joined);
96fdf90aacd6atagen2026-09-08 08:24 UTC51
    assert!(!summary.name.is_empty());
96fdf90aacd6atagen2026-09-08 08:24 UTC52
31ce2453c82fatagen2026-09-24 13:07 UTC53
    support::shut_down(&handle, &mut events, running).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC54
}
96fdf90aacd6atagen2026-09-08 08:24 UTC55
96fdf90aacd6atagen2026-09-08 08:24 UTC56
#[tokio::test]
96fdf90aacd6atagen2026-09-08 08:24 UTC57
async fn connection_watch_tracks_the_engine_lifecycle() {
96fdf90aacd6atagen2026-09-08 08:24 UTC58
    let server = MatrixMockServer::new().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC59
    server
96fdf90aacd6atagen2026-09-08 08:24 UTC60
        .mock_sliding_sync()
96fdf90aacd6atagen2026-09-08 08:24 UTC61
        .ok(v5::Response::new("1".to_owned()))
96fdf90aacd6atagen2026-09-08 08:24 UTC62
        .mount()
96fdf90aacd6atagen2026-09-08 08:24 UTC63
        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC64
    let client = server.client_builder().build().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC65
    let config = EngineConfig::default().with_initial_history_limit(0);
96fdf90aacd6atagen2026-09-08 08:24 UTC66
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
96fdf90aacd6atagen2026-09-08 08:24 UTC67
    let mut connection = handle.connection_state();
96fdf90aacd6atagen2026-09-08 08:24 UTC68
    let connection_peer = connection.clone();
96fdf90aacd6atagen2026-09-08 08:24 UTC69
    assert_eq!(*connection.borrow(), ConnectionState::Idle);
96fdf90aacd6atagen2026-09-08 08:24 UTC70
    assert_eq!(*connection_peer.borrow(), ConnectionState::Idle);
96fdf90aacd6atagen2026-09-08 08:24 UTC71
96fdf90aacd6atagen2026-09-08 08:24 UTC72
    let running = tokio::spawn(engine.run());
31ce2453c82fatagen2026-09-24 13:07 UTC73
    support::receive_until(&mut events, |event| {
96fdf90aacd6atagen2026-09-08 08:24 UTC74
        matches!(event, Event::ConnectionChanged(ConnectionState::Connecting))
96fdf90aacd6atagen2026-09-08 08:24 UTC75
    })
96fdf90aacd6atagen2026-09-08 08:24 UTC76
    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC77
    assert_ne!(*connection.borrow(), ConnectionState::Idle);
31ce2453c82fatagen2026-09-24 13:07 UTC78
    support::receive_until(&mut events, |event| {
96fdf90aacd6atagen2026-09-08 08:24 UTC79
        matches!(event, Event::ConnectionChanged(ConnectionState::Online))
96fdf90aacd6atagen2026-09-08 08:24 UTC80
    })
96fdf90aacd6atagen2026-09-08 08:24 UTC81
    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC82
    wait_for_connection(&mut connection, ConnectionState::Online).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC83
    assert_eq!(*connection_peer.borrow(), ConnectionState::Online);
96fdf90aacd6atagen2026-09-08 08:24 UTC84
31ce2453c82fatagen2026-09-24 13:07 UTC85
    support::shut_down(&handle, &mut events, running).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC86
    wait_for_connection(&mut connection, ConnectionState::Stopped).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC87
    assert_eq!(*connection_peer.borrow(), ConnectionState::Stopped);
96fdf90aacd6atagen2026-09-08 08:24 UTC88
}
96fdf90aacd6atagen2026-09-08 08:24 UTC89
96fdf90aacd6atagen2026-09-08 08:24 UTC90
#[tokio::test]
fa9114fcf75fatagen2026-09-24 12:51 UTC91
async fn soft_logout_says_so_and_stops_the_engine() {
fa9114fcf75fatagen2026-09-24 12:51 UTC92
    let server = MatrixMockServer::new().await;
fa9114fcf75fatagen2026-09-24 12:51 UTC93
    server
fa9114fcf75fatagen2026-09-24 12:51 UTC94
        .mock_sliding_sync()
fa9114fcf75fatagen2026-09-24 12:51 UTC95
        .error_unknown_token(true)
fa9114fcf75fatagen2026-09-24 12:51 UTC96
        .mount()
fa9114fcf75fatagen2026-09-24 12:51 UTC97
        .await;
fa9114fcf75fatagen2026-09-24 12:51 UTC98
    let client = server.client_builder().build().await;
fa9114fcf75fatagen2026-09-24 12:51 UTC99
    let (engine, _handle, mut events) = Engine::from_client(
fa9114fcf75fatagen2026-09-24 12:51 UTC100
        client,
fa9114fcf75fatagen2026-09-24 12:51 UTC101
        EngineConfig::default().with_initial_history_limit(0),
fa9114fcf75fatagen2026-09-24 12:51 UTC102
    )
fa9114fcf75fatagen2026-09-24 12:51 UTC103
    .await
fa9114fcf75fatagen2026-09-24 12:51 UTC104
    .unwrap();
fa9114fcf75fatagen2026-09-24 12:51 UTC105
    let running = tokio::spawn(engine.run());
31ce2453c82fatagen2026-09-24 13:07 UTC106
    support::receive_until(&mut events, |event| {
7c26fad34822atagen2026-09-26 09:41 UTC107
        matches!(
7c26fad34822atagen2026-09-26 09:41 UTC108
            event,
7c26fad34822atagen2026-09-26 09:41 UTC109
            Event::SessionAccessRejected {
7c26fad34822atagen2026-09-26 09:41 UTC110
                soft_logout: Some(_)
7c26fad34822atagen2026-09-26 09:41 UTC111
            }
7c26fad34822atagen2026-09-26 09:41 UTC112
        )
fa9114fcf75fatagen2026-09-24 12:51 UTC113
    })
fa9114fcf75fatagen2026-09-24 12:51 UTC114
    .await;
31ce2453c82fatagen2026-09-24 13:07 UTC115
    support::receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
fe4133dbcd47atagen2026-09-26 09:05 UTC116
    let termination = running.await.unwrap().unwrap();
fe4133dbcd47atagen2026-09-26 09:05 UTC117
    assert!(matches!(termination, Termination::SoftLoggedOut { .. }));
fe4133dbcd47atagen2026-09-26 09:05 UTC118
    assert_eq!(termination.retain(), Retain::Store);
fa9114fcf75fatagen2026-09-24 12:51 UTC119
}
fa9114fcf75fatagen2026-09-24 12:51 UTC120
fa9114fcf75fatagen2026-09-24 12:51 UTC121
#[tokio::test]
96fdf90aacd6atagen2026-09-08 08:24 UTC122
async fn closing_the_event_stream_stops_an_idle_engine() {
96fdf90aacd6atagen2026-09-08 08:24 UTC123
    let server = MatrixMockServer::new().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC124
    server
96fdf90aacd6atagen2026-09-08 08:24 UTC125
        .mock_sliding_sync()
96fdf90aacd6atagen2026-09-08 08:24 UTC126
        .ok(v5::Response::new("1".to_owned()))
96fdf90aacd6atagen2026-09-08 08:24 UTC127
        .mount()
96fdf90aacd6atagen2026-09-08 08:24 UTC128
        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC129
    let client = server.client_builder().build().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC130
    let (engine, handle, mut events) = Engine::from_client(
96fdf90aacd6atagen2026-09-08 08:24 UTC131
        client,
96fdf90aacd6atagen2026-09-08 08:24 UTC132
        EngineConfig::default().with_initial_history_limit(0),
96fdf90aacd6atagen2026-09-08 08:24 UTC133
    )
96fdf90aacd6atagen2026-09-08 08:24 UTC134
    .await
96fdf90aacd6atagen2026-09-08 08:24 UTC135
    .unwrap();
96fdf90aacd6atagen2026-09-08 08:24 UTC136
    let running = tokio::spawn(engine.run());
31ce2453c82fatagen2026-09-24 13:07 UTC137
    support::receive_until(&mut events, |event| {
96fdf90aacd6atagen2026-09-08 08:24 UTC138
        matches!(event, Event::ConnectionChanged(ConnectionState::Online))
96fdf90aacd6atagen2026-09-08 08:24 UTC139
    })
96fdf90aacd6atagen2026-09-08 08:24 UTC140
    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC141
    drop(events);
96fdf90aacd6atagen2026-09-08 08:24 UTC142
    let result = tokio::time::timeout(Duration::from_secs(2), running)
96fdf90aacd6atagen2026-09-08 08:24 UTC143
        .await
96fdf90aacd6atagen2026-09-08 08:24 UTC144
        .expect("engine did not stop after its event receiver closed")
96fdf90aacd6atagen2026-09-08 08:24 UTC145
        .unwrap();
96fdf90aacd6atagen2026-09-08 08:24 UTC146
    assert!(matches!(result, Err(EngineError::EventReceiverClosed)));
96fdf90aacd6atagen2026-09-08 08:24 UTC147
    drop(handle);
96fdf90aacd6atagen2026-09-08 08:24 UTC148
}
96fdf90aacd6atagen2026-09-08 08:24 UTC149
96fdf90aacd6atagen2026-09-08 08:24 UTC150
async fn wait_for_connection(
96fdf90aacd6atagen2026-09-08 08:24 UTC151
    connection: &mut watch::Receiver<ConnectionState>,
96fdf90aacd6atagen2026-09-08 08:24 UTC152
    expected: ConnectionState,
96fdf90aacd6atagen2026-09-08 08:24 UTC153
) {
96fdf90aacd6atagen2026-09-08 08:24 UTC154
    tokio::time::timeout(Duration::from_secs(2), async {
96fdf90aacd6atagen2026-09-08 08:24 UTC155
        loop {
96fdf90aacd6atagen2026-09-08 08:24 UTC156
            if *connection.borrow_and_update() == expected {
96fdf90aacd6atagen2026-09-08 08:24 UTC157
                return;
96fdf90aacd6atagen2026-09-08 08:24 UTC158
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC159
            connection
96fdf90aacd6atagen2026-09-08 08:24 UTC160
                .changed()
96fdf90aacd6atagen2026-09-08 08:24 UTC161
                .await
96fdf90aacd6atagen2026-09-08 08:24 UTC162
                .expect("connection watch closed before the expected state");
96fdf90aacd6atagen2026-09-08 08:24 UTC163
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC164
    })
96fdf90aacd6atagen2026-09-08 08:24 UTC165
    .await
96fdf90aacd6atagen2026-09-08 08:24 UTC166
    .expect("expected connection state did not arrive");
96fdf90aacd6atagen2026-09-08 08:24 UTC167
}
17e042fbc331atagen2026-10-08 12:39 UTC168
17e042fbc331atagen2026-10-08 12:39 UTC169
#[tokio::test]
17e042fbc331atagen2026-10-08 12:39 UTC170
async fn image_packs_follow_room_state_and_account_data() {
17e042fbc331atagen2026-10-08 12:39 UTC171
    let server = MatrixMockServer::new().await;
17e042fbc331atagen2026-10-08 12:39 UTC172
    server
17e042fbc331atagen2026-10-08 12:39 UTC173
        .mock_sliding_sync()
17e042fbc331atagen2026-10-08 12:39 UTC174
        .ok(v5::Response::new("1".to_owned()))
17e042fbc331atagen2026-10-08 12:39 UTC175
        .mount()
17e042fbc331atagen2026-10-08 12:39 UTC176
        .await;
17e042fbc331atagen2026-10-08 12:39 UTC177
    let client = server.client_builder().build().await;
17e042fbc331atagen2026-10-08 12:39 UTC178
    let room_id = room_id!("!packs:localhost");
17e042fbc331atagen2026-10-08 12:39 UTC179
    server.mock_room_state_encryption().plain().mount().await;
17e042fbc331atagen2026-10-08 12:39 UTC180
    server.sync_joined_room(&client, room_id).await;
17e042fbc331atagen2026-10-08 12:39 UTC181
    let config = EngineConfig::default().with_initial_history_limit(0);
17e042fbc331atagen2026-10-08 12:39 UTC182
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
17e042fbc331atagen2026-10-08 12:39 UTC183
    let running = tokio::spawn(engine.run());
17e042fbc331atagen2026-10-08 12:39 UTC184
17e042fbc331atagen2026-10-08 12:39 UTC185
    let pack: Raw<AnySyncStateEvent> = serde_json::from_value(json!({
17e042fbc331atagen2026-10-08 12:39 UTC186
        "type": "im.ponies.room_emotes",
17e042fbc331atagen2026-10-08 12:39 UTC187
        "state_key": "cats",
17e042fbc331atagen2026-10-08 12:39 UTC188
        "event_id": "$pack",
17e042fbc331atagen2026-10-08 12:39 UTC189
        "sender": "@example:localhost",
17e042fbc331atagen2026-10-08 12:39 UTC190
        "origin_server_ts": 1,
17e042fbc331atagen2026-10-08 12:39 UTC191
        "content": {
17e042fbc331atagen2026-10-08 12:39 UTC192
            "pack": { "display_name": "cats", "usage": ["emoticon"] },
17e042fbc331atagen2026-10-08 12:39 UTC193
            "images": {
17e042fbc331atagen2026-10-08 12:39 UTC194
                "blep": { "url": "mxc://localhost/blep" },
17e042fbc331atagen2026-10-08 12:39 UTC195
                "broken": { "url": "not an mxc" },
17e042fbc331atagen2026-10-08 12:39 UTC196
                "sticker": { "url": "mxc://localhost/sticker", "usage": ["sticker"] },
17e042fbc331atagen2026-10-08 12:39 UTC197
            },
17e042fbc331atagen2026-10-08 12:39 UTC198
        },
17e042fbc331atagen2026-10-08 12:39 UTC199
    }))
17e042fbc331atagen2026-10-08 12:39 UTC200
    .unwrap();
17e042fbc331atagen2026-10-08 12:39 UTC201
    server
17e042fbc331atagen2026-10-08 12:39 UTC202
        .mock_sync()
17e042fbc331atagen2026-10-08 12:39 UTC203
        .ok_and_run(&client, |builder| {
17e042fbc331atagen2026-10-08 12:39 UTC204
            builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(pack));
17e042fbc331atagen2026-10-08 12:39 UTC205
            builder.add_custom_global_account_data(json!({
17e042fbc331atagen2026-10-08 12:39 UTC206
                "type": "im.ponies.emote_rooms",
17e042fbc331atagen2026-10-08 12:39 UTC207
                "content": { "rooms": { "!packs:localhost": { "cats": {} } } },
17e042fbc331atagen2026-10-08 12:39 UTC208
            }));
17e042fbc331atagen2026-10-08 12:39 UTC209
        })
17e042fbc331atagen2026-10-08 12:39 UTC210
        .await;
17e042fbc331atagen2026-10-08 12:39 UTC211
    support::receive_until(&mut events, |event| {
17e042fbc331atagen2026-10-08 12:39 UTC212
        matches!(event, Event::ImagePacksChanged)
17e042fbc331atagen2026-10-08 12:39 UTC213
    })
17e042fbc331atagen2026-10-08 12:39 UTC214
    .await;
17e042fbc331atagen2026-10-08 12:39 UTC215
17e042fbc331atagen2026-10-08 12:39 UTC216
    let packs = handle.image_packs().await.unwrap();
17e042fbc331atagen2026-10-08 12:39 UTC217
    let offered = packs.for_room(Some(room_id));
17e042fbc331atagen2026-10-08 12:39 UTC218
    assert_eq!(offered.len(), 1, "an enabled room's pack was offered twice");
17e042fbc331atagen2026-10-08 12:39 UTC219
    let pack = offered[0];
17e042fbc331atagen2026-10-08 12:39 UTC220
    assert_eq!(pack.id, "room:!packs:localhost:cats");
17e042fbc331atagen2026-10-08 12:39 UTC221
    assert_eq!(packs.global, vec![pack.clone()]);
17e042fbc331atagen2026-10-08 12:39 UTC222
    let images: Vec<_> = pack
17e042fbc331atagen2026-10-08 12:39 UTC223
        .images
17e042fbc331atagen2026-10-08 12:39 UTC224
        .iter()
17e042fbc331atagen2026-10-08 12:39 UTC225
        .map(|image| {
17e042fbc331atagen2026-10-08 12:39 UTC226
            (
17e042fbc331atagen2026-10-08 12:39 UTC227
                image.shortcode.as_str(),
17e042fbc331atagen2026-10-08 12:39 UTC228
                image.reaction_key.as_str(),
17e042fbc331atagen2026-10-08 12:39 UTC229
                image.usage.clone(),
17e042fbc331atagen2026-10-08 12:39 UTC230
            )
17e042fbc331atagen2026-10-08 12:39 UTC231
        })
17e042fbc331atagen2026-10-08 12:39 UTC232
        .collect();
17e042fbc331atagen2026-10-08 12:39 UTC233
    assert_eq!(
17e042fbc331atagen2026-10-08 12:39 UTC234
        images,
17e042fbc331atagen2026-10-08 12:39 UTC235
        [
17e042fbc331atagen2026-10-08 12:39 UTC236
            ("blep", "mxc://localhost/blep", vec![PackUsage::Emoticon]),
17e042fbc331atagen2026-10-08 12:39 UTC237
            (
17e042fbc331atagen2026-10-08 12:39 UTC238
                "sticker",
17e042fbc331atagen2026-10-08 12:39 UTC239
                "mxc://localhost/sticker",
17e042fbc331atagen2026-10-08 12:39 UTC240
                vec![PackUsage::Sticker]
17e042fbc331atagen2026-10-08 12:39 UTC241
            ),
17e042fbc331atagen2026-10-08 12:39 UTC242
        ]
17e042fbc331atagen2026-10-08 12:39 UTC243
    );
17e042fbc331atagen2026-10-08 12:39 UTC244
17e042fbc331atagen2026-10-08 12:39 UTC245
    support::shut_down(&handle, &mut events, running).await;
17e042fbc331atagen2026-10-08 12:39 UTC246
}
e5c6427adb35atagen2026-10-25 10:23 UTC247
e5c6427adb35atagen2026-10-25 10:23 UTC248
/// the sdk counts none of this once threads are on, and a threaded receipt never reaches
e5c6427adb35atagen2026-10-25 10:23 UTC249
/// room info, so only our own recount notices it
e5c6427adb35atagen2026-10-25 10:23 UTC250
#[tokio::test]
e5c6427adb35atagen2026-10-25 10:23 UTC251
async fn thread_unreads_follow_threaded_receipts() {
e5c6427adb35atagen2026-10-25 10:23 UTC252
    let server = MatrixMockServer::new().await;
e5c6427adb35atagen2026-10-25 10:23 UTC253
    server
e5c6427adb35atagen2026-10-25 10:23 UTC254
        .mock_sliding_sync()
e5c6427adb35atagen2026-10-25 10:23 UTC255
        .ok(v5::Response::new("1".to_owned()))
e5c6427adb35atagen2026-10-25 10:23 UTC256
        .mount()
e5c6427adb35atagen2026-10-25 10:23 UTC257
        .await;
21b9227729a4atagen2026-10-25 11:03 UTC258
    let client = server
21b9227729a4atagen2026-10-25 11:03 UTC259
        .client_builder()
21b9227729a4atagen2026-10-25 11:03 UTC260
        .on_builder(|builder| {
21b9227729a4atagen2026-10-25 11:03 UTC261
            builder.with_threading_support(ThreadingSupport::Enabled {
21b9227729a4atagen2026-10-25 11:03 UTC262
                with_subscriptions: false,
21b9227729a4atagen2026-10-25 11:03 UTC263
            })
21b9227729a4atagen2026-10-25 11:03 UTC264
        })
21b9227729a4atagen2026-10-25 11:03 UTC265
        .build()
21b9227729a4atagen2026-10-25 11:03 UTC266
        .await;
e5c6427adb35atagen2026-10-25 10:23 UTC267
    let room_id = room_id!("!threads:localhost");
21b9227729a4atagen2026-10-25 11:03 UTC268
    let own = client.user_id().unwrap().to_owned();
e5c6427adb35atagen2026-10-25 10:23 UTC269
    server.mock_room_state_encryption().plain().mount().await;
e5c6427adb35atagen2026-10-25 10:23 UTC270
    client.event_cache().subscribe().unwrap();
21b9227729a4atagen2026-10-25 11:03 UTC271
    // without our own member event there's no push context, so no push actions to count
21b9227729a4atagen2026-10-25 11:03 UTC272
    server
21b9227729a4atagen2026-10-25 11:03 UTC273
        .sync_room(
21b9227729a4atagen2026-10-25 11:03 UTC274
            &client,
21b9227729a4atagen2026-10-25 11:03 UTC275
            JoinedRoomBuilder::new(room_id)
21b9227729a4atagen2026-10-25 11:03 UTC276
                .add_state_event(EventFactory::new().room(room_id).member(&own)),
21b9227729a4atagen2026-10-25 11:03 UTC277
        )
21b9227729a4atagen2026-10-25 11:03 UTC278
        .await;
e5c6427adb35atagen2026-10-25 10:23 UTC279
    let config = EngineConfig::default().with_initial_history_limit(0);
e5c6427adb35atagen2026-10-25 10:23 UTC280
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
e5c6427adb35atagen2026-10-25 10:23 UTC281
    let running = tokio::spawn(engine.run());
e5c6427adb35atagen2026-10-25 10:23 UTC282
e5c6427adb35atagen2026-10-25 10:23 UTC283
    let (root, first, second) = (
e5c6427adb35atagen2026-10-25 10:23 UTC284
        event_id!("$root"),
e5c6427adb35atagen2026-10-25 10:23 UTC285
        event_id!("$first"),
e5c6427adb35atagen2026-10-25 10:23 UTC286
        event_id!("$second"),
e5c6427adb35atagen2026-10-25 10:23 UTC287
    );
e5c6427adb35atagen2026-10-25 10:23 UTC288
    let factory = EventFactory::new().room(room_id).sender(&ALICE);
e5c6427adb35atagen2026-10-25 10:23 UTC289
    server
e5c6427adb35atagen2026-10-25 10:23 UTC290
        .sync_room(
e5c6427adb35atagen2026-10-25 10:23 UTC291
            &client,
e5c6427adb35atagen2026-10-25 10:23 UTC292
            JoinedRoomBuilder::new(room_id)
e5c6427adb35atagen2026-10-25 10:23 UTC293
                .add_timeline_event(factory.text_msg("root").event_id(root))
e5c6427adb35atagen2026-10-25 10:23 UTC294
                .add_timeline_event(
e5c6427adb35atagen2026-10-25 10:23 UTC295
                    factory
e5c6427adb35atagen2026-10-25 10:23 UTC296
                        .text_msg("you there?")
e5c6427adb35atagen2026-10-25 10:23 UTC297
                        .in_thread(root, root)
e5c6427adb35atagen2026-10-25 10:23 UTC298
                        .mentions(Mentions::with_user_ids([own.clone()]))
e5c6427adb35atagen2026-10-25 10:23 UTC299
                        .event_id(first),
e5c6427adb35atagen2026-10-25 10:23 UTC300
                )
e5c6427adb35atagen2026-10-25 10:23 UTC301
                .add_timeline_event(
e5c6427adb35atagen2026-10-25 10:23 UTC302
                    factory
e5c6427adb35atagen2026-10-25 10:23 UTC303
                        .text_msg("hello?")
e5c6427adb35atagen2026-10-25 10:23 UTC304
                        .in_thread(root, first)
e5c6427adb35atagen2026-10-25 10:23 UTC305
                        .event_id(second),
e5c6427adb35atagen2026-10-25 10:23 UTC306
                ),
e5c6427adb35atagen2026-10-25 10:23 UTC307
        )
e5c6427adb35atagen2026-10-25 10:23 UTC308
        .await;
e5c6427adb35atagen2026-10-25 10:23 UTC309
    let unread = thread_unread_once(&handle, &mut events, room_id, |unread| {
e5c6427adb35atagen2026-10-25 10:23 UTC310
        unread.is_some_and(|unread| unread.messages == 2)
e5c6427adb35atagen2026-10-25 10:23 UTC311
    })
e5c6427adb35atagen2026-10-25 10:23 UTC312
    .await;
e5c6427adb35atagen2026-10-25 10:23 UTC313
    assert_eq!(unread.mentions, 1);
e5c6427adb35atagen2026-10-25 10:23 UTC314
    let summary = room(&handle, room_id).await;
e5c6427adb35atagen2026-10-25 10:23 UTC315
    assert!(
e5c6427adb35atagen2026-10-25 10:23 UTC316
        summary.unread_mentions >= 1,
e5c6427adb35atagen2026-10-25 10:23 UTC317
        "the thread mention left the room's count"
e5c6427adb35atagen2026-10-25 10:23 UTC318
    );
e5c6427adb35atagen2026-10-25 10:23 UTC319
e5c6427adb35atagen2026-10-25 10:23 UTC320
    server
e5c6427adb35atagen2026-10-25 10:23 UTC321
        .sync_room(
e5c6427adb35atagen2026-10-25 10:23 UTC322
            &client,
e5c6427adb35atagen2026-10-25 10:23 UTC323
            JoinedRoomBuilder::new(room_id).add_receipt(
e5c6427adb35atagen2026-10-25 10:23 UTC324
                factory
e5c6427adb35atagen2026-10-25 10:23 UTC325
                    .read_receipts()
e5c6427adb35atagen2026-10-25 10:23 UTC326
                    .add(
e5c6427adb35atagen2026-10-25 10:23 UTC327
                        first,
e5c6427adb35atagen2026-10-25 10:23 UTC328
                        &own,
e5c6427adb35atagen2026-10-25 10:23 UTC329
                        ReceiptType::Read,
e5c6427adb35atagen2026-10-25 10:23 UTC330
                        ReceiptThread::Thread(root.to_owned()),
e5c6427adb35atagen2026-10-25 10:23 UTC331
                    )
e5c6427adb35atagen2026-10-25 10:23 UTC332
                    .into_event(),
e5c6427adb35atagen2026-10-25 10:23 UTC333
            ),
e5c6427adb35atagen2026-10-25 10:23 UTC334
        )
e5c6427adb35atagen2026-10-25 10:23 UTC335
        .await;
e5c6427adb35atagen2026-10-25 10:23 UTC336
    let unread = thread_unread_once(&handle, &mut events, room_id, |unread| {
e5c6427adb35atagen2026-10-25 10:23 UTC337
        unread.is_some_and(|unread| unread.messages == 1)
e5c6427adb35atagen2026-10-25 10:23 UTC338
    })
e5c6427adb35atagen2026-10-25 10:23 UTC339
    .await;
e5c6427adb35atagen2026-10-25 10:23 UTC340
    assert_eq!(unread.mentions, 0);
e5c6427adb35atagen2026-10-25 10:23 UTC341
e5c6427adb35atagen2026-10-25 10:23 UTC342
    server
e5c6427adb35atagen2026-10-25 10:23 UTC343
        .sync_room(
e5c6427adb35atagen2026-10-25 10:23 UTC344
            &client,
e5c6427adb35atagen2026-10-25 10:23 UTC345
            JoinedRoomBuilder::new(room_id).add_receipt(
e5c6427adb35atagen2026-10-25 10:23 UTC346
                factory
e5c6427adb35atagen2026-10-25 10:23 UTC347
                    .read_receipts()
e5c6427adb35atagen2026-10-25 10:23 UTC348
                    .add(second, &own, ReceiptType::Read, ReceiptThread::Unthreaded)
e5c6427adb35atagen2026-10-25 10:23 UTC349
                    .into_event(),
e5c6427adb35atagen2026-10-25 10:23 UTC350
            ),
e5c6427adb35atagen2026-10-25 10:23 UTC351
        )
e5c6427adb35atagen2026-10-25 10:23 UTC352
        .await;
e5c6427adb35atagen2026-10-25 10:23 UTC353
    thread_unread_once(&handle, &mut events, room_id, |unread| unread.is_none()).await;
e5c6427adb35atagen2026-10-25 10:23 UTC354
e5c6427adb35atagen2026-10-25 10:23 UTC355
    support::shut_down(&handle, &mut events, running).await;
e5c6427adb35atagen2026-10-25 10:23 UTC356
}
e5c6427adb35atagen2026-10-25 10:23 UTC357
e5c6427adb35atagen2026-10-25 10:23 UTC358
async fn room(handle: &macltok::ClientHandle, room_id: &RoomId) -> macltok::RoomSummary {
e5c6427adb35atagen2026-10-25 10:23 UTC359
    handle
e5c6427adb35atagen2026-10-25 10:23 UTC360
        .rooms()
e5c6427adb35atagen2026-10-25 10:23 UTC361
        .await
e5c6427adb35atagen2026-10-25 10:23 UTC362
        .into_iter()
e5c6427adb35atagen2026-10-25 10:23 UTC363
        .find(|summary| summary.room_id == room_id)
e5c6427adb35atagen2026-10-25 10:23 UTC364
        .expect("synced room was absent from the room summaries")
e5c6427adb35atagen2026-10-25 10:23 UTC365
}
e5c6427adb35atagen2026-10-25 10:23 UTC366
e5c6427adb35atagen2026-10-25 10:23 UTC367
/// waits out `RoomsChanged` until the root's counts satisfy `done`
e5c6427adb35atagen2026-10-25 10:23 UTC368
async fn thread_unread_once(
e5c6427adb35atagen2026-10-25 10:23 UTC369
    handle: &macltok::ClientHandle,
e5c6427adb35atagen2026-10-25 10:23 UTC370
    events: &mut macltok::EventStream,
e5c6427adb35atagen2026-10-25 10:23 UTC371
    room_id: &RoomId,
e5c6427adb35atagen2026-10-25 10:23 UTC372
    done: impl Fn(Option<&ThreadUnread>) -> bool,
e5c6427adb35atagen2026-10-25 10:23 UTC373
) -> ThreadUnread {
e5c6427adb35atagen2026-10-25 10:23 UTC374
    let root = event_id!("$root");
e5c6427adb35atagen2026-10-25 10:23 UTC375
    tokio::time::timeout(Duration::from_secs(10), async {
e5c6427adb35atagen2026-10-25 10:23 UTC376
        loop {
e5c6427adb35atagen2026-10-25 10:23 UTC377
            let summary = room(handle, room_id).await;
e5c6427adb35atagen2026-10-25 10:23 UTC378
            let unread = summary.thread_unreads.get(root);
e5c6427adb35atagen2026-10-25 10:23 UTC379
            if done(unread) {
e5c6427adb35atagen2026-10-25 10:23 UTC380
                return unread.copied().unwrap_or_default();
e5c6427adb35atagen2026-10-25 10:23 UTC381
            }
e5c6427adb35atagen2026-10-25 10:23 UTC382
            support::receive_until(events, |event| matches!(event, Event::RoomsChanged)).await;
e5c6427adb35atagen2026-10-25 10:23 UTC383
        }
e5c6427adb35atagen2026-10-25 10:23 UTC384
    })
e5c6427adb35atagen2026-10-25 10:23 UTC385
    .await
e5c6427adb35atagen2026-10-25 10:23 UTC386
    .expect("thread unreads never settled")
e5c6427adb35atagen2026-10-25 10:23 UTC387
}
e7c1b037154datagen2026-10-25 13:50 UTC388
e7c1b037154datagen2026-10-25 13:50 UTC389
/// continuwuity 26.9 bundles a thread's latest reply as bare content, so nothing dates the
e7c1b037154datagen2026-10-25 13:50 UTC390
/// page's count, and the cache handing back replies the server already counted counted them
e7c1b037154datagen2026-10-25 13:50 UTC391
/// again
e7c1b037154datagen2026-10-25 13:50 UTC392
#[tokio::test]
e7c1b037154datagen2026-10-25 13:50 UTC393
async fn a_thread_list_page_keeps_the_servers_count() {
e7c1b037154datagen2026-10-25 13:50 UTC394
    let server = MatrixMockServer::new().await;
e7c1b037154datagen2026-10-25 13:50 UTC395
    server
e7c1b037154datagen2026-10-25 13:50 UTC396
        .mock_sliding_sync()
e7c1b037154datagen2026-10-25 13:50 UTC397
        .ok(v5::Response::new("1".to_owned()))
e7c1b037154datagen2026-10-25 13:50 UTC398
        .mount()
e7c1b037154datagen2026-10-25 13:50 UTC399
        .await;
e7c1b037154datagen2026-10-25 13:50 UTC400
    let client = server
e7c1b037154datagen2026-10-25 13:50 UTC401
        .client_builder()
e7c1b037154datagen2026-10-25 13:50 UTC402
        .on_builder(|builder| {
e7c1b037154datagen2026-10-25 13:50 UTC403
            builder.with_threading_support(ThreadingSupport::Enabled {
e7c1b037154datagen2026-10-25 13:50 UTC404
                with_subscriptions: false,
e7c1b037154datagen2026-10-25 13:50 UTC405
            })
e7c1b037154datagen2026-10-25 13:50 UTC406
        })
e7c1b037154datagen2026-10-25 13:50 UTC407
        .build()
e7c1b037154datagen2026-10-25 13:50 UTC408
        .await;
e7c1b037154datagen2026-10-25 13:50 UTC409
    let room_id = room_id!("!thread-list:localhost");
e7c1b037154datagen2026-10-25 13:50 UTC410
    let own = client.user_id().unwrap().to_owned();
e7c1b037154datagen2026-10-25 13:50 UTC411
    server.mock_room_state_encryption().plain().mount().await;
e7c1b037154datagen2026-10-25 13:50 UTC412
    client.event_cache().subscribe().unwrap();
e7c1b037154datagen2026-10-25 13:50 UTC413
    let factory = EventFactory::new().room(room_id).sender(&ALICE);
e7c1b037154datagen2026-10-25 13:50 UTC414
    let (root, first, second, third) = (
e7c1b037154datagen2026-10-25 13:50 UTC415
        event_id!("$list-root"),
e7c1b037154datagen2026-10-25 13:50 UTC416
        event_id!("$list-first"),
e7c1b037154datagen2026-10-25 13:50 UTC417
        event_id!("$list-second"),
e7c1b037154datagen2026-10-25 13:50 UTC418
        event_id!("$list-third"),
e7c1b037154datagen2026-10-25 13:50 UTC419
    );
e7c1b037154datagen2026-10-25 13:50 UTC420
    let reply = |event_id, previous, body| {
e7c1b037154datagen2026-10-25 13:50 UTC421
        factory
e7c1b037154datagen2026-10-25 13:50 UTC422
            .text_msg(body)
e7c1b037154datagen2026-10-25 13:50 UTC423
            .in_thread(root, previous)
e7c1b037154datagen2026-10-25 13:50 UTC424
            .event_id(event_id)
e7c1b037154datagen2026-10-25 13:50 UTC425
    };
e7c1b037154datagen2026-10-25 13:50 UTC426
    server
e7c1b037154datagen2026-10-25 13:50 UTC427
        .sync_room(
e7c1b037154datagen2026-10-25 13:50 UTC428
            &client,
e7c1b037154datagen2026-10-25 13:50 UTC429
            JoinedRoomBuilder::new(room_id)
e7c1b037154datagen2026-10-25 13:50 UTC430
                .add_state_event(EventFactory::new().room(room_id).member(&own))
e7c1b037154datagen2026-10-25 13:50 UTC431
                .add_timeline_event(factory.text_msg("root").event_id(root))
e7c1b037154datagen2026-10-25 13:50 UTC432
                .add_timeline_event(reply(first, root, "one"))
e7c1b037154datagen2026-10-25 13:50 UTC433
                .add_timeline_event(reply(second, first, "two")),
e7c1b037154datagen2026-10-25 13:50 UTC434
        )
e7c1b037154datagen2026-10-25 13:50 UTC435
        .await;
e7c1b037154datagen2026-10-25 13:50 UTC436
    let listed: Raw<AnyTimelineEvent> = serde_json::from_value(json!({
e7c1b037154datagen2026-10-25 13:50 UTC437
        "type": "m.room.message",
e7c1b037154datagen2026-10-25 13:50 UTC438
        "event_id": root,
e7c1b037154datagen2026-10-25 13:50 UTC439
        "room_id": room_id,
e7c1b037154datagen2026-10-25 13:50 UTC440
        "sender": &*ALICE,
e7c1b037154datagen2026-10-25 13:50 UTC441
        "origin_server_ts": 1,
e7c1b037154datagen2026-10-25 13:50 UTC442
        "content": { "msgtype": "m.text", "body": "root" },
e7c1b037154datagen2026-10-25 13:50 UTC443
        "unsigned": { "m.relations": { "m.thread": {
e7c1b037154datagen2026-10-25 13:50 UTC444
            "latest_event": { "msgtype": "m.text", "body": "two" },
e7c1b037154datagen2026-10-25 13:50 UTC445
            "count": 2,
e7c1b037154datagen2026-10-25 13:50 UTC446
            "current_user_participated": false,
e7c1b037154datagen2026-10-25 13:50 UTC447
        } } },
e7c1b037154datagen2026-10-25 13:50 UTC448
    }))
e7c1b037154datagen2026-10-25 13:50 UTC449
    .unwrap();
e7c1b037154datagen2026-10-25 13:50 UTC450
    server
e7c1b037154datagen2026-10-25 13:50 UTC451
        .mock_room_threads()
e7c1b037154datagen2026-10-25 13:50 UTC452
        .ok(vec![listed], None)
e7c1b037154datagen2026-10-25 13:50 UTC453
        .mount()
e7c1b037154datagen2026-10-25 13:50 UTC454
        .await;
e7c1b037154datagen2026-10-25 13:50 UTC455
    let config = EngineConfig::default().with_initial_history_limit(0);
e7c1b037154datagen2026-10-25 13:50 UTC456
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
e7c1b037154datagen2026-10-25 13:50 UTC457
    let running = tokio::spawn(engine.run());
e7c1b037154datagen2026-10-25 13:50 UTC458
e7c1b037154datagen2026-10-25 13:50 UTC459
    handle.open_thread_list(room_id.to_owned()).await.unwrap();
e7c1b037154datagen2026-10-25 13:50 UTC460
    handle
e7c1b037154datagen2026-10-25 13:50 UTC461
        .paginate_thread_list(room_id.to_owned(), 20)
e7c1b037154datagen2026-10-25 13:50 UTC462
        .await
e7c1b037154datagen2026-10-25 13:50 UTC463
        .unwrap();
e7c1b037154datagen2026-10-25 13:50 UTC464
    tokio::time::sleep(Duration::from_millis(5)).await;
e7c1b037154datagen2026-10-25 13:50 UTC465
    server
e7c1b037154datagen2026-10-25 13:50 UTC466
        .sync_room(
e7c1b037154datagen2026-10-25 13:50 UTC467
            &client,
e7c1b037154datagen2026-10-25 13:50 UTC468
            JoinedRoomBuilder::new(room_id)
e7c1b037154datagen2026-10-25 13:50 UTC469
                .add_timeline_event(reply(first, root, "one"))
e7c1b037154datagen2026-10-25 13:50 UTC470
                .add_timeline_event(reply(second, first, "two"))
e7c1b037154datagen2026-10-25 13:50 UTC471
                .add_timeline_event(
e7c1b037154datagen2026-10-25 13:50 UTC472
                    reply(third, second, "three").server_ts(MilliSecondsSinceUnixEpoch::now()),
e7c1b037154datagen2026-10-25 13:50 UTC473
                ),
e7c1b037154datagen2026-10-25 13:50 UTC474
        )
e7c1b037154datagen2026-10-25 13:50 UTC475
        .await;
e7c1b037154datagen2026-10-25 13:50 UTC476
    let changed = tokio::time::timeout(
e7c1b037154datagen2026-10-25 13:50 UTC477
        Duration::from_secs(10),
e7c1b037154datagen2026-10-25 13:50 UTC478
        support::receive_until(&mut events, |event| {
e7c1b037154datagen2026-10-25 13:50 UTC479
            matches!(event, Event::ThreadListChanged { upserts, .. }
e7c1b037154datagen2026-10-25 13:50 UTC480
                if upserts.iter().any(|entry| entry.summary.latest_event_id.as_deref() == Some(third)))
e7c1b037154datagen2026-10-25 13:50 UTC481
        }),
e7c1b037154datagen2026-10-25 13:50 UTC482
    )
e7c1b037154datagen2026-10-25 13:50 UTC483
    .await
e7c1b037154datagen2026-10-25 13:50 UTC484
    .expect("the new reply never reached the thread list");
e7c1b037154datagen2026-10-25 13:50 UTC485
    let Event::ThreadListChanged { upserts, .. } = changed else {
e7c1b037154datagen2026-10-25 13:50 UTC486
        unreachable!()
e7c1b037154datagen2026-10-25 13:50 UTC487
    };
e7c1b037154datagen2026-10-25 13:50 UTC488
    let entry = upserts.last().unwrap();
e7c1b037154datagen2026-10-25 13:50 UTC489
    assert_eq!(
e7c1b037154datagen2026-10-25 13:50 UTC490
        entry.summary.reply_count, 3,
e7c1b037154datagen2026-10-25 13:50 UTC491
        "replies the server had counted were counted again"
e7c1b037154datagen2026-10-25 13:50 UTC492
    );
e7c1b037154datagen2026-10-25 13:50 UTC493
e7c1b037154datagen2026-10-25 13:50 UTC494
    support::shut_down(&handle, &mut events, running).await;
e7c1b037154datagen2026-10-25 13:50 UTC495
}
97fb5720d881atagen2026-10-25 14:15 UTC496
97fb5720d881atagen2026-10-25 14:15 UTC497
/// with continuwuity's undated latest, the page has to date each thread from the replies the
97fb5720d881atagen2026-10-25 14:15 UTC498
/// cache already holds, or it sorts by the root
97fb5720d881atagen2026-10-25 14:15 UTC499
#[tokio::test]
97fb5720d881atagen2026-10-25 14:15 UTC500
async fn a_thread_list_page_dates_threads_from_cached_replies() {
97fb5720d881atagen2026-10-25 14:15 UTC501
    let server = MatrixMockServer::new().await;
97fb5720d881atagen2026-10-25 14:15 UTC502
    server
97fb5720d881atagen2026-10-25 14:15 UTC503
        .mock_sliding_sync()
97fb5720d881atagen2026-10-25 14:15 UTC504
        .ok(v5::Response::new("1".to_owned()))
97fb5720d881atagen2026-10-25 14:15 UTC505
        .mount()
97fb5720d881atagen2026-10-25 14:15 UTC506
        .await;
97fb5720d881atagen2026-10-25 14:15 UTC507
    let client = server
97fb5720d881atagen2026-10-25 14:15 UTC508
        .client_builder()
97fb5720d881atagen2026-10-25 14:15 UTC509
        .on_builder(|builder| {
97fb5720d881atagen2026-10-25 14:15 UTC510
            builder.with_threading_support(ThreadingSupport::Enabled {
97fb5720d881atagen2026-10-25 14:15 UTC511
                with_subscriptions: false,
97fb5720d881atagen2026-10-25 14:15 UTC512
            })
97fb5720d881atagen2026-10-25 14:15 UTC513
        })
97fb5720d881atagen2026-10-25 14:15 UTC514
        .build()
97fb5720d881atagen2026-10-25 14:15 UTC515
        .await;
97fb5720d881atagen2026-10-25 14:15 UTC516
    let room_id = room_id!("!thread-dates:localhost");
97fb5720d881atagen2026-10-25 14:15 UTC517
    let own = client.user_id().unwrap().to_owned();
97fb5720d881atagen2026-10-25 14:15 UTC518
    server.mock_room_state_encryption().plain().mount().await;
97fb5720d881atagen2026-10-25 14:15 UTC519
    client.event_cache().subscribe().unwrap();
97fb5720d881atagen2026-10-25 14:15 UTC520
    let factory = EventFactory::new().room(room_id).sender(&ALICE);
97fb5720d881atagen2026-10-25 14:15 UTC521
    let (early, first, second, late) = (
97fb5720d881atagen2026-10-25 14:15 UTC522
        event_id!("$dates-early"),
97fb5720d881atagen2026-10-25 14:15 UTC523
        event_id!("$dates-first"),
97fb5720d881atagen2026-10-25 14:15 UTC524
        event_id!("$dates-second"),
97fb5720d881atagen2026-10-25 14:15 UTC525
        event_id!("$dates-late"),
97fb5720d881atagen2026-10-25 14:15 UTC526
    );
97fb5720d881atagen2026-10-25 14:15 UTC527
    server
97fb5720d881atagen2026-10-25 14:15 UTC528
        .sync_room(
97fb5720d881atagen2026-10-25 14:15 UTC529
            &client,
97fb5720d881atagen2026-10-25 14:15 UTC530
            JoinedRoomBuilder::new(room_id)
97fb5720d881atagen2026-10-25 14:15 UTC531
                .add_state_event(EventFactory::new().room(room_id).member(&own))
97fb5720d881atagen2026-10-25 14:15 UTC532
                .add_timeline_event(factory.text_msg("early").event_id(early).server_ts(1_000))
97fb5720d881atagen2026-10-25 14:15 UTC533
                .add_timeline_event(
97fb5720d881atagen2026-10-25 14:15 UTC534
                    factory
97fb5720d881atagen2026-10-25 14:15 UTC535
                        .text_msg("one")
97fb5720d881atagen2026-10-25 14:15 UTC536
                        .in_thread(early, early)
97fb5720d881atagen2026-10-25 14:15 UTC537
                        .event_id(first)
97fb5720d881atagen2026-10-25 14:15 UTC538
                        .server_ts(5_000),
97fb5720d881atagen2026-10-25 14:15 UTC539
                )
97fb5720d881atagen2026-10-25 14:15 UTC540
                .add_timeline_event(factory.text_msg("late").event_id(late).server_ts(7_000))
97fb5720d881atagen2026-10-25 14:15 UTC541
                .add_timeline_event(
97fb5720d881atagen2026-10-25 14:15 UTC542
                    factory
97fb5720d881atagen2026-10-25 14:15 UTC543
                        .text_msg("two")
97fb5720d881atagen2026-10-25 14:15 UTC544
                        .in_thread(early, first)
97fb5720d881atagen2026-10-25 14:15 UTC545
                        .event_id(second)
97fb5720d881atagen2026-10-25 14:15 UTC546
                        .server_ts(9_000),
97fb5720d881atagen2026-10-25 14:15 UTC547
                ),
97fb5720d881atagen2026-10-25 14:15 UTC548
        )
97fb5720d881atagen2026-10-25 14:15 UTC549
        .await;
97fb5720d881atagen2026-10-25 14:15 UTC550
    let listed = |root: &EventId, timestamp: u64, count: u64| -> Raw<AnyTimelineEvent> {
97fb5720d881atagen2026-10-25 14:15 UTC551
        serde_json::from_value(json!({
97fb5720d881atagen2026-10-25 14:15 UTC552
            "type": "m.room.message",
97fb5720d881atagen2026-10-25 14:15 UTC553
            "event_id": root,
97fb5720d881atagen2026-10-25 14:15 UTC554
            "room_id": room_id,
97fb5720d881atagen2026-10-25 14:15 UTC555
            "sender": &*ALICE,
97fb5720d881atagen2026-10-25 14:15 UTC556
            "origin_server_ts": timestamp,
97fb5720d881atagen2026-10-25 14:15 UTC557
            "content": { "msgtype": "m.text", "body": "root" },
97fb5720d881atagen2026-10-25 14:15 UTC558
            "unsigned": { "m.relations": { "m.thread": {
97fb5720d881atagen2026-10-25 14:15 UTC559
                "latest_event": { "msgtype": "m.text", "body": "latest" },
97fb5720d881atagen2026-10-25 14:15 UTC560
                "count": count,
97fb5720d881atagen2026-10-25 14:15 UTC561
                "current_user_participated": false,
97fb5720d881atagen2026-10-25 14:15 UTC562
            } } },
97fb5720d881atagen2026-10-25 14:15 UTC563
        }))
97fb5720d881atagen2026-10-25 14:15 UTC564
        .unwrap()
97fb5720d881atagen2026-10-25 14:15 UTC565
    };
97fb5720d881atagen2026-10-25 14:15 UTC566
    server
97fb5720d881atagen2026-10-25 14:15 UTC567
        .mock_room_threads()
97fb5720d881atagen2026-10-25 14:15 UTC568
        .ok(vec![listed(late, 7_000, 1), listed(early, 1_000, 2)], None)
97fb5720d881atagen2026-10-25 14:15 UTC569
        .mount()
97fb5720d881atagen2026-10-25 14:15 UTC570
        .await;
97fb5720d881atagen2026-10-25 14:15 UTC571
    let config = EngineConfig::default().with_initial_history_limit(0);
97fb5720d881atagen2026-10-25 14:15 UTC572
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
97fb5720d881atagen2026-10-25 14:15 UTC573
    let running = tokio::spawn(engine.run());
97fb5720d881atagen2026-10-25 14:15 UTC574
97fb5720d881atagen2026-10-25 14:15 UTC575
    handle.open_thread_list(room_id.to_owned()).await.unwrap();
97fb5720d881atagen2026-10-25 14:15 UTC576
    handle
97fb5720d881atagen2026-10-25 14:15 UTC577
        .paginate_thread_list(room_id.to_owned(), 20)
97fb5720d881atagen2026-10-25 14:15 UTC578
        .await
97fb5720d881atagen2026-10-25 14:15 UTC579
        .unwrap();
97fb5720d881atagen2026-10-25 14:15 UTC580
    let changed = support::receive_until(&mut events, |event| {
97fb5720d881atagen2026-10-25 14:15 UTC581
        matches!(event, Event::ThreadListChanged { .. })
97fb5720d881atagen2026-10-25 14:15 UTC582
    })
97fb5720d881atagen2026-10-25 14:15 UTC583
    .await;
97fb5720d881atagen2026-10-25 14:15 UTC584
    let Event::ThreadListChanged { upserts, .. } = changed else {
97fb5720d881atagen2026-10-25 14:15 UTC585
        unreachable!()
97fb5720d881atagen2026-10-25 14:15 UTC586
    };
97fb5720d881atagen2026-10-25 14:15 UTC587
    let mut entries = macltok::imbl::Vector::new();
97fb5720d881atagen2026-10-25 14:15 UTC588
    for entry in upserts {
97fb5720d881atagen2026-10-25 14:15 UTC589
        ThreadListEntry::upsert(&mut entries, entry);
97fb5720d881atagen2026-10-25 14:15 UTC590
    }
97fb5720d881atagen2026-10-25 14:15 UTC591
    let order: Vec<_> = entries
97fb5720d881atagen2026-10-25 14:15 UTC592
        .iter()
97fb5720d881atagen2026-10-25 14:15 UTC593
        .map(|entry| (entry.root_event_id.as_str(), entry.latest_activity_ms))
97fb5720d881atagen2026-10-25 14:15 UTC594
        .collect();
97fb5720d881atagen2026-10-25 14:15 UTC595
    assert_eq!(
97fb5720d881atagen2026-10-25 14:15 UTC596
        order,
97fb5720d881atagen2026-10-25 14:15 UTC597
        [(early.as_str(), 9_000), (late.as_str(), 7_000)],
97fb5720d881atagen2026-10-25 14:15 UTC598
        "a thread was dated by its root rather than its newest cached reply"
97fb5720d881atagen2026-10-25 14:15 UTC599
    );
97fb5720d881atagen2026-10-25 14:15 UTC600
    let dated = &entries[0];
97fb5720d881atagen2026-10-25 14:15 UTC601
    assert_eq!(dated.summary.latest_event_id.as_deref(), Some(second));
97fb5720d881atagen2026-10-25 14:15 UTC602
    assert_eq!(dated.summary.latest_event_timestamp_ms, Some(9_000));
97fb5720d881atagen2026-10-25 14:15 UTC603
    assert_eq!(dated.summary.reply_count, 2);
97fb5720d881atagen2026-10-25 14:15 UTC604
97fb5720d881atagen2026-10-25 14:15 UTC605
    support::shut_down(&handle, &mut events, running).await;
97fb5720d881atagen2026-10-25 14:15 UTC606
}