atagen

macltok

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

Blame: tests/mentions_inbox.rs

cbb715e11963atagen2026-10-24 23:24 UTC1
mod support;
cbb715e11963atagen2026-10-24 23:24 UTC2
cbb715e11963atagen2026-10-24 23:24 UTC3
use std::{
cbb715e11963atagen2026-10-24 23:24 UTC4
    collections::{BTreeMap, HashSet},
cbb715e11963atagen2026-10-24 23:24 UTC5
    sync::{
cbb715e11963atagen2026-10-24 23:24 UTC6
        Arc, Mutex,
cbb715e11963atagen2026-10-24 23:24 UTC7
        atomic::{AtomicUsize, Ordering},
cbb715e11963atagen2026-10-24 23:24 UTC8
    },
cbb715e11963atagen2026-10-24 23:24 UTC9
    time::Duration,
cbb715e11963atagen2026-10-24 23:24 UTC10
};
cbb715e11963atagen2026-10-24 23:24 UTC11
cbb715e11963atagen2026-10-24 23:24 UTC12
#[cfg(feature = "sqlite")]
cbb715e11963atagen2026-10-24 23:24 UTC13
use macltok::{EncryptedSessionStore, PasswordLogin, Restore, Session, SessionStore, StoreConfig};
cbb715e11963atagen2026-10-24 23:24 UTC14
use macltok::{Engine, EngineConfig, Event, MentionsPage};
cbb715e11963atagen2026-10-24 23:24 UTC15
use matrix_sdk::{
cbb715e11963atagen2026-10-24 23:24 UTC16
    Client,
cbb715e11963atagen2026-10-24 23:24 UTC17
    ruma::{OwnedRoomId, OwnedUserId, event_id, events::Mentions, room_id, user_id},
cbb715e11963atagen2026-10-24 23:24 UTC18
    test_utils::mocks::{MatrixMockServer, RoomContextResponseTemplate},
cbb715e11963atagen2026-10-24 23:24 UTC19
};
cbb715e11963atagen2026-10-24 23:24 UTC20
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
cbb715e11963atagen2026-10-24 23:24 UTC21
use ruma::push::{EventMatchConditionData, NewConditionalPushRule, PushCondition, Ruleset};
cbb715e11963atagen2026-10-24 23:24 UTC22
use serde_json::{Value, json};
cbb715e11963atagen2026-10-24 23:24 UTC23
#[cfg(feature = "sqlite")]
cbb715e11963atagen2026-10-24 23:24 UTC24
use support::receive_until;
cbb715e11963atagen2026-10-24 23:24 UTC25
use support::shut_down;
cbb715e11963atagen2026-10-24 23:24 UTC26
use wiremock::{
cbb715e11963atagen2026-10-24 23:24 UTC27
    Mock, Request, Respond, ResponseTemplate,
cbb715e11963atagen2026-10-24 23:24 UTC28
    matchers::{method, path, path_regex, query_param},
cbb715e11963atagen2026-10-24 23:24 UTC29
};
cbb715e11963atagen2026-10-24 23:24 UTC30
cbb715e11963atagen2026-10-24 23:24 UTC31
#[cfg(feature = "serde")]
cbb715e11963atagen2026-10-24 23:24 UTC32
use macltok::{MentionCoverage, MentionsBoundary, MentionsCommand};
cbb715e11963atagen2026-10-24 23:24 UTC33
cbb715e11963atagen2026-10-24 23:24 UTC34
const SLIDING_SYNC_PATH: &str = "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync";
cbb715e11963atagen2026-10-24 23:24 UTC35
cbb715e11963atagen2026-10-24 23:24 UTC36
struct SlidingFeed {
cbb715e11963atagen2026-10-24 23:24 UTC37
    rooms: Arc<Mutex<BTreeMap<OwnedRoomId, Vec<Value>>>>,
cbb715e11963atagen2026-10-24 23:24 UTC38
    required_state: Arc<Mutex<BTreeMap<OwnedRoomId, Vec<Value>>>>,
cbb715e11963atagen2026-10-24 23:24 UTC39
    requests: Arc<AtomicUsize>,
cbb715e11963atagen2026-10-24 23:24 UTC40
}
cbb715e11963atagen2026-10-24 23:24 UTC41
cbb715e11963atagen2026-10-24 23:24 UTC42
impl SlidingFeed {
cbb715e11963atagen2026-10-24 23:24 UTC43
    fn new(room_id: OwnedRoomId, events: Vec<Value>) -> Self {
cbb715e11963atagen2026-10-24 23:24 UTC44
        Self {
cbb715e11963atagen2026-10-24 23:24 UTC45
            rooms: Arc::new(Mutex::new(BTreeMap::from([(room_id, events)]))),
cbb715e11963atagen2026-10-24 23:24 UTC46
            required_state: Arc::new(Mutex::new(BTreeMap::new())),
cbb715e11963atagen2026-10-24 23:24 UTC47
            requests: Arc::new(AtomicUsize::new(0)),
cbb715e11963atagen2026-10-24 23:24 UTC48
        }
cbb715e11963atagen2026-10-24 23:24 UTC49
    }
cbb715e11963atagen2026-10-24 23:24 UTC50
cbb715e11963atagen2026-10-24 23:24 UTC51
    fn replace(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
cbb715e11963atagen2026-10-24 23:24 UTC52
        self.rooms
cbb715e11963atagen2026-10-24 23:24 UTC53
            .lock()
cbb715e11963atagen2026-10-24 23:24 UTC54
            .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC55
            .insert(room_id.to_owned(), events);
cbb715e11963atagen2026-10-24 23:24 UTC56
    }
cbb715e11963atagen2026-10-24 23:24 UTC57
cbb715e11963atagen2026-10-24 23:24 UTC58
    fn add_room(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
cbb715e11963atagen2026-10-24 23:24 UTC59
        self.rooms
cbb715e11963atagen2026-10-24 23:24 UTC60
            .lock()
cbb715e11963atagen2026-10-24 23:24 UTC61
            .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC62
            .insert(room_id.to_owned(), events);
cbb715e11963atagen2026-10-24 23:24 UTC63
    }
cbb715e11963atagen2026-10-24 23:24 UTC64
cbb715e11963atagen2026-10-24 23:24 UTC65
    fn set_required_state(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
cbb715e11963atagen2026-10-24 23:24 UTC66
        self.required_state
cbb715e11963atagen2026-10-24 23:24 UTC67
            .lock()
cbb715e11963atagen2026-10-24 23:24 UTC68
            .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC69
            .insert(room_id.to_owned(), events);
cbb715e11963atagen2026-10-24 23:24 UTC70
    }
cbb715e11963atagen2026-10-24 23:24 UTC71
cbb715e11963atagen2026-10-24 23:24 UTC72
    async fn mount(&self, server: &MatrixMockServer) {
cbb715e11963atagen2026-10-24 23:24 UTC73
        Mock::given(method("POST"))
cbb715e11963atagen2026-10-24 23:24 UTC74
            .and(path(SLIDING_SYNC_PATH))
cbb715e11963atagen2026-10-24 23:24 UTC75
            .respond_with(self.clone())
cbb715e11963atagen2026-10-24 23:24 UTC76
            .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC77
            .await;
cbb715e11963atagen2026-10-24 23:24 UTC78
    }
cbb715e11963atagen2026-10-24 23:24 UTC79
}
cbb715e11963atagen2026-10-24 23:24 UTC80
cbb715e11963atagen2026-10-24 23:24 UTC81
impl Clone for SlidingFeed {
cbb715e11963atagen2026-10-24 23:24 UTC82
    fn clone(&self) -> Self {
cbb715e11963atagen2026-10-24 23:24 UTC83
        Self {
cbb715e11963atagen2026-10-24 23:24 UTC84
            rooms: self.rooms.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC85
            required_state: self.required_state.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC86
            requests: self.requests.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC87
        }
cbb715e11963atagen2026-10-24 23:24 UTC88
    }
cbb715e11963atagen2026-10-24 23:24 UTC89
}
cbb715e11963atagen2026-10-24 23:24 UTC90
cbb715e11963atagen2026-10-24 23:24 UTC91
impl Respond for SlidingFeed {
cbb715e11963atagen2026-10-24 23:24 UTC92
    fn respond(&self, _request: &Request) -> ResponseTemplate {
cbb715e11963atagen2026-10-24 23:24 UTC93
        let request = self.requests.fetch_add(1, Ordering::AcqRel);
cbb715e11963atagen2026-10-24 23:24 UTC94
        let required_state = self.required_state.lock().unwrap().clone();
cbb715e11963atagen2026-10-24 23:24 UTC95
        let rooms: serde_json::Map<String, Value> = self
cbb715e11963atagen2026-10-24 23:24 UTC96
            .rooms
cbb715e11963atagen2026-10-24 23:24 UTC97
            .lock()
cbb715e11963atagen2026-10-24 23:24 UTC98
            .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC99
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC100
            .map(|(room_id, events)| {
cbb715e11963atagen2026-10-24 23:24 UTC101
                (
cbb715e11963atagen2026-10-24 23:24 UTC102
                    room_id.to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC103
                    json!({
cbb715e11963atagen2026-10-24 23:24 UTC104
                        "initial": request == 0,
cbb715e11963atagen2026-10-24 23:24 UTC105
                        "required_state": required_state
cbb715e11963atagen2026-10-24 23:24 UTC106
                            .get(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC107
                            .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC108
                            .unwrap_or_default(),
cbb715e11963atagen2026-10-24 23:24 UTC109
                        "timeline": events,
cbb715e11963atagen2026-10-24 23:24 UTC110
                    }),
cbb715e11963atagen2026-10-24 23:24 UTC111
                )
cbb715e11963atagen2026-10-24 23:24 UTC112
            })
cbb715e11963atagen2026-10-24 23:24 UTC113
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC114
        ResponseTemplate::new(200)
cbb715e11963atagen2026-10-24 23:24 UTC115
            .set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC116
                "txn_id": null,
cbb715e11963atagen2026-10-24 23:24 UTC117
                "pos": format!("mentions-{request}"),
cbb715e11963atagen2026-10-24 23:24 UTC118
                "lists": {},
cbb715e11963atagen2026-10-24 23:24 UTC119
                "rooms": rooms,
cbb715e11963atagen2026-10-24 23:24 UTC120
                "extensions": {},
cbb715e11963atagen2026-10-24 23:24 UTC121
            }))
