atagen

macltok

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