cbb715e11963atagen2026-10-24 23:24 UTC122
            .set_delay(Duration::from_millis(20))
cbb715e11963atagen2026-10-24 23:24 UTC123
    }
cbb715e11963atagen2026-10-24 23:24 UTC124
}
cbb715e11963atagen2026-10-24 23:24 UTC125
cbb715e11963atagen2026-10-24 23:24 UTC126
async fn joined_client(server: &MatrixMockServer, room_id: &ruma::RoomId) -> (Client, OwnedUserId) {
cbb715e11963atagen2026-10-24 23:24 UTC127
    let client = server.client_builder().build().await;
cbb715e11963atagen2026-10-24 23:24 UTC128
    let own_user = client.user_id().unwrap().to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC129
    let room_id = room_id.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC130
    server
cbb715e11963atagen2026-10-24 23:24 UTC131
        .mock_sync()
cbb715e11963atagen2026-10-24 23:24 UTC132
        .ok_and_run(&client, {
cbb715e11963atagen2026-10-24 23:24 UTC133
            let own_user = own_user.clone();
cbb715e11963atagen2026-10-24 23:24 UTC134
            move |builder| {
cbb715e11963atagen2026-10-24 23:24 UTC135
                let factory = EventFactory::new().room(&room_id);
cbb715e11963atagen2026-10-24 23:24 UTC136
                builder.add_global_account_data(
cbb715e11963atagen2026-10-24 23:24 UTC137
                    factory.push_rules(Ruleset::server_default(&own_user)),
cbb715e11963atagen2026-10-24 23:24 UTC138
                );
cbb715e11963atagen2026-10-24 23:24 UTC139
                builder.add_joined_room(
cbb715e11963atagen2026-10-24 23:24 UTC140
                    JoinedRoomBuilder::new(&room_id).add_state_event(factory.member(&own_user)),
cbb715e11963atagen2026-10-24 23:24 UTC141
                );
cbb715e11963atagen2026-10-24 23:24 UTC142
            }
cbb715e11963atagen2026-10-24 23:24 UTC143
        })
cbb715e11963atagen2026-10-24 23:24 UTC144
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC145
    (client, own_user)
cbb715e11963atagen2026-10-24 23:24 UTC146
}
cbb715e11963atagen2026-10-24 23:24 UTC147
cbb715e11963atagen2026-10-24 23:24 UTC148
fn message(
cbb715e11963atagen2026-10-24 23:24 UTC149
    event_id: &str,
cbb715e11963atagen2026-10-24 23:24 UTC150
    sender: &str,
cbb715e11963atagen2026-10-24 23:24 UTC151
    timestamp: u64,
cbb715e11963atagen2026-10-24 23:24 UTC152
    body: &str,
cbb715e11963atagen2026-10-24 23:24 UTC153
    mentioned: Option<&str>,
cbb715e11963atagen2026-10-24 23:24 UTC154
) -> Value {
cbb715e11963atagen2026-10-24 23:24 UTC155
    let mut content = json!({ "msgtype": "m.text", "body": body });
cbb715e11963atagen2026-10-24 23:24 UTC156
    if let Some(mentioned) = mentioned {
cbb715e11963atagen2026-10-24 23:24 UTC157
        content["m.mentions"] = json!({ "user_ids": [mentioned] });
cbb715e11963atagen2026-10-24 23:24 UTC158
    }
cbb715e11963atagen2026-10-24 23:24 UTC159
    json!({
cbb715e11963atagen2026-10-24 23:24 UTC160
        "type": "m.room.message",
cbb715e11963atagen2026-10-24 23:24 UTC161
        "event_id": event_id,
cbb715e11963atagen2026-10-24 23:24 UTC162
        "sender": sender,
cbb715e11963atagen2026-10-24 23:24 UTC163
        "origin_server_ts": timestamp,
cbb715e11963atagen2026-10-24 23:24 UTC164
        "content": content,
cbb715e11963atagen2026-10-24 23:24 UTC165
    })
cbb715e11963atagen2026-10-24 23:24 UTC166
}
cbb715e11963atagen2026-10-24 23:24 UTC167
cbb715e11963atagen2026-10-24 23:24 UTC168
fn room_mention(event_id: &str, sender: &str, timestamp: u64) -> Value {
cbb715e11963atagen2026-10-24 23:24 UTC169
    let mut event = message(event_id, sender, timestamp, "room mention", None);
cbb715e11963atagen2026-10-24 23:24 UTC170
    event["content"]["m.mentions"] = json!({ "room": true });
cbb715e11963atagen2026-10-24 23:24 UTC171
    event
cbb715e11963atagen2026-10-24 23:24 UTC172
}
cbb715e11963atagen2026-10-24 23:24 UTC173
cbb715e11963atagen2026-10-24 23:24 UTC174
async fn mentions_page(
cbb715e11963atagen2026-10-24 23:24 UTC175
    events: &mut macltok::EventStream,
cbb715e11963atagen2026-10-24 23:24 UTC176
    room_id: &ruma::RoomId,
cbb715e11963atagen2026-10-24 23:24 UTC177
    predicate: impl Fn(&MentionsPage) -> bool,
cbb715e11963atagen2026-10-24 23:24 UTC178
) -> MentionsPage {
cbb715e11963atagen2026-10-24 23:24 UTC179
    mentions_page_with_diagnostics(events, room_id, predicate, None).await
cbb715e11963atagen2026-10-24 23:24 UTC180
}
cbb715e11963atagen2026-10-24 23:24 UTC181
cbb715e11963atagen2026-10-24 23:24 UTC182
async fn mentions_page_with_diagnostics(
cbb715e11963atagen2026-10-24 23:24 UTC183
    events: &mut macltok::EventStream,
cbb715e11963atagen2026-10-24 23:24 UTC184
    room_id: &ruma::RoomId,
cbb715e11963atagen2026-10-24 23:24 UTC185
    predicate: impl Fn(&MentionsPage) -> bool,
cbb715e11963atagen2026-10-24 23:24 UTC186
    diagnostic_server: Option<&MatrixMockServer>,
cbb715e11963atagen2026-10-24 23:24 UTC187
) -> MentionsPage {
cbb715e11963atagen2026-10-24 23:24 UTC188
    let mut last_page = None;
cbb715e11963atagen2026-10-24 23:24 UTC189
    let result = tokio::time::timeout(Duration::from_secs(5), async {
cbb715e11963atagen2026-10-24 23:24 UTC190
        while let Some(event) = events.recv().await {
cbb715e11963atagen2026-10-24 23:24 UTC191
            match event {
cbb715e11963atagen2026-10-24 23:24 UTC192
                Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id => {
cbb715e11963atagen2026-10-24 23:24 UTC193
                    panic!("the unopened room produced a timeline reset");
cbb715e11963atagen2026-10-24 23:24 UTC194
                }
cbb715e11963atagen2026-10-24 23:24 UTC195
                Event::MentionsChanged(update) => {
cbb715e11963atagen2026-10-24 23:24 UTC196
                    let snapshot = update.snapshot;
cbb715e11963atagen2026-10-24 23:24 UTC197
                    let matched = predicate(&snapshot);
cbb715e11963atagen2026-10-24 23:24 UTC198
                    last_page = Some(snapshot.clone());
cbb715e11963atagen2026-10-24 23:24 UTC199
                    if matched {
cbb715e11963atagen2026-10-24 23:24 UTC200
                        return snapshot;
cbb715e11963atagen2026-10-24 23:24 UTC201
                    }
cbb715e11963atagen2026-10-24 23:24 UTC202
                }
cbb715e11963atagen2026-10-24 23:24 UTC203
                _ => {}
cbb715e11963atagen2026-10-24 23:24 UTC204
            }
cbb715e11963atagen2026-10-24 23:24 UTC205
        }
cbb715e11963atagen2026-10-24 23:24 UTC206
        panic!("event stream closed before the expected mentions page");
cbb715e11963atagen2026-10-24 23:24 UTC207
    })
cbb715e11963atagen2026-10-24 23:24 UTC208
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC209
cbb715e11963atagen2026-10-24 23:24 UTC210
    match result {
cbb715e11963atagen2026-10-24 23:24 UTC211
        Ok(page) => page,
cbb715e11963atagen2026-10-24 23:24 UTC212
        Err(_) => {
cbb715e11963atagen2026-10-24 23:24 UTC213
            let last_state = last_page.map(|page: MentionsPage| {
cbb715e11963atagen2026-10-24 23:24 UTC214
                (
cbb715e11963atagen2026-10-24 23:24 UTC215
                    page.revision,
cbb715e11963atagen2026-10-24 23:24 UTC216
                    page.unread_count,
cbb715e11963atagen2026-10-24 23:24 UTC217
                    page.entries
cbb715e11963atagen2026-10-24 23:24 UTC218
                        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC219
                        .map(|entry| {
cbb715e11963atagen2026-10-24 23:24 UTC220
                            (
cbb715e11963atagen2026-10-24 23:24 UTC221
                                entry.key.room_id.to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC222
                                entry.key.event_id.to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC223
                                entry.capture.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC224
                                entry.revision,
cbb715e11963atagen2026-10-24 23:24 UTC225
                            )
cbb715e11963atagen2026-10-24 23:24 UTC226
                        })
cbb715e11963atagen2026-10-24 23:24 UTC227
                        .collect::<Vec<_>>(),
cbb715e11963atagen2026-10-24 23:24 UTC228
                )
cbb715e11963atagen2026-10-24 23:24 UTC229
            });
cbb715e11963atagen2026-10-24 23:24 UTC230
            let requests = if let Some(server) = diagnostic_server {
cbb715e11963atagen2026-10-24 23:24 UTC231
                server.server().received_requests().await.map(|requests| {
cbb715e11963atagen2026-10-24 23:24 UTC232
                    requests
cbb715e11963atagen2026-10-24 23:24 UTC233
                        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC234
                        .filter(|request| {
cbb715e11963atagen2026-10-24 23:24 UTC235
                            let path = request.url.path();
cbb715e11963atagen2026-10-24 23:24 UTC236
                            path.contains(room_id.as_str())
cbb715e11963atagen2026-10-24 23:24 UTC237
                                && (path.contains("/context/") || path.ends_with("/messages"))
cbb715e11963atagen2026-10-24 23:24 UTC238
                        })
cbb715e11963atagen2026-10-24 23:24 UTC239
                        .map(|request| format!("{} {}", request.method, request.url))
cbb715e11963atagen2026-10-24 23:24 UTC240
                        .collect::<Vec<_>>()
cbb715e11963atagen2026-10-24 23:24 UTC241
                })
cbb715e11963atagen2026-10-24 23:24 UTC242
            } else {
cbb715e11963atagen2026-10-24 23:24 UTC243
                None
cbb715e11963atagen2026-10-24 23:24 UTC244
            };
cbb715e11963atagen2026-10-24 23:24 UTC245
            panic!(
cbb715e11963atagen2026-10-24 23:24 UTC246
                "expected mentions update did not arrive, last state {last_state:?}, requests {requests:?}"
cbb715e11963atagen2026-10-24 23:24 UTC247
            );
cbb715e11963atagen2026-10-24 23:24 UTC248
        }
cbb715e11963atagen2026-10-24 23:24 UTC249
    }
cbb715e11963atagen2026-10-24 23:24 UTC250
}
cbb715e11963atagen2026-10-24 23:24 UTC251
cbb715e11963atagen2026-10-24 23:24 UTC252
fn entry_ids(page: &MentionsPage) -> HashSet<String> {
cbb715e11963atagen2026-10-24 23:24 UTC253
    page.entries
cbb715e11963atagen2026-10-24 23:24 UTC254
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC255
        .map(|entry| entry.key.event_id.to_string())
cbb715e11963atagen2026-10-24 23:24 UTC256
        .collect()
cbb715e11963atagen2026-10-24 23:24 UTC257
}
cbb715e11963atagen2026-10-24 23:24 UTC258
cbb715e11963atagen2026-10-24 23:24 UTC259
async fn wait_for_requests(feed: &SlidingFeed, minimum: usize) {
cbb715e11963atagen2026-10-24 23:24 UTC260
    tokio::time::timeout(Duration::from_secs(5), async {
cbb715e11963atagen2026-10-24 23:24 UTC261
        while feed.requests.load(Ordering::Acquire) < minimum {
cbb715e11963atagen2026-10-24 23:24 UTC262
            tokio::task::yield_now().await;
cbb715e11963atagen2026-10-24 23:24 UTC263
        }
cbb715e11963atagen2026-10-24 23:24 UTC264
    })
cbb715e11963atagen2026-10-24 23:24 UTC265
    .await
cbb715e11963atagen2026-10-24 23:24 UTC266
    .expect("the sliding sync feed did not receive another request");
cbb715e11963atagen2026-10-24 23:24 UTC267
}
cbb715e11963atagen2026-10-24 23:24 UTC268
cbb715e11963atagen2026-10-24 23:24 UTC269
async fn mount_messages_page(
cbb715e11963atagen2026-10-24 23:24 UTC270
    server: &MatrixMockServer,
cbb715e11963atagen2026-10-24 23:24 UTC271
    room_id: &ruma::RoomId,
cbb715e11963atagen2026-10-24 23:24 UTC272
    direction: &str,
cbb715e11963atagen2026-10-24 23:24 UTC273
    token: &str,
cbb715e11963atagen2026-10-24 23:24 UTC274
    limit: u16,
cbb715e11963atagen2026-10-24 23:24 UTC275
) {
cbb715e11963atagen2026-10-24 23:24 UTC276
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC277
        .and(path_regex(format!(
cbb715e11963atagen2026-10-24 23:24 UTC278
            r"^/_matrix/client/v3/rooms/{room_id}/messages$"
cbb715e11963atagen2026-10-24 23:24 UTC279
        )))
cbb715e11963atagen2026-10-24 23:24 UTC280
        .and(query_param("dir", direction))
cbb715e11963atagen2026-10-24 23:24 UTC281
        .and(query_param("from", token))
cbb715e11963atagen2026-10-24 23:24 UTC282
        .and(move |request: &Request| {
cbb715e11963atagen2026-10-24 23:24 UTC283
            request
cbb715e11963atagen2026-10-24 23:24 UTC284
                .url
cbb715e11963atagen2026-10-24 23:24 UTC285
                .query_pairs()
cbb715e11963atagen2026-10-24 23:24 UTC286
                .find(|(key, _)| key == "limit")
cbb715e11963atagen2026-10-24 23:24 UTC287
                .map_or(limit == 10, |(_, value)| value == limit.to_string())
cbb715e11963atagen2026-10-24 23:24 UTC288
        })
cbb715e11963atagen2026-10-24 23:24 UTC289
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC290
            "start": token,
cbb715e11963atagen2026-10-24 23:24 UTC291
            "end": format!("{token}-next"),
cbb715e11963atagen2026-10-24 23:24 UTC292
            "chunk": [],
cbb715e11963atagen2026-10-24 23:24 UTC293
            "state": [],
cbb715e11963atagen2026-10-24 23:24 UTC294
        })))
cbb715e11963atagen2026-10-24 23:24 UTC295
        .expect(1)
cbb715e11963atagen2026-10-24 23:24 UTC296
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC297
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC298
}
cbb715e11963atagen2026-10-24 23:24 UTC299
cbb715e11963atagen2026-10-24 23:24 UTC300
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC301
async fn unopened_room_mentions_are_deduplicated_and_read_through_uses_a_revision_boundary() {
cbb715e11963atagen2026-10-24 23:24 UTC302
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC303
    let room_id = room_id!("!mentions-inbox:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC304
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC305
    let sender = user_id!("@alice:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC306
    let initial = vec![
cbb715e11963atagen2026-10-24 23:24 UTC307
        message(
cbb715e11963atagen2026-10-24 23:24 UTC308
            "$mention",
cbb715e11963atagen2026-10-24 23:24 UTC309
            sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC310
            2_000,
cbb715e11963atagen2026-10-24 23:24 UTC311
            "direct mention",
cbb715e11963atagen2026-10-24 23:24 UTC312
            Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC313
        ),
cbb715e11963atagen2026-10-24 23:24 UTC314
        message(
cbb715e11963atagen2026-10-24 23:24 UTC315
            "$own-mention",
cbb715e11963atagen2026-10-24 23:24 UTC316
            own_user.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC317
            2_100,
cbb715e11963atagen2026-10-24 23:24 UTC318
            "own event",
cbb715e11963atagen2026-10-24 23:24 UTC319
            Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC320
        ),
cbb715e11963atagen2026-10-24 23:24 UTC321
        message(
cbb715e11963atagen2026-10-24 23:24 UTC322
            "$unrelated",
cbb715e11963atagen2026-10-24 23:24 UTC323
            sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC324
            2_200,
cbb715e11963atagen2026-10-24 23:24 UTC325
            "ordinary message",
cbb715e11963atagen2026-10-24 23:24 UTC326
            None,
cbb715e11963atagen2026-10-24 23:24 UTC327
        ),
cbb715e11963atagen2026-10-24 23:24 UTC328
    ];
cbb715e11963atagen2026-10-24 23:24 UTC329
    let feed = SlidingFeed::new(room_id.to_owned(), initial.clone());
cbb715e11963atagen2026-10-24 23:24 UTC330
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC331
cbb715e11963atagen2026-10-24 23:24 UTC332
    let (engine, handle, mut events) = Engine::from_client(
cbb715e11963atagen2026-10-24 23:24 UTC333
        client,
cbb715e11963atagen2026-10-24 23:24 UTC334
        EngineConfig::default().with_initial_history_limit(0),
cbb715e11963atagen2026-10-24 23:24 UTC335
    )
cbb715e11963atagen2026-10-24 23:24 UTC336
    .await
cbb715e11963atagen2026-10-24 23:24 UTC337
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC338
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC339
cbb715e11963atagen2026-10-24 23:24 UTC340
    let first = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
cbb715e11963atagen2026-10-24 23:24 UTC341
    assert_eq!(entry_ids(&first), HashSet::from(["$mention".to_owned()]));
cbb715e11963atagen2026-10-24 23:24 UTC342
    assert_eq!(first.unread_count, 1);
cbb715e11963atagen2026-10-24 23:24 UTC343
    let first_boundary = first.boundary.clone();
cbb715e11963atagen2026-10-24 23:24 UTC344
cbb715e11963atagen2026-10-24 23:24 UTC345
    wait_for_requests(&feed, 3).await;
cbb715e11963atagen2026-10-24 23:24 UTC346
    let replayed = handle.mentions_page(None, 50).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC347
    assert_eq!(entry_ids(&replayed), HashSet::from(["$mention".to_owned()]));
cbb715e11963atagen2026-10-24 23:24 UTC348
cbb715e11963atagen2026-10-24 23:24 UTC349
    let key = replayed.entries[0].key.clone();
cbb715e11963atagen2026-10-24 23:24 UTC350
    handle
cbb715e11963atagen2026-10-24 23:24 UTC351
        .set_mentions_read(vec![key.clone()], true)
cbb715e11963atagen2026-10-24 23:24 UTC352
        .await
cbb715e11963atagen2026-10-24 23:24 UTC353
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC354
    let read = handle.mentions_page(None, 50).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC355
    assert_eq!(read.unread_count, 0);
cbb715e11963atagen2026-10-24 23:24 UTC356
cbb715e11963atagen2026-10-24 23:24 UTC357
    feed.replace(
cbb715e11963atagen2026-10-24 23:24 UTC358
        room_id,
cbb715e11963atagen2026-10-24 23:24 UTC359
        [
cbb715e11963atagen2026-10-24 23:24 UTC360
            initial,
cbb715e11963atagen2026-10-24 23:24 UTC361
            vec![message(
cbb715e11963atagen2026-10-24 23:24 UTC362
                "$older-mention",
cbb715e11963atagen2026-10-24 23:24 UTC363
                sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC364
                1_000,
cbb715e11963atagen2026-10-24 23:24 UTC365
                "older direct mention",
cbb715e11963atagen2026-10-24 23:24 UTC366
                Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC367
            )],
cbb715e11963atagen2026-10-24 23:24 UTC368
        ]
cbb715e11963atagen2026-10-24 23:24 UTC369
        .concat(),
cbb715e11963atagen2026-10-24 23:24 UTC370
    );
cbb715e11963atagen2026-10-24 23:24 UTC371
    let updated = mentions_page(&mut events, room_id, |page| page.entries.len() == 2).await;
cbb715e11963atagen2026-10-24 23:24 UTC372
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC373
        entry_ids(&updated),
cbb715e11963atagen2026-10-24 23:24 UTC374
        HashSet::from(["$mention".to_owned(), "$older-mention".to_owned(),])
cbb715e11963atagen2026-10-24 23:24 UTC375
    );
cbb715e11963atagen2026-10-24 23:24 UTC376
cbb715e11963atagen2026-10-24 23:24 UTC377
    handle
cbb715e11963atagen2026-10-24 23:24 UTC378
        .mark_mentions_read_through(first_boundary)
cbb715e11963atagen2026-10-24 23:24 UTC379
        .await
cbb715e11963atagen2026-10-24 23:24 UTC380
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC381
    let after_boundary = handle.mentions_page(None, 50).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC382
    let read_states: Vec<_> = after_boundary
cbb715e11963atagen2026-10-24 23:24 UTC383
        .entries
cbb715e11963atagen2026-10-24 23:24 UTC384
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC385
        .map(|entry| (entry.key.event_id.to_string(), entry.read))
cbb715e11963atagen2026-10-24 23:24 UTC386
        .collect();
cbb715e11963atagen2026-10-24 23:24 UTC387
    assert!(read_states.contains(&("$mention".to_owned(), true)));
cbb715e11963atagen2026-10-24 23:24 UTC388
    assert!(read_states.contains(&("$older-mention".to_owned(), false)));
cbb715e11963atagen2026-10-24 23:24 UTC389
    assert_eq!(after_boundary.unread_count, 1);
cbb715e11963atagen2026-10-24 23:24 UTC390
cbb715e11963atagen2026-10-24 23:24 UTC391
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC392
}
cbb715e11963atagen2026-10-24 23:24 UTC393
cbb715e11963atagen2026-10-24 23:24 UTC394
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC395
async fn edits_can_remove_and_restore_a_mention_until_redaction_removes_its_target() {
cbb715e11963atagen2026-10-24 23:24 UTC396
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC397
    let room_id = room_id!("!mentions-edits:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC398
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC399
    let sender = user_id!("@alice:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC400
    let original = message(
cbb715e11963atagen2026-10-24 23:24 UTC401
        "$edited-target",
cbb715e11963atagen2026-10-24 23:24 UTC402
        sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC403
        1_000,
cbb715e11963atagen2026-10-24 23:24 UTC404
        "original mention",
cbb715e11963atagen2026-10-24 23:24 UTC405
        Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC406
    );
cbb715e11963atagen2026-10-24 23:24 UTC407
    let removal = json!({
cbb715e11963atagen2026-10-24 23:24 UTC408
        "type": "m.room.message",
cbb715e11963atagen2026-10-24 23:24 UTC409
        "event_id": "$remove-edit",
cbb715e11963atagen2026-10-24 23:24 UTC410
        "sender": sender,
cbb715e11963atagen2026-10-24 23:24 UTC411
        "origin_server_ts": 2_000,
cbb715e11963atagen2026-10-24 23:24 UTC412
        "content": {
cbb715e11963atagen2026-10-24 23:24 UTC413
            "msgtype": "m.text",
cbb715e11963atagen2026-10-24 23:24 UTC414
            "body": "* edited without a mention",
cbb715e11963atagen2026-10-24 23:24 UTC415
            "m.relates_to": { "rel_type": "m.replace", "event_id": "$edited-target" },
cbb715e11963atagen2026-10-24 23:24 UTC416
            "m.new_content": { "msgtype": "m.text", "body": "edited without a mention" },
cbb715e11963atagen2026-10-24 23:24 UTC417
        },
cbb715e11963atagen2026-10-24 23:24 UTC418
    });
cbb715e11963atagen2026-10-24 23:24 UTC419
    let addition = json!({
cbb715e11963atagen2026-10-24 23:24 UTC420
        "type": "m.room.message",
cbb715e11963atagen2026-10-24 23:24 UTC421
        "event_id": "$add-edit",
cbb715e11963atagen2026-10-24 23:24 UTC422
        "sender": sender,
cbb715e11963atagen2026-10-24 23:24 UTC423
        "origin_server_ts": 3_000,
cbb715e11963atagen2026-10-24 23:24 UTC424
        "content": {
cbb715e11963atagen2026-10-24 23:24 UTC425
            "msgtype": "m.text",
cbb715e11963atagen2026-10-24 23:24 UTC426
            "body": "* edited to mention you",
cbb715e11963atagen2026-10-24 23:24 UTC427
            "m.relates_to": { "rel_type": "m.replace", "event_id": "$edited-target" },
cbb715e11963atagen2026-10-24 23:24 UTC428
            "m.new_content": {
cbb715e11963atagen2026-10-24 23:24 UTC429
                "msgtype": "m.text",
cbb715e11963atagen2026-10-24 23:24 UTC430
                "body": "edited to mention you",
cbb715e11963atagen2026-10-24 23:24 UTC431
                "m.mentions": { "user_ids": [own_user.as_str()] },
cbb715e11963atagen2026-10-24 23:24 UTC432
            },
cbb715e11963atagen2026-10-24 23:24 UTC433
        },
cbb715e11963atagen2026-10-24 23:24 UTC434
    });
cbb715e11963atagen2026-10-24 23:24 UTC435
    let redaction = json!({
cbb715e11963atagen2026-10-24 23:24 UTC436
        "type": "m.room.redaction",
cbb715e11963atagen2026-10-24 23:24 UTC437
        "event_id": "$redaction",
cbb715e11963atagen2026-10-24 23:24 UTC438
        "sender": sender,
cbb715e11963atagen2026-10-24 23:24 UTC439
        "origin_server_ts": 4_000,
cbb715e11963atagen2026-10-24 23:24 UTC440
        "redacts": "$edited-target",
cbb715e11963atagen2026-10-24 23:24 UTC441
        "content": {},
cbb715e11963atagen2026-10-24 23:24 UTC442
    });
cbb715e11963atagen2026-10-24 23:24 UTC443
    let feed = SlidingFeed::new(room_id.to_owned(), vec![original.clone()]);
cbb715e11963atagen2026-10-24 23:24 UTC444
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC445
cbb715e11963atagen2026-10-24 23:24 UTC446
    let (engine, handle, mut events) = Engine::from_client(
cbb715e11963atagen2026-10-24 23:24 UTC447
        client,
cbb715e11963atagen2026-10-24 23:24 UTC448
        EngineConfig::default().with_initial_history_limit(0),
cbb715e11963atagen2026-10-24 23:24 UTC449
    )
cbb715e11963atagen2026-10-24 23:24 UTC450
    .await
cbb715e11963atagen2026-10-24 23:24 UTC451
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC452
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC453
cbb715e11963atagen2026-10-24 23:24 UTC454
    let discovered = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
cbb715e11963atagen2026-10-24 23:24 UTC455
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC456
        entry_ids(&discovered),
cbb715e11963atagen2026-10-24 23:24 UTC457
        HashSet::from(["$edited-target".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC458
    );
cbb715e11963atagen2026-10-24 23:24 UTC459
cbb715e11963atagen2026-10-24 23:24 UTC460
    feed.replace(room_id, vec![original.clone(), removal.clone()]);
cbb715e11963atagen2026-10-24 23:24 UTC461
    let removed = mentions_page(&mut events, room_id, |page| {
cbb715e11963atagen2026-10-24 23:24 UTC462
        page.revision > discovered.revision && page.entries.is_empty()
cbb715e11963atagen2026-10-24 23:24 UTC463
    })
cbb715e11963atagen2026-10-24 23:24 UTC464
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC465
    assert!(removed.entries.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC466
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC467
        handle
cbb715e11963atagen2026-10-24 23:24 UTC468
            .mention_context(room_id, ruma::event_id!("$edited-target"))
cbb715e11963atagen2026-10-24 23:24 UTC469
            .await
cbb715e11963atagen2026-10-24 23:24 UTC470
            .unwrap_err()
cbb715e11963atagen2026-10-24 23:24 UTC471
            .kind,
cbb715e11963atagen2026-10-24 23:24 UTC472
        macltok::ErrorKind::EventNotFound
cbb715e11963atagen2026-10-24 23:24 UTC473
    );
cbb715e11963atagen2026-10-24 23:24 UTC474
cbb715e11963atagen2026-10-24 23:24 UTC475
    feed.replace(room_id, vec![original.clone(), removal, addition]);
cbb715e11963atagen2026-10-24 23:24 UTC476
    let restored = mentions_page(&mut events, room_id, |page| {
cbb715e11963atagen2026-10-24 23:24 UTC477
        page.revision > removed.revision && entry_ids(page).contains("$edited-target")
cbb715e11963atagen2026-10-24 23:24 UTC478
    })
cbb715e11963atagen2026-10-24 23:24 UTC479
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC480
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC481
        entry_ids(&restored),
cbb715e11963atagen2026-10-24 23:24 UTC482
        HashSet::from(["$edited-target".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC483
    );
cbb715e11963atagen2026-10-24 23:24 UTC484
cbb715e11963atagen2026-10-24 23:24 UTC485
    feed.replace(room_id, vec![original, redaction]);
cbb715e11963atagen2026-10-24 23:24 UTC486
    let redacted = mentions_page(&mut events, room_id, |page| {
cbb715e11963atagen2026-10-24 23:24 UTC487
        page.revision > restored.revision && page.entries.is_empty()
cbb715e11963atagen2026-10-24 23:24 UTC488
    })
cbb715e11963atagen2026-10-24 23:24 UTC489
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC490
    assert!(redacted.entries.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC491
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC492
        handle
cbb715e11963atagen2026-10-24 23:24 UTC493
            .mention_context(room_id, ruma::event_id!("$edited-target"))
cbb715e11963atagen2026-10-24 23:24 UTC494
            .await
cbb715e11963atagen2026-10-24 23:24 UTC495
            .unwrap_err()
cbb715e11963atagen2026-10-24 23:24 UTC496
            .kind,
cbb715e11963atagen2026-10-24 23:24 UTC497
        macltok::ErrorKind::EventNotFound
cbb715e11963atagen2026-10-24 23:24 UTC498
    );
cbb715e11963atagen2026-10-24 23:24 UTC499
cbb715e11963atagen2026-10-24 23:24 UTC500
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC501
}
cbb715e11963atagen2026-10-24 23:24 UTC502
cbb715e11963atagen2026-10-24 23:24 UTC503
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC504
async fn room_mentions_require_power_while_muted_direct_mentions_remain_and_ignored_senders_drop() {
cbb715e11963atagen2026-10-24 23:24 UTC505
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC506
    let client = server.client_builder().build().await;
cbb715e11963atagen2026-10-24 23:24 UTC507
    let own_user = client.user_id().unwrap().to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC508
    let room_mention_room = room_id!("!room-mention-policy:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC509
    let muted_room = room_id!("!muted-mention-policy:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC510
    let ignored_sender = user_id!("@ignored:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC511
    let sender = user_id!("@alice:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC512
    let mut rules = Ruleset::server_default(&own_user);
cbb715e11963atagen2026-10-24 23:24 UTC513
    rules.override_.shift_insert(
cbb715e11963atagen2026-10-24 23:24 UTC514
        1,
cbb715e11963atagen2026-10-24 23:24 UTC515
        NewConditionalPushRule::new(
cbb715e11963atagen2026-10-24 23:24 UTC516
            "mute-inbox-room".to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC517
            vec![PushCondition::EventMatch(EventMatchConditionData::new(
cbb715e11963atagen2026-10-24 23:24 UTC518
                "room_id".to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC519
                muted_room.to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC520
            ))],
cbb715e11963atagen2026-10-24 23:24 UTC521
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC522
        )
cbb715e11963atagen2026-10-24 23:24 UTC523
        .into(),
cbb715e11963atagen2026-10-24 23:24 UTC524
    );
cbb715e11963atagen2026-10-24 23:24 UTC525
    server
cbb715e11963atagen2026-10-24 23:24 UTC526
        .mock_sync()
cbb715e11963atagen2026-10-24 23:24 UTC527
        .ok_and_run(&client, {
cbb715e11963atagen2026-10-24 23:24 UTC528
            let own_user = own_user.clone();
cbb715e11963atagen2026-10-24 23:24 UTC529
            let ignored_sender = ignored_sender.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC530
            move |builder| {
cbb715e11963atagen2026-10-24 23:24 UTC531
                let factory = EventFactory::new();
cbb715e11963atagen2026-10-24 23:24 UTC532
                builder.add_global_account_data(factory.push_rules(rules));
cbb715e11963atagen2026-10-24 23:24 UTC533
                builder.add_global_account_data(factory.ignored_user_list([ignored_sender]));
cbb715e11963atagen2026-10-24 23:24 UTC534
                builder.add_joined_room(
cbb715e11963atagen2026-10-24 23:24 UTC535
                    JoinedRoomBuilder::new(room_mention_room)
cbb715e11963atagen2026-10-24 23:24 UTC536
                        .add_state_event(
cbb715e11963atagen2026-10-24 23:24 UTC537
                            EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC538
                                .room(room_mention_room)
cbb715e11963atagen2026-10-24 23:24 UTC539
                                .member(&own_user),
cbb715e11963atagen2026-10-24 23:24 UTC540
                        )
cbb715e11963atagen2026-10-24 23:24 UTC541
                        .add_state_event(
cbb715e11963atagen2026-10-24 23:24 UTC542
                            EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC543
                                .room(room_mention_room)
cbb715e11963atagen2026-10-24 23:24 UTC544
                                .default_power_levels()
cbb715e11963atagen2026-10-24 23:24 UTC545
                                .sender(&own_user),
cbb715e11963atagen2026-10-24 23:24 UTC546
                        ),
cbb715e11963atagen2026-10-24 23:24 UTC547
                );
cbb715e11963atagen2026-10-24 23:24 UTC548
                builder.add_joined_room(
cbb715e11963atagen2026-10-24 23:24 UTC549
                    JoinedRoomBuilder::new(muted_room)
cbb715e11963atagen2026-10-24 23:24 UTC550
                        .add_state_event(EventFactory::new().room(muted_room).member(&own_user))
cbb715e11963atagen2026-10-24 23:24 UTC551
                        .add_state_event(
cbb715e11963atagen2026-10-24 23:24 UTC552
                            EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC553
                                .room(muted_room)
cbb715e11963atagen2026-10-24 23:24 UTC554
                                .default_power_levels()
cbb715e11963atagen2026-10-24 23:24 UTC555
                                .sender(&own_user),
cbb715e11963atagen2026-10-24 23:24 UTC556
                        ),
cbb715e11963atagen2026-10-24 23:24 UTC557
                );
cbb715e11963atagen2026-10-24 23:24 UTC558
            }
cbb715e11963atagen2026-10-24 23:24 UTC559
        })
cbb715e11963atagen2026-10-24 23:24 UTC560
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC561
cbb715e11963atagen2026-10-24 23:24 UTC562
    let feed = SlidingFeed::new(
cbb715e11963atagen2026-10-24 23:24 UTC563
        room_mention_room.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC564
        vec![room_mention(
cbb715e11963atagen2026-10-24 23:24 UTC565
            "$unauthorized-room-mention",
cbb715e11963atagen2026-10-24 23:24 UTC566
            sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC567
            3_000,
cbb715e11963atagen2026-10-24 23:24 UTC568
        )],
cbb715e11963atagen2026-10-24 23:24 UTC569
    );
cbb715e11963atagen2026-10-24 23:24 UTC570
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC571
    let muted_events = vec![message(
cbb715e11963atagen2026-10-24 23:24 UTC572
        "$muted-direct-mention",
cbb715e11963atagen2026-10-24 23:24 UTC573
        sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC574
        2_000,
cbb715e11963atagen2026-10-24 23:24 UTC575
        "muted direct mention",
cbb715e11963atagen2026-10-24 23:24 UTC576
        Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC577
    )];
cbb715e11963atagen2026-10-24 23:24 UTC578
    let ignored = message(
cbb715e11963atagen2026-10-24 23:24 UTC579
        "$ignored-direct-mention",
cbb715e11963atagen2026-10-24 23:24 UTC580
        ignored_sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC581
        1_000,
cbb715e11963atagen2026-10-24 23:24 UTC582
        "ignored direct mention",
cbb715e11963atagen2026-10-24 23:24 UTC583
        Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC584
    );
cbb715e11963atagen2026-10-24 23:24 UTC585
    let mut muted_events = muted_events;
cbb715e11963atagen2026-10-24 23:24 UTC586
    muted_events.push(ignored);
cbb715e11963atagen2026-10-24 23:24 UTC587
    feed.add_room(muted_room, muted_events);
cbb715e11963atagen2026-10-24 23:24 UTC588
cbb715e11963atagen2026-10-24 23:24 UTC589
    let (engine, handle, mut events) = Engine::from_client(
cbb715e11963atagen2026-10-24 23:24 UTC590
        client,
cbb715e11963atagen2026-10-24 23:24 UTC591
        EngineConfig::default().with_initial_history_limit(0),
cbb715e11963atagen2026-10-24 23:24 UTC592
    )
cbb715e11963atagen2026-10-24 23:24 UTC593
    .await
cbb715e11963atagen2026-10-24 23:24 UTC594
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC595
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC596
cbb715e11963atagen2026-10-24 23:24 UTC597
    let page = mentions_page(&mut events, room_mention_room, |page| {
cbb715e11963atagen2026-10-24 23:24 UTC598
        page.entries
cbb715e11963atagen2026-10-24 23:24 UTC599
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC600
            .any(|entry| entry.key.event_id.as_str() == "$muted-direct-mention")
cbb715e11963atagen2026-10-24 23:24 UTC601
    })
cbb715e11963atagen2026-10-24 23:24 UTC602
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC603
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC604
        entry_ids(&page),
cbb715e11963atagen2026-10-24 23:24 UTC605
        HashSet::from(["$muted-direct-mention".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC606
    );
cbb715e11963atagen2026-10-24 23:24 UTC607
    assert!(page.entries[0].notification_suppressed);
cbb715e11963atagen2026-10-24 23:24 UTC608
    assert!(!entry_ids(&page).contains("$unauthorized-room-mention"));
cbb715e11963atagen2026-10-24 23:24 UTC609
    assert!(!entry_ids(&page).contains("$ignored-direct-mention"));
cbb715e11963atagen2026-10-24 23:24 UTC610
    let room_ids: HashSet<_> = page
cbb715e11963atagen2026-10-24 23:24 UTC611
        .entries
cbb715e11963atagen2026-10-24 23:24 UTC612
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC613
        .map(|entry| entry.key.room_id.clone())
cbb715e11963atagen2026-10-24 23:24 UTC614
        .collect();
cbb715e11963atagen2026-10-24 23:24 UTC615
    assert!(room_ids.contains(&muted_room.to_owned()));
cbb715e11963atagen2026-10-24 23:24 UTC616
cbb715e11963atagen2026-10-24 23:24 UTC617
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC618
}
cbb715e11963atagen2026-10-24 23:24 UTC619
cbb715e11963atagen2026-10-24 23:24 UTC620
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC621
async fn event_context_uses_independent_direction_limits_and_saved_mention_context_is_offline() {
cbb715e11963atagen2026-10-24 23:24 UTC622
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC623
    let room_id = room_id!("!mentions-context:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC624
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC625
    let mention_id = event_id!("$mention-context");
cbb715e11963atagen2026-10-24 23:24 UTC626
    let context_id = event_id!("$manual-context");
cbb715e11963atagen2026-10-24 23:24 UTC627
    let mention_event = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC628
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC629
        .text_msg("mention with saved context")
cbb715e11963atagen2026-10-24 23:24 UTC630
        .mentions(Mentions::with_user_ids([own_user.clone()]))
cbb715e11963atagen2026-10-24 23:24 UTC631
        .event_id(mention_id)
cbb715e11963atagen2026-10-24 23:24 UTC632
        .sender(&ALICE)
cbb715e11963atagen2026-10-24 23:24 UTC633
        .into_event();
cbb715e11963atagen2026-10-24 23:24 UTC634
    let context_event = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC635
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC636
        .text_msg("manually fetched context")
cbb715e11963atagen2026-10-24 23:24 UTC637
        .event_id(context_id)
cbb715e11963atagen2026-10-24 23:24 UTC638
        .sender(&ALICE)
cbb715e11963atagen2026-10-24 23:24 UTC639
        .into_event();
cbb715e11963atagen2026-10-24 23:24 UTC640
cbb715e11963atagen2026-10-24 23:24 UTC641
    server
cbb715e11963atagen2026-10-24 23:24 UTC642
        .mock_room_event_context()
cbb715e11963atagen2026-10-24 23:24 UTC643
        .room(room_id.to_owned())
cbb715e11963atagen2026-10-24 23:24 UTC644
        .match_event_id()
cbb715e11963atagen2026-10-24 23:24 UTC645
        .ok(RoomContextResponseTemplate::new(mention_event.clone())
cbb715e11963atagen2026-10-24 23:24 UTC646
            .start("mention-before")
cbb715e11963atagen2026-10-24 23:24 UTC647
            .end("mention-after"))
cbb715e11963atagen2026-10-24 23:24 UTC648
        .expect(1)
cbb715e11963atagen2026-10-24 23:24 UTC649
        .mount()
cbb715e11963atagen2026-10-24 23:24 UTC650
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC651
    server
cbb715e11963atagen2026-10-24 23:24 UTC652
        .mock_room_event_context()
cbb715e11963atagen2026-10-24 23:24 UTC653
        .room(room_id.to_owned())
cbb715e11963atagen2026-10-24 23:24 UTC654
        .match_event_id()
cbb715e11963atagen2026-10-24 23:24 UTC655
        .ok(RoomContextResponseTemplate::new(context_event)
cbb715e11963atagen2026-10-24 23:24 UTC656
            .start("manual-before")
cbb715e11963atagen2026-10-24 23:24 UTC657
            .end("manual-after"))
cbb715e11963atagen2026-10-24 23:24 UTC658
        .expect(1)
cbb715e11963atagen2026-10-24 23:24 UTC659
        .mount()
cbb715e11963atagen2026-10-24 23:24 UTC660
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC661
    mount_messages_page(&server, room_id, "b", "mention-before", 10).await;
cbb715e11963atagen2026-10-24 23:24 UTC662
    mount_messages_page(&server, room_id, "f", "mention-after", 10).await;
cbb715e11963atagen2026-10-24 23:24 UTC663
    mount_messages_page(&server, room_id, "b", "manual-before", 2).await;
cbb715e11963atagen2026-10-24 23:24 UTC664
    mount_messages_page(&server, room_id, "f", "manual-after", 3).await;
cbb715e11963atagen2026-10-24 23:24 UTC665
cbb715e11963atagen2026-10-24 23:24 UTC666
    let feed = SlidingFeed::new(
cbb715e11963atagen2026-10-24 23:24 UTC667
        room_id.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC668
        vec![message(
cbb715e11963atagen2026-10-24 23:24 UTC669
            mention_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC670
            ALICE.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC671
            5_000,
cbb715e11963atagen2026-10-24 23:24 UTC672
            "mention with saved context",
cbb715e11963atagen2026-10-24 23:24 UTC673
            Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC674
        )],
cbb715e11963atagen2026-10-24 23:24 UTC675
    );
cbb715e11963atagen2026-10-24 23:24 UTC676
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC677
    let (engine, handle, mut events) = Engine::from_client(
cbb715e11963atagen2026-10-24 23:24 UTC678
        client,
cbb715e11963atagen2026-10-24 23:24 UTC679
        EngineConfig::default().with_initial_history_limit(0),
cbb715e11963atagen2026-10-24 23:24 UTC680
    )
cbb715e11963atagen2026-10-24 23:24 UTC681
    .await
cbb715e11963atagen2026-10-24 23:24 UTC682
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC683
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC684
cbb715e11963atagen2026-10-24 23:24 UTC685
    let captured = mentions_page_with_diagnostics(
cbb715e11963atagen2026-10-24 23:24 UTC686
        &mut events,
cbb715e11963atagen2026-10-24 23:24 UTC687
        room_id,
cbb715e11963atagen2026-10-24 23:24 UTC688
        |page| {
cbb715e11963atagen2026-10-24 23:24 UTC689
            page.entries.iter().any(|entry| {
cbb715e11963atagen2026-10-24 23:24 UTC690
                entry.key.event_id == mention_id
cbb715e11963atagen2026-10-24 23:24 UTC691
                    && matches!(&entry.capture, macltok::MentionCaptureState::Partial)
cbb715e11963atagen2026-10-24 23:24 UTC692
            })
cbb715e11963atagen2026-10-24 23:24 UTC693
        },
cbb715e11963atagen2026-10-24 23:24 UTC694
        Some(&server),
cbb715e11963atagen2026-10-24 23:24 UTC695
    )
cbb715e11963atagen2026-10-24 23:24 UTC696
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC697
    let saved = handle.mention_context(room_id, mention_id).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC698
    assert!(saved.context.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC699
    assert!(matches!(saved.state, macltok::MentionCaptureState::Partial));
cbb715e11963atagen2026-10-24 23:24 UTC700
cbb715e11963atagen2026-10-24 23:24 UTC701
    let before_saved_request_count = server
cbb715e11963atagen2026-10-24 23:24 UTC702
        .server()
cbb715e11963atagen2026-10-24 23:24 UTC703
        .received_requests()
cbb715e11963atagen2026-10-24 23:24 UTC704
        .await
cbb715e11963atagen2026-10-24 23:24 UTC705
        .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC706
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC707
        .filter(|request| request.url.path().contains("/context/$mention-context"))
cbb715e11963atagen2026-10-24 23:24 UTC708
        .count();
cbb715e11963atagen2026-10-24 23:24 UTC709
    let _ = handle.mention_context(room_id, mention_id).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC710
    let after_saved_request_count = server
cbb715e11963atagen2026-10-24 23:24 UTC711
        .server()
cbb715e11963atagen2026-10-24 23:24 UTC712
        .received_requests()
cbb715e11963atagen2026-10-24 23:24 UTC713
        .await
cbb715e11963atagen2026-10-24 23:24 UTC714
        .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC715
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC716
        .filter(|request| request.url.path().contains("/context/$mention-context"))
cbb715e11963atagen2026-10-24 23:24 UTC717
        .count();
cbb715e11963atagen2026-10-24 23:24 UTC718
    assert_eq!(after_saved_request_count, before_saved_request_count);
cbb715e11963atagen2026-10-24 23:24 UTC719
cbb715e11963atagen2026-10-24 23:24 UTC720
    let context = handle
cbb715e11963atagen2026-10-24 23:24 UTC721
        .event_context(room_id, context_id, 2, 3)
cbb715e11963atagen2026-10-24 23:24 UTC722
        .await
cbb715e11963atagen2026-10-24 23:24 UTC723
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC724
    assert_eq!(context.start_token.as_deref(), Some("manual-before-next"));
cbb715e11963atagen2026-10-24 23:24 UTC725
    assert_eq!(context.end_token.as_deref(), Some("manual-after-next"));
cbb715e11963atagen2026-10-24 23:24 UTC726
cbb715e11963atagen2026-10-24 23:24 UTC727
    let requests = server.server().received_requests().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC728
    let context_limits: Vec<_> = requests
cbb715e11963atagen2026-10-24 23:24 UTC729
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC730
        .filter(|request| request.url.path().contains("/context/"))
cbb715e11963atagen2026-10-24 23:24 UTC731
        .map(|request| {
cbb715e11963atagen2026-10-24 23:24 UTC732
            request
cbb715e11963atagen2026-10-24 23:24 UTC733
                .url
cbb715e11963atagen2026-10-24 23:24 UTC734
                .query_pairs()
cbb715e11963atagen2026-10-24 23:24 UTC735
                .find(|(key, _)| key == "limit")
cbb715e11963atagen2026-10-24 23:24 UTC736
                .map(|(_, value)| value.into_owned())
cbb715e11963atagen2026-10-24 23:24 UTC737
                .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC738
        })
cbb715e11963atagen2026-10-24 23:24 UTC739
        .collect();
cbb715e11963atagen2026-10-24 23:24 UTC740
    assert_eq!(context_limits, ["0", "0"]);
cbb715e11963atagen2026-10-24 23:24 UTC741
    let queries: Vec<_> = requests
cbb715e11963atagen2026-10-24 23:24 UTC742
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC743
        .filter(|request| request.url.path().ends_with("/messages"))
cbb715e11963atagen2026-10-24 23:24 UTC744
        .map(|request| {
cbb715e11963atagen2026-10-24 23:24 UTC745
            let query: std::collections::HashMap<_, _> =
cbb715e11963atagen2026-10-24 23:24 UTC746
                request.url.query_pairs().into_owned().collect();
cbb715e11963atagen2026-10-24 23:24 UTC747
            (
cbb715e11963atagen2026-10-24 23:24 UTC748
                query.get("dir").cloned().unwrap(),
cbb715e11963atagen2026-10-24 23:24 UTC749
                query.get("from").cloned().unwrap(),
cbb715e11963atagen2026-10-24 23:24 UTC750
                query
cbb715e11963atagen2026-10-24 23:24 UTC751
                    .get("limit")
cbb715e11963atagen2026-10-24 23:24 UTC752
                    .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC753
                    .unwrap_or_else(|| "10".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC754
            )
cbb715e11963atagen2026-10-24 23:24 UTC755
        })
cbb715e11963atagen2026-10-24 23:24 UTC756
        .collect();
cbb715e11963atagen2026-10-24 23:24 UTC757
    assert!(queries.contains(&("b".to_owned(), "mention-before".to_owned(), "10".to_owned())));
cbb715e11963atagen2026-10-24 23:24 UTC758
    assert!(queries.contains(&("f".to_owned(), "mention-after".to_owned(), "10".to_owned())));
cbb715e11963atagen2026-10-24 23:24 UTC759
    assert!(queries.contains(&("b".to_owned(), "manual-before".to_owned(), "2".to_owned())));
cbb715e11963atagen2026-10-24 23:24 UTC760
    assert!(queries.contains(&("f".to_owned(), "manual-after".to_owned(), "3".to_owned())));
cbb715e11963atagen2026-10-24 23:24 UTC761
    assert_eq!(captured.unread_count, 1);
cbb715e11963atagen2026-10-24 23:24 UTC762
cbb715e11963atagen2026-10-24 23:24 UTC763
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC764
    server.server().verify().await;
cbb715e11963atagen2026-10-24 23:24 UTC765
}
cbb715e11963atagen2026-10-24 23:24 UTC766
cbb715e11963atagen2026-10-24 23:24 UTC767
#[cfg(feature = "sqlite")]
cbb715e11963atagen2026-10-24 23:24 UTC768
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC769
async fn sqlite_mentions_survive_engine_shutdown_and_session_resume() {
cbb715e11963atagen2026-10-24 23:24 UTC770
    static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);
cbb715e11963atagen2026-10-24 23:24 UTC771
cbb715e11963atagen2026-10-24 23:24 UTC772
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC773
    server
cbb715e11963atagen2026-10-24 23:24 UTC774
        .mock_versions()
cbb715e11963atagen2026-10-24 23:24 UTC775
        .with_simplified_sliding_sync()
cbb715e11963atagen2026-10-24 23:24 UTC776
        .ok()
cbb715e11963atagen2026-10-24 23:24 UTC777
        .mount()
cbb715e11963atagen2026-10-24 23:24 UTC778
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC779
    server.mock_login().ok().mount().await;
cbb715e11963atagen2026-10-24 23:24 UTC780
cbb715e11963atagen2026-10-24 23:24 UTC781
    let root = std::env::temp_dir().join(format!(
cbb715e11963atagen2026-10-24 23:24 UTC782
        "macltok-mentions-inbox-{}-{}",
cbb715e11963atagen2026-10-24 23:24 UTC783
        std::process::id(),
cbb715e11963atagen2026-10-24 23:24 UTC784
        NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)
cbb715e11963atagen2026-10-24 23:24 UTC785
    ));
cbb715e11963atagen2026-10-24 23:24 UTC786
    let store_path = root.join("store");
cbb715e11963atagen2026-10-24 23:24 UTC787
    let sessions_path = root.join("records").join("session");
cbb715e11963atagen2026-10-24 23:24 UTC788
    let sessions = Arc::new(EncryptedSessionStore::file(
cbb715e11963atagen2026-10-24 23:24 UTC789
        &sessions_path,
cbb715e11963atagen2026-10-24 23:24 UTC790
        StoreConfig::sqlite(&store_path, "mentions inbox test passphrase"),
cbb715e11963atagen2026-10-24 23:24 UTC791
    ));
cbb715e11963atagen2026-10-24 23:24 UTC792
    let room_id = room_id!("!mentions-resume:matrix.org");
cbb715e11963atagen2026-10-24 23:24 UTC793
    let own_user = user_id!("@cheeky_monkey:matrix.org");
cbb715e11963atagen2026-10-24 23:24 UTC794
    let feed = SlidingFeed::new(
cbb715e11963atagen2026-10-24 23:24 UTC795
        room_id.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC796
        vec![message(
cbb715e11963atagen2026-10-24 23:24 UTC797
            "$durable-mention",
cbb715e11963atagen2026-10-24 23:24 UTC798
            "@alice:matrix.org",
cbb715e11963atagen2026-10-24 23:24 UTC799
            7_000,
cbb715e11963atagen2026-10-24 23:24 UTC800
            "persist this mention",
cbb715e11963atagen2026-10-24 23:24 UTC801
            Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC802
        )],
cbb715e11963atagen2026-10-24 23:24 UTC803
    );
cbb715e11963atagen2026-10-24 23:24 UTC804
    feed.set_required_state(
cbb715e11963atagen2026-10-24 23:24 UTC805
        room_id,
cbb715e11963atagen2026-10-24 23:24 UTC806
        vec![json!({
cbb715e11963atagen2026-10-24 23:24 UTC807
            "type": "m.room.member",
cbb715e11963atagen2026-10-24 23:24 UTC808
            "event_id": "$joined-member",
cbb715e11963atagen2026-10-24 23:24 UTC809
            "sender": own_user,
cbb715e11963atagen2026-10-24 23:24 UTC810
            "state_key": own_user,
cbb715e11963atagen2026-10-24 23:24 UTC811
            "origin_server_ts": 1,
cbb715e11963atagen2026-10-24 23:24 UTC812
            "content": { "membership": "join" },
cbb715e11963atagen2026-10-24 23:24 UTC813
        })],
cbb715e11963atagen2026-10-24 23:24 UTC814
    );
cbb715e11963atagen2026-10-24 23:24 UTC815
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC816
cbb715e11963atagen2026-10-24 23:24 UTC817
    let config = EngineConfig::default().with_initial_history_limit(0);
cbb715e11963atagen2026-10-24 23:24 UTC818
    let session = Session::login_password(
cbb715e11963atagen2026-10-24 23:24 UTC819
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
cbb715e11963atagen2026-10-24 23:24 UTC820
        sessions.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC821
    )
cbb715e11963atagen2026-10-24 23:24 UTC822
    .await
cbb715e11963atagen2026-10-24 23:24 UTC823
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC824
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC825
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC826
    let first = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
cbb715e11963atagen2026-10-24 23:24 UTC827
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC828
        entry_ids(&first),
cbb715e11963atagen2026-10-24 23:24 UTC829
        HashSet::from(["$durable-mention".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC830
    );
cbb715e11963atagen2026-10-24 23:24 UTC831
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC832
    drop(handle);
cbb715e11963atagen2026-10-24 23:24 UTC833
cbb715e11963atagen2026-10-24 23:24 UTC834
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
cbb715e11963atagen2026-10-24 23:24 UTC835
        panic!("the saved session did not resume");
cbb715e11963atagen2026-10-24 23:24 UTC836
    };
cbb715e11963atagen2026-10-24 23:24 UTC837
    let (engine, handle, mut events) = session.build(config).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC838
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC839
    receive_until(&mut events, |event| {
cbb715e11963atagen2026-10-24 23:24 UTC840
        matches!(event, Event::Authenticated { .. })
cbb715e11963atagen2026-10-24 23:24 UTC841
    })
cbb715e11963atagen2026-10-24 23:24 UTC842
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC843
    let resumed = handle.mentions_page(None, 50).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC844
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC845
        entry_ids(&resumed),
cbb715e11963atagen2026-10-24 23:24 UTC846
        HashSet::from(["$durable-mention".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC847
    );
cbb715e11963atagen2026-10-24 23:24 UTC848
    assert_eq!(resumed.unread_count, 1);
cbb715e11963atagen2026-10-24 23:24 UTC849
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC850
    drop(handle);
cbb715e11963atagen2026-10-24 23:24 UTC851
cbb715e11963atagen2026-10-24 23:24 UTC852
    sessions.discard().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC853
    std::fs::remove_dir_all(root).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC854
}
cbb715e11963atagen2026-10-24 23:24 UTC855
cbb715e11963atagen2026-10-24 23:24 UTC856
#[cfg(feature = "serde")]
cbb715e11963atagen2026-10-24 23:24 UTC857
#[test]
cbb715e11963atagen2026-10-24 23:24 UTC858
fn mentions_command_and_page_round_trip_through_serde() {
cbb715e11963atagen2026-10-24 23:24 UTC859
    let command = MentionsCommand::ReadThrough(MentionsBoundary {
cbb715e11963atagen2026-10-24 23:24 UTC860
        account: user_id!("@alice:example.org").to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC861
        revision: 7,
cbb715e11963atagen2026-10-24 23:24 UTC862
    });
cbb715e11963atagen2026-10-24 23:24 UTC863
    let encoded_command = serde_json::to_value(&command).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC864
    let decoded_command: MentionsCommand = serde_json::from_value(encoded_command).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC865
    assert_eq!(decoded_command, command);
cbb715e11963atagen2026-10-24 23:24 UTC866
cbb715e11963atagen2026-10-24 23:24 UTC867
    let page = MentionsPage {
cbb715e11963atagen2026-10-24 23:24 UTC868
        entries: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC869
        next_cursor: Some("opaque cursor".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC870
        end_reached: false,
cbb715e11963atagen2026-10-24 23:24 UTC871
        coverage: MentionCoverage::default(),
cbb715e11963atagen2026-10-24 23:24 UTC872
        unread_count: 0,
cbb715e11963atagen2026-10-24 23:24 UTC873
        revision: 7,
cbb715e11963atagen2026-10-24 23:24 UTC874
        boundary: MentionsBoundary {
cbb715e11963atagen2026-10-24 23:24 UTC875
            account: user_id!("@alice:example.org").to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC876
            revision: 7,
cbb715e11963atagen2026-10-24 23:24 UTC877
        },
cbb715e11963atagen2026-10-24 23:24 UTC878
    };
cbb715e11963atagen2026-10-24 23:24 UTC879
    let encoded_page = serde_json::to_value(&page).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC880
    let decoded_page: MentionsPage = serde_json::from_value(encoded_page).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC881
    assert_eq!(decoded_page, page);
cbb715e11963atagen2026-10-24 23:24 UTC882
}