atagen

macltok

public
Star0
Watch0
2cf8b408671e16dec979db151f90fc334fefb5b0
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,
74befbc72bafatagen2026-10-25 11:18 UTC337
        EngineConfig::default()
74befbc72bafatagen2026-10-25 11:18 UTC338
            .with_initial_history_limit(0)
74befbc72bafatagen2026-10-25 11:18 UTC339
            .with_search_backfill_pages_per_room(0),
cbb715e11963atagen2026-10-24 23:24 UTC340
    )
cbb715e11963atagen2026-10-24 23:24 UTC341
    .await
cbb715e11963atagen2026-10-24 23:24 UTC342
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC343
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC344
cbb715e11963atagen2026-10-24 23:24 UTC345
    let first = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
cbb715e11963atagen2026-10-24 23:24 UTC346
    assert_eq!(entry_ids(&first), HashSet::from(["$mention".to_owned()]));
cbb715e11963atagen2026-10-24 23:24 UTC347
    assert_eq!(first.unread_count, 1);
cbb715e11963atagen2026-10-24 23:24 UTC348
    let first_boundary = first.boundary.clone();
cbb715e11963atagen2026-10-24 23:24 UTC349
cbb715e11963atagen2026-10-24 23:24 UTC350
    wait_for_requests(&feed, 3).await;
cbb715e11963atagen2026-10-24 23:24 UTC351
    let replayed = handle.mentions_page(None, 50).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC352
    assert_eq!(entry_ids(&replayed), HashSet::from(["$mention".to_owned()]));
cbb715e11963atagen2026-10-24 23:24 UTC353
cbb715e11963atagen2026-10-24 23:24 UTC354
    let key = replayed.entries[0].key.clone();
cbb715e11963atagen2026-10-24 23:24 UTC355
    handle
cbb715e11963atagen2026-10-24 23:24 UTC356
        .set_mentions_read(vec![key.clone()], true)
cbb715e11963atagen2026-10-24 23:24 UTC357
        .await
cbb715e11963atagen2026-10-24 23:24 UTC358
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC359
    let read = handle.mentions_page(None, 50).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC360
    assert_eq!(read.unread_count, 0);
cbb715e11963atagen2026-10-24 23:24 UTC361
cbb715e11963atagen2026-10-24 23:24 UTC362
    feed.replace(
cbb715e11963atagen2026-10-24 23:24 UTC363
        room_id,
cbb715e11963atagen2026-10-24 23:24 UTC364
        [
cbb715e11963atagen2026-10-24 23:24 UTC365
            initial,
cbb715e11963atagen2026-10-24 23:24 UTC366
            vec![message(
cbb715e11963atagen2026-10-24 23:24 UTC367
                "$older-mention",
cbb715e11963atagen2026-10-24 23:24 UTC368
                sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC369
                1_000,
cbb715e11963atagen2026-10-24 23:24 UTC370
                "older direct mention",
cbb715e11963atagen2026-10-24 23:24 UTC371
                Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC372
            )],
cbb715e11963atagen2026-10-24 23:24 UTC373
        ]
cbb715e11963atagen2026-10-24 23:24 UTC374
        .concat(),
cbb715e11963atagen2026-10-24 23:24 UTC375
    );
cbb715e11963atagen2026-10-24 23:24 UTC376
    let updated = mentions_page(&mut events, room_id, |page| page.entries.len() == 2).await;
cbb715e11963atagen2026-10-24 23:24 UTC377
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC378
        entry_ids(&updated),
cbb715e11963atagen2026-10-24 23:24 UTC379
        HashSet::from(["$mention".to_owned(), "$older-mention".to_owned(),])
cbb715e11963atagen2026-10-24 23:24 UTC380
    );
cbb715e11963atagen2026-10-24 23:24 UTC381
cbb715e11963atagen2026-10-24 23:24 UTC382
    handle
cbb715e11963atagen2026-10-24 23:24 UTC383
        .mark_mentions_read_through(first_boundary)
cbb715e11963atagen2026-10-24 23:24 UTC384
        .await
cbb715e11963atagen2026-10-24 23:24 UTC385
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC386
    let after_boundary = handle.mentions_page(None, 50).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC387
    let read_states: Vec<_> = after_boundary
cbb715e11963atagen2026-10-24 23:24 UTC388
        .entries
cbb715e11963atagen2026-10-24 23:24 UTC389
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC390
        .map(|entry| (entry.key.event_id.to_string(), entry.read))
cbb715e11963atagen2026-10-24 23:24 UTC391
        .collect();
cbb715e11963atagen2026-10-24 23:24 UTC392
    assert!(read_states.contains(&("$mention".to_owned(), true)));
cbb715e11963atagen2026-10-24 23:24 UTC393
    assert!(read_states.contains(&("$older-mention".to_owned(), false)));
cbb715e11963atagen2026-10-24 23:24 UTC394
    assert_eq!(after_boundary.unread_count, 1);
cbb715e11963atagen2026-10-24 23:24 UTC395
cbb715e11963atagen2026-10-24 23:24 UTC396
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC397
}
cbb715e11963atagen2026-10-24 23:24 UTC398
cbb715e11963atagen2026-10-24 23:24 UTC399
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC400
async fn edits_can_remove_and_restore_a_mention_until_redaction_removes_its_target() {
cbb715e11963atagen2026-10-24 23:24 UTC401
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC402
    let room_id = room_id!("!mentions-edits:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC403
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC404
    let sender = user_id!("@alice:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC405
    let original = message(
cbb715e11963atagen2026-10-24 23:24 UTC406
        "$edited-target",
cbb715e11963atagen2026-10-24 23:24 UTC407
        sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC408
        1_000,
cbb715e11963atagen2026-10-24 23:24 UTC409
        "original mention",
cbb715e11963atagen2026-10-24 23:24 UTC410
        Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC411
    );
cbb715e11963atagen2026-10-24 23:24 UTC412
    let removal = json!({
cbb715e11963atagen2026-10-24 23:24 UTC413
        "type": "m.room.message",
cbb715e11963atagen2026-10-24 23:24 UTC414
        "event_id": "$remove-edit",
cbb715e11963atagen2026-10-24 23:24 UTC415
        "sender": sender,
cbb715e11963atagen2026-10-24 23:24 UTC416
        "origin_server_ts": 2_000,
cbb715e11963atagen2026-10-24 23:24 UTC417
        "content": {
cbb715e11963atagen2026-10-24 23:24 UTC418
            "msgtype": "m.text",
cbb715e11963atagen2026-10-24 23:24 UTC419
            "body": "* edited without a mention",
cbb715e11963atagen2026-10-24 23:24 UTC420
            "m.relates_to": { "rel_type": "m.replace", "event_id": "$edited-target" },
cbb715e11963atagen2026-10-24 23:24 UTC421
            "m.new_content": { "msgtype": "m.text", "body": "edited without a mention" },
cbb715e11963atagen2026-10-24 23:24 UTC422
        },
cbb715e11963atagen2026-10-24 23:24 UTC423
    });
cbb715e11963atagen2026-10-24 23:24 UTC424
    let addition = json!({
cbb715e11963atagen2026-10-24 23:24 UTC425
        "type": "m.room.message",
cbb715e11963atagen2026-10-24 23:24 UTC426
        "event_id": "$add-edit",
cbb715e11963atagen2026-10-24 23:24 UTC427
        "sender": sender,
cbb715e11963atagen2026-10-24 23:24 UTC428
        "origin_server_ts": 3_000,
cbb715e11963atagen2026-10-24 23:24 UTC429
        "content": {
cbb715e11963atagen2026-10-24 23:24 UTC430
            "msgtype": "m.text",
cbb715e11963atagen2026-10-24 23:24 UTC431
            "body": "* edited to mention you",
cbb715e11963atagen2026-10-24 23:24 UTC432
            "m.relates_to": { "rel_type": "m.replace", "event_id": "$edited-target" },
cbb715e11963atagen2026-10-24 23:24 UTC433
            "m.new_content": {
cbb715e11963atagen2026-10-24 23:24 UTC434
                "msgtype": "m.text",
cbb715e11963atagen2026-10-24 23:24 UTC435
                "body": "edited to mention you",
cbb715e11963atagen2026-10-24 23:24 UTC436
                "m.mentions": { "user_ids": [own_user.as_str()] },
cbb715e11963atagen2026-10-24 23:24 UTC437
            },
cbb715e11963atagen2026-10-24 23:24 UTC438
        },
cbb715e11963atagen2026-10-24 23:24 UTC439
    });
cbb715e11963atagen2026-10-24 23:24 UTC440
    let redaction = json!({
cbb715e11963atagen2026-10-24 23:24 UTC441
        "type": "m.room.redaction",
cbb715e11963atagen2026-10-24 23:24 UTC442
        "event_id": "$redaction",
cbb715e11963atagen2026-10-24 23:24 UTC443
        "sender": sender,
cbb715e11963atagen2026-10-24 23:24 UTC444
        "origin_server_ts": 4_000,
cbb715e11963atagen2026-10-24 23:24 UTC445
        "redacts": "$edited-target",
cbb715e11963atagen2026-10-24 23:24 UTC446
        "content": {},
cbb715e11963atagen2026-10-24 23:24 UTC447
    });
cbb715e11963atagen2026-10-24 23:24 UTC448
    let feed = SlidingFeed::new(room_id.to_owned(), vec![original.clone()]);
cbb715e11963atagen2026-10-24 23:24 UTC449
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC450
cbb715e11963atagen2026-10-24 23:24 UTC451
    let (engine, handle, mut events) = Engine::from_client(
cbb715e11963atagen2026-10-24 23:24 UTC452
        client,
74befbc72bafatagen2026-10-25 11:18 UTC453
        EngineConfig::default()
74befbc72bafatagen2026-10-25 11:18 UTC454
            .with_initial_history_limit(0)
74befbc72bafatagen2026-10-25 11:18 UTC455
            .with_search_backfill_pages_per_room(0),
cbb715e11963atagen2026-10-24 23:24 UTC456
    )
cbb715e11963atagen2026-10-24 23:24 UTC457
    .await
cbb715e11963atagen2026-10-24 23:24 UTC458
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC459
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC460
cbb715e11963atagen2026-10-24 23:24 UTC461
    let discovered = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
cbb715e11963atagen2026-10-24 23:24 UTC462
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC463
        entry_ids(&discovered),
cbb715e11963atagen2026-10-24 23:24 UTC464
        HashSet::from(["$edited-target".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC465
    );
cbb715e11963atagen2026-10-24 23:24 UTC466
cbb715e11963atagen2026-10-24 23:24 UTC467
    feed.replace(room_id, vec![original.clone(), removal.clone()]);
cbb715e11963atagen2026-10-24 23:24 UTC468
    let removed = mentions_page(&mut events, room_id, |page| {
cbb715e11963atagen2026-10-24 23:24 UTC469
        page.revision > discovered.revision && page.entries.is_empty()
cbb715e11963atagen2026-10-24 23:24 UTC470
    })
cbb715e11963atagen2026-10-24 23:24 UTC471
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC472
    assert!(removed.entries.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC473
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC474
        handle
cbb715e11963atagen2026-10-24 23:24 UTC475
            .mention_context(room_id, ruma::event_id!("$edited-target"))
cbb715e11963atagen2026-10-24 23:24 UTC476
            .await
cbb715e11963atagen2026-10-24 23:24 UTC477
            .unwrap_err()
cbb715e11963atagen2026-10-24 23:24 UTC478
            .kind,
cbb715e11963atagen2026-10-24 23:24 UTC479
        macltok::ErrorKind::EventNotFound
cbb715e11963atagen2026-10-24 23:24 UTC480
    );
cbb715e11963atagen2026-10-24 23:24 UTC481
cbb715e11963atagen2026-10-24 23:24 UTC482
    feed.replace(room_id, vec![original.clone(), removal, addition]);
cbb715e11963atagen2026-10-24 23:24 UTC483
    let restored = mentions_page(&mut events, room_id, |page| {
cbb715e11963atagen2026-10-24 23:24 UTC484
        page.revision > removed.revision && entry_ids(page).contains("$edited-target")
cbb715e11963atagen2026-10-24 23:24 UTC485
    })
cbb715e11963atagen2026-10-24 23:24 UTC486
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC487
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC488
        entry_ids(&restored),
cbb715e11963atagen2026-10-24 23:24 UTC489
        HashSet::from(["$edited-target".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC490
    );
cbb715e11963atagen2026-10-24 23:24 UTC491
cbb715e11963atagen2026-10-24 23:24 UTC492
    feed.replace(room_id, vec![original, redaction]);
cbb715e11963atagen2026-10-24 23:24 UTC493
    let redacted = mentions_page(&mut events, room_id, |page| {
cbb715e11963atagen2026-10-24 23:24 UTC494
        page.revision > restored.revision && page.entries.is_empty()
cbb715e11963atagen2026-10-24 23:24 UTC495
    })
cbb715e11963atagen2026-10-24 23:24 UTC496
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC497
    assert!(redacted.entries.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC498
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC499
        handle
cbb715e11963atagen2026-10-24 23:24 UTC500
            .mention_context(room_id, ruma::event_id!("$edited-target"))
cbb715e11963atagen2026-10-24 23:24 UTC501
            .await
cbb715e11963atagen2026-10-24 23:24 UTC502
            .unwrap_err()
cbb715e11963atagen2026-10-24 23:24 UTC503
            .kind,
cbb715e11963atagen2026-10-24 23:24 UTC504
        macltok::ErrorKind::EventNotFound
cbb715e11963atagen2026-10-24 23:24 UTC505
    );
cbb715e11963atagen2026-10-24 23:24 UTC506
cbb715e11963atagen2026-10-24 23:24 UTC507
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC508
}
cbb715e11963atagen2026-10-24 23:24 UTC509
cbb715e11963atagen2026-10-24 23:24 UTC510
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC511
async fn room_mentions_require_power_while_muted_direct_mentions_remain_and_ignored_senders_drop() {
cbb715e11963atagen2026-10-24 23:24 UTC512
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC513
    let client = server.client_builder().build().await;
cbb715e11963atagen2026-10-24 23:24 UTC514
    let own_user = client.user_id().unwrap().to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC515
    let room_mention_room = room_id!("!room-mention-policy:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC516
    let muted_room = room_id!("!muted-mention-policy:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC517
    let ignored_sender = user_id!("@ignored:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC518
    let sender = user_id!("@alice:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC519
    let mut rules = Ruleset::server_default(&own_user);
cbb715e11963atagen2026-10-24 23:24 UTC520
    rules.override_.shift_insert(
cbb715e11963atagen2026-10-24 23:24 UTC521
        1,
cbb715e11963atagen2026-10-24 23:24 UTC522
        NewConditionalPushRule::new(
cbb715e11963atagen2026-10-24 23:24 UTC523
            "mute-inbox-room".to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC524
            vec![PushCondition::EventMatch(EventMatchConditionData::new(
cbb715e11963atagen2026-10-24 23:24 UTC525
                "room_id".to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC526
                muted_room.to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC527
            ))],
cbb715e11963atagen2026-10-24 23:24 UTC528
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC529
        )
cbb715e11963atagen2026-10-24 23:24 UTC530
        .into(),
cbb715e11963atagen2026-10-24 23:24 UTC531
    );
cbb715e11963atagen2026-10-24 23:24 UTC532
    server
cbb715e11963atagen2026-10-24 23:24 UTC533
        .mock_sync()
cbb715e11963atagen2026-10-24 23:24 UTC534
        .ok_and_run(&client, {
cbb715e11963atagen2026-10-24 23:24 UTC535
            let own_user = own_user.clone();
cbb715e11963atagen2026-10-24 23:24 UTC536
            let ignored_sender = ignored_sender.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC537
            move |builder| {
cbb715e11963atagen2026-10-24 23:24 UTC538
                let factory = EventFactory::new();
cbb715e11963atagen2026-10-24 23:24 UTC539
                builder.add_global_account_data(factory.push_rules(rules));
cbb715e11963atagen2026-10-24 23:24 UTC540
                builder.add_global_account_data(factory.ignored_user_list([ignored_sender]));
cbb715e11963atagen2026-10-24 23:24 UTC541
                builder.add_joined_room(
cbb715e11963atagen2026-10-24 23:24 UTC542
                    JoinedRoomBuilder::new(room_mention_room)
cbb715e11963atagen2026-10-24 23:24 UTC543
                        .add_state_event(
cbb715e11963atagen2026-10-24 23:24 UTC544
                            EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC545
                                .room(room_mention_room)
cbb715e11963atagen2026-10-24 23:24 UTC546
                                .member(&own_user),
cbb715e11963atagen2026-10-24 23:24 UTC547
                        )
cbb715e11963atagen2026-10-24 23:24 UTC548
                        .add_state_event(
cbb715e11963atagen2026-10-24 23:24 UTC549
                            EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC550
                                .room(room_mention_room)
cbb715e11963atagen2026-10-24 23:24 UTC551
                                .default_power_levels()
cbb715e11963atagen2026-10-24 23:24 UTC552
                                .sender(&own_user),
cbb715e11963atagen2026-10-24 23:24 UTC553
                        ),
cbb715e11963atagen2026-10-24 23:24 UTC554
                );
cbb715e11963atagen2026-10-24 23:24 UTC555
                builder.add_joined_room(
cbb715e11963atagen2026-10-24 23:24 UTC556
                    JoinedRoomBuilder::new(muted_room)
cbb715e11963atagen2026-10-24 23:24 UTC557
                        .add_state_event(EventFactory::new().room(muted_room).member(&own_user))
cbb715e11963atagen2026-10-24 23:24 UTC558
                        .add_state_event(
cbb715e11963atagen2026-10-24 23:24 UTC559
                            EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC560
                                .room(muted_room)
cbb715e11963atagen2026-10-24 23:24 UTC561
                                .default_power_levels()
cbb715e11963atagen2026-10-24 23:24 UTC562
                                .sender(&own_user),
cbb715e11963atagen2026-10-24 23:24 UTC563
                        ),
cbb715e11963atagen2026-10-24 23:24 UTC564
                );
cbb715e11963atagen2026-10-24 23:24 UTC565
            }
cbb715e11963atagen2026-10-24 23:24 UTC566
        })
cbb715e11963atagen2026-10-24 23:24 UTC567
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC568
cbb715e11963atagen2026-10-24 23:24 UTC569
    let feed = SlidingFeed::new(
cbb715e11963atagen2026-10-24 23:24 UTC570
        room_mention_room.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC571
        vec![room_mention(
cbb715e11963atagen2026-10-24 23:24 UTC572
            "$unauthorized-room-mention",
cbb715e11963atagen2026-10-24 23:24 UTC573
            sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC574
            3_000,
cbb715e11963atagen2026-10-24 23:24 UTC575
        )],
cbb715e11963atagen2026-10-24 23:24 UTC576
    );
cbb715e11963atagen2026-10-24 23:24 UTC577
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC578
    let muted_events = vec![message(
cbb715e11963atagen2026-10-24 23:24 UTC579
        "$muted-direct-mention",
cbb715e11963atagen2026-10-24 23:24 UTC580
        sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC581
        2_000,
cbb715e11963atagen2026-10-24 23:24 UTC582
        "muted 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 ignored = message(
cbb715e11963atagen2026-10-24 23:24 UTC586
        "$ignored-direct-mention",
cbb715e11963atagen2026-10-24 23:24 UTC587
        ignored_sender.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC588
        1_000,
cbb715e11963atagen2026-10-24 23:24 UTC589
        "ignored direct mention",
cbb715e11963atagen2026-10-24 23:24 UTC590
        Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC591
    );
cbb715e11963atagen2026-10-24 23:24 UTC592
    let mut muted_events = muted_events;
cbb715e11963atagen2026-10-24 23:24 UTC593
    muted_events.push(ignored);
cbb715e11963atagen2026-10-24 23:24 UTC594
    feed.add_room(muted_room, muted_events);
cbb715e11963atagen2026-10-24 23:24 UTC595
cbb715e11963atagen2026-10-24 23:24 UTC596
    let (engine, handle, mut events) = Engine::from_client(
cbb715e11963atagen2026-10-24 23:24 UTC597
        client,
74befbc72bafatagen2026-10-25 11:18 UTC598
        EngineConfig::default()
74befbc72bafatagen2026-10-25 11:18 UTC599
            .with_initial_history_limit(0)
74befbc72bafatagen2026-10-25 11:18 UTC600
            .with_search_backfill_pages_per_room(0),
cbb715e11963atagen2026-10-24 23:24 UTC601
    )
cbb715e11963atagen2026-10-24 23:24 UTC602
    .await
cbb715e11963atagen2026-10-24 23:24 UTC603
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC604
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC605
cbb715e11963atagen2026-10-24 23:24 UTC606
    let page = mentions_page(&mut events, room_mention_room, |page| {
cbb715e11963atagen2026-10-24 23:24 UTC607
        page.entries
cbb715e11963atagen2026-10-24 23:24 UTC608
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC609
            .any(|entry| entry.key.event_id.as_str() == "$muted-direct-mention")
cbb715e11963atagen2026-10-24 23:24 UTC610
    })
cbb715e11963atagen2026-10-24 23:24 UTC611
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC612
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC613
        entry_ids(&page),
cbb715e11963atagen2026-10-24 23:24 UTC614
        HashSet::from(["$muted-direct-mention".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC615
    );
cbb715e11963atagen2026-10-24 23:24 UTC616
    assert!(page.entries[0].notification_suppressed);
cbb715e11963atagen2026-10-24 23:24 UTC617
    assert!(!entry_ids(&page).contains("$unauthorized-room-mention"));
cbb715e11963atagen2026-10-24 23:24 UTC618
    assert!(!entry_ids(&page).contains("$ignored-direct-mention"));
cbb715e11963atagen2026-10-24 23:24 UTC619
    let room_ids: HashSet<_> = page
cbb715e11963atagen2026-10-24 23:24 UTC620
        .entries
cbb715e11963atagen2026-10-24 23:24 UTC621
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC622
        .map(|entry| entry.key.room_id.clone())
cbb715e11963atagen2026-10-24 23:24 UTC623
        .collect();
cbb715e11963atagen2026-10-24 23:24 UTC624
    assert!(room_ids.contains(&muted_room.to_owned()));
cbb715e11963atagen2026-10-24 23:24 UTC625
cbb715e11963atagen2026-10-24 23:24 UTC626
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC627
}
cbb715e11963atagen2026-10-24 23:24 UTC628
cbb715e11963atagen2026-10-24 23:24 UTC629
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC630
async fn event_context_uses_independent_direction_limits_and_saved_mention_context_is_offline() {
cbb715e11963atagen2026-10-24 23:24 UTC631
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC632
    let room_id = room_id!("!mentions-context:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC633
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC634
    let mention_id = event_id!("$mention-context");
cbb715e11963atagen2026-10-24 23:24 UTC635
    let context_id = event_id!("$manual-context");
cbb715e11963atagen2026-10-24 23:24 UTC636
    let mention_event = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC637
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC638
        .text_msg("mention with saved context")
cbb715e11963atagen2026-10-24 23:24 UTC639
        .mentions(Mentions::with_user_ids([own_user.clone()]))
cbb715e11963atagen2026-10-24 23:24 UTC640
        .event_id(mention_id)
cbb715e11963atagen2026-10-24 23:24 UTC641
        .sender(&ALICE)
cbb715e11963atagen2026-10-24 23:24 UTC642
        .into_event();
cbb715e11963atagen2026-10-24 23:24 UTC643
    let context_event = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC644
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC645
        .text_msg("manually fetched context")
cbb715e11963atagen2026-10-24 23:24 UTC646
        .event_id(context_id)
cbb715e11963atagen2026-10-24 23:24 UTC647
        .sender(&ALICE)
cbb715e11963atagen2026-10-24 23:24 UTC648
        .into_event();
cbb715e11963atagen2026-10-24 23:24 UTC649
cbb715e11963atagen2026-10-24 23:24 UTC650
    server
cbb715e11963atagen2026-10-24 23:24 UTC651
        .mock_room_event_context()
cbb715e11963atagen2026-10-24 23:24 UTC652
        .room(room_id.to_owned())
cbb715e11963atagen2026-10-24 23:24 UTC653
        .match_event_id()
cbb715e11963atagen2026-10-24 23:24 UTC654
        .ok(RoomContextResponseTemplate::new(mention_event.clone())
cbb715e11963atagen2026-10-24 23:24 UTC655
            .start("mention-before")
cbb715e11963atagen2026-10-24 23:24 UTC656
            .end("mention-after"))
cbb715e11963atagen2026-10-24 23:24 UTC657
        .expect(1)
cbb715e11963atagen2026-10-24 23:24 UTC658
        .mount()
cbb715e11963atagen2026-10-24 23:24 UTC659
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC660
    server
cbb715e11963atagen2026-10-24 23:24 UTC661
        .mock_room_event_context()
cbb715e11963atagen2026-10-24 23:24 UTC662
        .room(room_id.to_owned())
cbb715e11963atagen2026-10-24 23:24 UTC663
        .match_event_id()
cbb715e11963atagen2026-10-24 23:24 UTC664
        .ok(RoomContextResponseTemplate::new(context_event)
cbb715e11963atagen2026-10-24 23:24 UTC665
            .start("manual-before")
cbb715e11963atagen2026-10-24 23:24 UTC666
            .end("manual-after"))
cbb715e11963atagen2026-10-24 23:24 UTC667
        .expect(1)
cbb715e11963atagen2026-10-24 23:24 UTC668
        .mount()
cbb715e11963atagen2026-10-24 23:24 UTC669
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC670
    mount_messages_page(&server, room_id, "b", "mention-before", 10).await;
cbb715e11963atagen2026-10-24 23:24 UTC671
    mount_messages_page(&server, room_id, "f", "mention-after", 10).await;
cbb715e11963atagen2026-10-24 23:24 UTC672
    mount_messages_page(&server, room_id, "b", "manual-before", 2).await;
cbb715e11963atagen2026-10-24 23:24 UTC673
    mount_messages_page(&server, room_id, "f", "manual-after", 3).await;
cbb715e11963atagen2026-10-24 23:24 UTC674
cbb715e11963atagen2026-10-24 23:24 UTC675
    let feed = SlidingFeed::new(
cbb715e11963atagen2026-10-24 23:24 UTC676
        room_id.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC677
        vec![message(
cbb715e11963atagen2026-10-24 23:24 UTC678
            mention_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC679
            ALICE.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC680
            5_000,
cbb715e11963atagen2026-10-24 23:24 UTC681
            "mention with saved context",
cbb715e11963atagen2026-10-24 23:24 UTC682
            Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC683
        )],
cbb715e11963atagen2026-10-24 23:24 UTC684
    );
cbb715e11963atagen2026-10-24 23:24 UTC685
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC686
    let (engine, handle, mut events) = Engine::from_client(
cbb715e11963atagen2026-10-24 23:24 UTC687
        client,
74befbc72bafatagen2026-10-25 11:18 UTC688
        EngineConfig::default()
74befbc72bafatagen2026-10-25 11:18 UTC689
            .with_initial_history_limit(0)
74befbc72bafatagen2026-10-25 11:18 UTC690
            .with_search_backfill_pages_per_room(0),
cbb715e11963atagen2026-10-24 23:24 UTC691
    )
cbb715e11963atagen2026-10-24 23:24 UTC692
    .await
cbb715e11963atagen2026-10-24 23:24 UTC693
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC694
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC695
cbb715e11963atagen2026-10-24 23:24 UTC696
    let captured = mentions_page_with_diagnostics(
cbb715e11963atagen2026-10-24 23:24 UTC697
        &mut events,
cbb715e11963atagen2026-10-24 23:24 UTC698
        room_id,
cbb715e11963atagen2026-10-24 23:24 UTC699
        |page| {
cbb715e11963atagen2026-10-24 23:24 UTC700
            page.entries.iter().any(|entry| {
cbb715e11963atagen2026-10-24 23:24 UTC701
                entry.key.event_id == mention_id
cbb715e11963atagen2026-10-24 23:24 UTC702
                    && matches!(&entry.capture, macltok::MentionCaptureState::Partial)
cbb715e11963atagen2026-10-24 23:24 UTC703
            })
cbb715e11963atagen2026-10-24 23:24 UTC704
        },
cbb715e11963atagen2026-10-24 23:24 UTC705
        Some(&server),
cbb715e11963atagen2026-10-24 23:24 UTC706
    )
cbb715e11963atagen2026-10-24 23:24 UTC707
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC708
    let saved = handle.mention_context(room_id, mention_id).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC709
    assert!(saved.context.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC710
    assert!(matches!(saved.state, macltok::MentionCaptureState::Partial));
cbb715e11963atagen2026-10-24 23:24 UTC711
cbb715e11963atagen2026-10-24 23:24 UTC712
    let before_saved_request_count = server
cbb715e11963atagen2026-10-24 23:24 UTC713
        .server()
cbb715e11963atagen2026-10-24 23:24 UTC714
        .received_requests()
cbb715e11963atagen2026-10-24 23:24 UTC715
        .await
cbb715e11963atagen2026-10-24 23:24 UTC716
        .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC717
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC718
        .filter(|request| request.url.path().contains("/context/$mention-context"))
cbb715e11963atagen2026-10-24 23:24 UTC719
        .count();
cbb715e11963atagen2026-10-24 23:24 UTC720
    let _ = handle.mention_context(room_id, mention_id).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC721
    let after_saved_request_count = server
cbb715e11963atagen2026-10-24 23:24 UTC722
        .server()
cbb715e11963atagen2026-10-24 23:24 UTC723
        .received_requests()
cbb715e11963atagen2026-10-24 23:24 UTC724
        .await
cbb715e11963atagen2026-10-24 23:24 UTC725
        .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC726
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC727
        .filter(|request| request.url.path().contains("/context/$mention-context"))
cbb715e11963atagen2026-10-24 23:24 UTC728
        .count();
cbb715e11963atagen2026-10-24 23:24 UTC729
    assert_eq!(after_saved_request_count, before_saved_request_count);
cbb715e11963atagen2026-10-24 23:24 UTC730
da8896938cf0atagen2026-10-25 10:04 UTC731
    let session = handle
cbb715e11963atagen2026-10-24 23:24 UTC732
        .event_context(room_id, context_id, 2, 3)
cbb715e11963atagen2026-10-24 23:24 UTC733
        .await
cbb715e11963atagen2026-10-24 23:24 UTC734
        .unwrap();
da8896938cf0atagen2026-10-25 10:04 UTC735
    let context = &session.context;
0e081dee5e28atagen2026-10-25 13:57 UTC736
    assert_eq!(
0e081dee5e28atagen2026-10-25 13:57 UTC737
        context.start_token, None,
0e081dee5e28atagen2026-10-25 13:57 UTC738
        "an empty page going back is the start"
0e081dee5e28atagen2026-10-25 13:57 UTC739
    );
cbb715e11963atagen2026-10-24 23:24 UTC740
    assert_eq!(context.end_token.as_deref(), Some("manual-after-next"));
da8896938cf0atagen2026-10-25 10:04 UTC741
    handle
da8896938cf0atagen2026-10-25 10:04 UTC742
        .close_event_context(&session.context_id)
da8896938cf0atagen2026-10-25 10:04 UTC743
        .await
da8896938cf0atagen2026-10-25 10:04 UTC744
        .unwrap();
da8896938cf0atagen2026-10-25 10:04 UTC745
    let closed = handle
da8896938cf0atagen2026-10-25 10:04 UTC746
        .event_context_before(&session.context_id, "manual-before-next", 5)
da8896938cf0atagen2026-10-25 10:04 UTC747
        .await
da8896938cf0atagen2026-10-25 10:04 UTC748
        .unwrap_err();
da8896938cf0atagen2026-10-25 10:04 UTC749
    assert_eq!(closed.kind, macltok::ErrorKind::EventContextNotOpen);
cbb715e11963atagen2026-10-24 23:24 UTC750
cbb715e11963atagen2026-10-24 23:24 UTC751
    let requests = server.server().received_requests().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC752
    let context_limits: Vec<_> = requests
cbb715e11963atagen2026-10-24 23:24 UTC753
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC754
        .filter(|request| request.url.path().contains("/context/"))
cbb715e11963atagen2026-10-24 23:24 UTC755
        .map(|request| {
cbb715e11963atagen2026-10-24 23:24 UTC756
            request
cbb715e11963atagen2026-10-24 23:24 UTC757
                .url
cbb715e11963atagen2026-10-24 23:24 UTC758
                .query_pairs()
cbb715e11963atagen2026-10-24 23:24 UTC759
                .find(|(key, _)| key == "limit")
cbb715e11963atagen2026-10-24 23:24 UTC760
                .map(|(_, value)| value.into_owned())
cbb715e11963atagen2026-10-24 23:24 UTC761
                .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC762
        })
cbb715e11963atagen2026-10-24 23:24 UTC763
        .collect();
cbb715e11963atagen2026-10-24 23:24 UTC764
    assert_eq!(context_limits, ["0", "0"]);
cbb715e11963atagen2026-10-24 23:24 UTC765
    let queries: Vec<_> = requests
cbb715e11963atagen2026-10-24 23:24 UTC766
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC767
        .filter(|request| request.url.path().ends_with("/messages"))
cbb715e11963atagen2026-10-24 23:24 UTC768
        .map(|request| {
cbb715e11963atagen2026-10-24 23:24 UTC769
            let query: std::collections::HashMap<_, _> =
cbb715e11963atagen2026-10-24 23:24 UTC770
                request.url.query_pairs().into_owned().collect();
cbb715e11963atagen2026-10-24 23:24 UTC771
            (
cbb715e11963atagen2026-10-24 23:24 UTC772
                query.get("dir").cloned().unwrap(),
cbb715e11963atagen2026-10-24 23:24 UTC773
                query.get("from").cloned().unwrap(),
cbb715e11963atagen2026-10-24 23:24 UTC774
                query
cbb715e11963atagen2026-10-24 23:24 UTC775
                    .get("limit")
cbb715e11963atagen2026-10-24 23:24 UTC776
                    .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC777
                    .unwrap_or_else(|| "10".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC778
            )
cbb715e11963atagen2026-10-24 23:24 UTC779
        })
cbb715e11963atagen2026-10-24 23:24 UTC780
        .collect();
cbb715e11963atagen2026-10-24 23:24 UTC781
    assert!(queries.contains(&("b".to_owned(), "mention-before".to_owned(), "10".to_owned())));
cbb715e11963atagen2026-10-24 23:24 UTC782
    assert!(queries.contains(&("f".to_owned(), "mention-after".to_owned(), "10".to_owned())));
cbb715e11963atagen2026-10-24 23:24 UTC783
    assert!(queries.contains(&("b".to_owned(), "manual-before".to_owned(), "2".to_owned())));
cbb715e11963atagen2026-10-24 23:24 UTC784
    assert!(queries.contains(&("f".to_owned(), "manual-after".to_owned(), "3".to_owned())));
cbb715e11963atagen2026-10-24 23:24 UTC785
    assert_eq!(captured.unread_count, 1);
cbb715e11963atagen2026-10-24 23:24 UTC786
cbb715e11963atagen2026-10-24 23:24 UTC787
    shut_down(&handle, &mut events, running).await;
96680d38cc31atagen2026-10-25 09:20 UTC788
    server.server().verify().await;
96680d38cc31atagen2026-10-25 09:20 UTC789
}
96680d38cc31atagen2026-10-25 09:20 UTC790
96680d38cc31atagen2026-10-25 09:20 UTC791
#[tokio::test]
96680d38cc31atagen2026-10-25 09:20 UTC792
async fn highlighting_keywords_are_mentions_and_quiet_ones_are_not() {
96680d38cc31atagen2026-10-25 09:20 UTC793
    let server = MatrixMockServer::new().await;
96680d38cc31atagen2026-10-25 09:20 UTC794
    let client = server.client_builder().build().await;
96680d38cc31atagen2026-10-25 09:20 UTC795
    let own_user = client.user_id().unwrap().to_owned();
96680d38cc31atagen2026-10-25 09:20 UTC796
    let room_id = room_id!("!keyword-policy:localhost");
96680d38cc31atagen2026-10-25 09:20 UTC797
    let sender = user_id!("@alice:localhost");
96680d38cc31atagen2026-10-25 09:20 UTC798
    let mut rules = Ruleset::server_default(&own_user);
96680d38cc31atagen2026-10-25 09:20 UTC799
    let notify = [
96680d38cc31atagen2026-10-25 09:20 UTC800
        Action::Notify,
96680d38cc31atagen2026-10-25 09:20 UTC801
        Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
96680d38cc31atagen2026-10-25 09:20 UTC802
    ];
96680d38cc31atagen2026-10-25 09:20 UTC803
    let mut loud = notify.to_vec();
96680d38cc31atagen2026-10-25 09:20 UTC804
    loud.push(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)));
96680d38cc31atagen2026-10-25 09:20 UTC805
    rules
96680d38cc31atagen2026-10-25 09:20 UTC806
        .content
96680d38cc31atagen2026-10-25 09:20 UTC807
        .insert(NewPatternedPushRule::new("deploy".to_owned(), "deploy".to_owned(), loud).into());
96680d38cc31atagen2026-10-25 09:20 UTC808
    rules.content.insert(
96680d38cc31atagen2026-10-25 09:20 UTC809
        NewPatternedPushRule::new("lunch".to_owned(), "lunch".to_owned(), notify.to_vec()).into(),
96680d38cc31atagen2026-10-25 09:20 UTC810
    );
96680d38cc31atagen2026-10-25 09:20 UTC811
    server
96680d38cc31atagen2026-10-25 09:20 UTC812
        .mock_sync()
96680d38cc31atagen2026-10-25 09:20 UTC813
        .ok_and_run(&client, {
96680d38cc31atagen2026-10-25 09:20 UTC814
            let own_user = own_user.clone();
96680d38cc31atagen2026-10-25 09:20 UTC815
            move |builder| {
96680d38cc31atagen2026-10-25 09:20 UTC816
                let factory = EventFactory::new().room(room_id);
96680d38cc31atagen2026-10-25 09:20 UTC817
                builder.add_global_account_data(factory.push_rules(rules));
96680d38cc31atagen2026-10-25 09:20 UTC818
                builder.add_joined_room(
96680d38cc31atagen2026-10-25 09:20 UTC819
                    JoinedRoomBuilder::new(room_id).add_state_event(factory.member(&own_user)),
96680d38cc31atagen2026-10-25 09:20 UTC820
                );
96680d38cc31atagen2026-10-25 09:20 UTC821
            }
96680d38cc31atagen2026-10-25 09:20 UTC822
        })
96680d38cc31atagen2026-10-25 09:20 UTC823
        .await;
96680d38cc31atagen2026-10-25 09:20 UTC824
    let feed = SlidingFeed::new(
96680d38cc31atagen2026-10-25 09:20 UTC825
        room_id.to_owned(),
96680d38cc31atagen2026-10-25 09:20 UTC826
        vec![
96680d38cc31atagen2026-10-25 09:20 UTC827
            message(
96680d38cc31atagen2026-10-25 09:20 UTC828
                "$keyword",
96680d38cc31atagen2026-10-25 09:20 UTC829
                sender.as_str(),
96680d38cc31atagen2026-10-25 09:20 UTC830
                1_000,
96680d38cc31atagen2026-10-25 09:20 UTC831
                "the deploy is done",
96680d38cc31atagen2026-10-25 09:20 UTC832
                None,
96680d38cc31atagen2026-10-25 09:20 UTC833
            ),
96680d38cc31atagen2026-10-25 09:20 UTC834
            message("$quiet", sender.as_str(), 2_000, "lunch anyone", None),
96680d38cc31atagen2026-10-25 09:20 UTC835
            message("$plain", sender.as_str(), 3_000, "nothing to see", None),
96680d38cc31atagen2026-10-25 09:20 UTC836
            message(
96680d38cc31atagen2026-10-25 09:20 UTC837
                "$keyword-direct",
96680d38cc31atagen2026-10-25 09:20 UTC838
                sender.as_str(),
96680d38cc31atagen2026-10-25 09:20 UTC839
                4_000,
96680d38cc31atagen2026-10-25 09:20 UTC840
                "deploy now",
96680d38cc31atagen2026-10-25 09:20 UTC841
                Some(own_user.as_str()),
96680d38cc31atagen2026-10-25 09:20 UTC842
            ),
96680d38cc31atagen2026-10-25 09:20 UTC843
            message("$own-keyword", own_user.as_str(), 5_000, "my deploy", None),
96680d38cc31atagen2026-10-25 09:20 UTC844
        ],
96680d38cc31atagen2026-10-25 09:20 UTC845
    );
96680d38cc31atagen2026-10-25 09:20 UTC846
    feed.mount(&server).await;
96680d38cc31atagen2026-10-25 09:20 UTC847
96680d38cc31atagen2026-10-25 09:20 UTC848
    let (engine, handle, mut events) = Engine::from_client(
96680d38cc31atagen2026-10-25 09:20 UTC849
        client,
74befbc72bafatagen2026-10-25 11:18 UTC850
        EngineConfig::default()
74befbc72bafatagen2026-10-25 11:18 UTC851
            .with_initial_history_limit(0)
74befbc72bafatagen2026-10-25 11:18 UTC852
            .with_search_backfill_pages_per_room(0),
96680d38cc31atagen2026-10-25 09:20 UTC853
    )
96680d38cc31atagen2026-10-25 09:20 UTC854
    .await
96680d38cc31atagen2026-10-25 09:20 UTC855
    .unwrap();
96680d38cc31atagen2026-10-25 09:20 UTC856
    let running = tokio::spawn(engine.run());
96680d38cc31atagen2026-10-25 09:20 UTC857
96680d38cc31atagen2026-10-25 09:20 UTC858
    let page = mentions_page(&mut events, room_id, |page| page.entries.len() >= 2).await;
96680d38cc31atagen2026-10-25 09:20 UTC859
    assert_eq!(
96680d38cc31atagen2026-10-25 09:20 UTC860
        entry_ids(&page),
96680d38cc31atagen2026-10-25 09:20 UTC861
        HashSet::from(["$keyword".to_owned(), "$keyword-direct".to_owned()])
96680d38cc31atagen2026-10-25 09:20 UTC862
    );
96680d38cc31atagen2026-10-25 09:20 UTC863
    let reasons: BTreeMap<_, _> = page
96680d38cc31atagen2026-10-25 09:20 UTC864
        .entries
96680d38cc31atagen2026-10-25 09:20 UTC865
        .iter()
96680d38cc31atagen2026-10-25 09:20 UTC866
        .map(|entry| (entry.key.event_id.to_string(), entry.reasons.clone()))
96680d38cc31atagen2026-10-25 09:20 UTC867
        .collect();
96680d38cc31atagen2026-10-25 09:20 UTC868
    assert_eq!(reasons["$keyword"], vec![MentionReason::Keyword]);
96680d38cc31atagen2026-10-25 09:20 UTC869
    assert_eq!(reasons["$keyword-direct"], vec![MentionReason::Direct]);
96680d38cc31atagen2026-10-25 09:20 UTC870
    assert_eq!(page.unread_count, 2);
96680d38cc31atagen2026-10-25 09:20 UTC871
    assert!(
96680d38cc31atagen2026-10-25 09:20 UTC872
        page.entries
96680d38cc31atagen2026-10-25 09:20 UTC873
            .iter()
96680d38cc31atagen2026-10-25 09:20 UTC874
            .all(|entry| !entry.notification_suppressed)
96680d38cc31atagen2026-10-25 09:20 UTC875
    );
96680d38cc31atagen2026-10-25 09:20 UTC876
96680d38cc31atagen2026-10-25 09:20 UTC877
    shut_down(&handle, &mut events, running).await;
96680d38cc31atagen2026-10-25 09:20 UTC878
}
96680d38cc31atagen2026-10-25 09:20 UTC879
e9af9e0e0f31atagen2026-10-25 10:55 UTC880
fn actions_are(expected: Value) -> impl Fn(&Request) -> bool + Send + Sync + 'static {
e9af9e0e0f31atagen2026-10-25 10:55 UTC881
    move |request: &Request| {
e9af9e0e0f31atagen2026-10-25 10:55 UTC882
        serde_json::from_slice::<Value>(&request.body).is_ok_and(|body| body["actions"] == expected)
e9af9e0e0f31atagen2026-10-25 10:55 UTC883
    }
e9af9e0e0f31atagen2026-10-25 10:55 UTC884
}
e9af9e0e0f31atagen2026-10-25 10:55 UTC885
e9af9e0e0f31atagen2026-10-25 10:55 UTC886
async fn mount_keyword_rule(server: &MatrixMockServer, actions: Value) {
e9af9e0e0f31atagen2026-10-25 10:55 UTC887
    Mock::given(method("GET"))
e9af9e0e0f31atagen2026-10-25 10:55 UTC888
        .and(path("/_matrix/client/v3/pushrules/global/content/deploy"))
e9af9e0e0f31atagen2026-10-25 10:55 UTC889
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
e9af9e0e0f31atagen2026-10-25 10:55 UTC890
            "rule_id": "deploy",
e9af9e0e0f31atagen2026-10-25 10:55 UTC891
            "pattern": "deploy",
e9af9e0e0f31atagen2026-10-25 10:55 UTC892
            "default": false,
e9af9e0e0f31atagen2026-10-25 10:55 UTC893
            "enabled": true,
e9af9e0e0f31atagen2026-10-25 10:55 UTC894
            "actions": actions,
e9af9e0e0f31atagen2026-10-25 10:55 UTC895
        })))
e9af9e0e0f31atagen2026-10-25 10:55 UTC896
        .mount(server.server())
e9af9e0e0f31atagen2026-10-25 10:55 UTC897
        .await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC898
}
e9af9e0e0f31atagen2026-10-25 10:55 UTC899
96680d38cc31atagen2026-10-25 09:20 UTC900
#[tokio::test]
96680d38cc31atagen2026-10-25 09:20 UTC901
async fn adding_a_keyword_gives_its_rule_a_highlight() {
96680d38cc31atagen2026-10-25 09:20 UTC902
    let server = MatrixMockServer::new().await;
96680d38cc31atagen2026-10-25 09:20 UTC903
    let room_id = room_id!("!keyword-rule:localhost");
96680d38cc31atagen2026-10-25 09:20 UTC904
    let (client, _) = joined_client(&server, room_id).await;
96680d38cc31atagen2026-10-25 09:20 UTC905
    Mock::given(method("PUT"))
96680d38cc31atagen2026-10-25 09:20 UTC906
        .and(path("/_matrix/client/v3/pushrules/global/content/deploy"))
96680d38cc31atagen2026-10-25 09:20 UTC907
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
96680d38cc31atagen2026-10-25 09:20 UTC908
        .expect(1)
96680d38cc31atagen2026-10-25 09:20 UTC909
        .mount(server.server())
96680d38cc31atagen2026-10-25 09:20 UTC910
        .await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC911
    mount_keyword_rule(
e9af9e0e0f31atagen2026-10-25 10:55 UTC912
        &server,
e9af9e0e0f31atagen2026-10-25 10:55 UTC913
        json!(["notify", {"set_tweak": "sound", "value": "default"}]),
e9af9e0e0f31atagen2026-10-25 10:55 UTC914
    )
e9af9e0e0f31atagen2026-10-25 10:55 UTC915
    .await;
96680d38cc31atagen2026-10-25 09:20 UTC916
    Mock::given(method("PUT"))
96680d38cc31atagen2026-10-25 09:20 UTC917
        .and(path(
96680d38cc31atagen2026-10-25 09:20 UTC918
            "/_matrix/client/v3/pushrules/global/content/deploy/actions",
96680d38cc31atagen2026-10-25 09:20 UTC919
        ))
e9af9e0e0f31atagen2026-10-25 10:55 UTC920
        .and(actions_are(json!([
e9af9e0e0f31atagen2026-10-25 10:55 UTC921
            "notify",
e9af9e0e0f31atagen2026-10-25 10:55 UTC922
            {"set_tweak": "sound", "value": "default"},
e9af9e0e0f31atagen2026-10-25 10:55 UTC923
            {"set_tweak": "highlight"},
e9af9e0e0f31atagen2026-10-25 10:55 UTC924
        ])))
e9af9e0e0f31atagen2026-10-25 10:55 UTC925
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
e9af9e0e0f31atagen2026-10-25 10:55 UTC926
        .expect(1)
e9af9e0e0f31atagen2026-10-25 10:55 UTC927
        .mount(server.server())
e9af9e0e0f31atagen2026-10-25 10:55 UTC928
        .await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC929
    SlidingFeed::new(room_id.to_owned(), Vec::new())
e9af9e0e0f31atagen2026-10-25 10:55 UTC930
        .mount(&server)
e9af9e0e0f31atagen2026-10-25 10:55 UTC931
        .await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC932
74befbc72bafatagen2026-10-25 11:18 UTC933
    let (engine, handle, mut events) = Engine::from_client(
74befbc72bafatagen2026-10-25 11:18 UTC934
        client,
74befbc72bafatagen2026-10-25 11:18 UTC935
        EngineConfig::default().with_search_backfill_pages_per_room(0),
74befbc72bafatagen2026-10-25 11:18 UTC936
    )
74befbc72bafatagen2026-10-25 11:18 UTC937
    .await
74befbc72bafatagen2026-10-25 11:18 UTC938
    .unwrap();
e9af9e0e0f31atagen2026-10-25 10:55 UTC939
    let running = tokio::spawn(engine.run());
e9af9e0e0f31atagen2026-10-25 10:55 UTC940
    handle.add_notification_keyword("deploy").await.unwrap();
e9af9e0e0f31atagen2026-10-25 10:55 UTC941
    shut_down(&handle, &mut events, running).await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC942
    server.server().verify().await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC943
}
e9af9e0e0f31atagen2026-10-25 10:55 UTC944
e9af9e0e0f31atagen2026-10-25 10:55 UTC945
#[tokio::test]
e9af9e0e0f31atagen2026-10-25 10:55 UTC946
async fn a_quiet_keyword_rule_only_gains_the_highlight() {
e9af9e0e0f31atagen2026-10-25 10:55 UTC947
    let server = MatrixMockServer::new().await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC948
    let client = server.client_builder().build().await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC949
    let own_user = client.user_id().unwrap().to_owned();
e9af9e0e0f31atagen2026-10-25 10:55 UTC950
    let room_id = room_id!("!quiet-keyword-rule:localhost");
e9af9e0e0f31atagen2026-10-25 10:55 UTC951
    let mut rules = Ruleset::server_default(&own_user);
e9af9e0e0f31atagen2026-10-25 10:55 UTC952
    rules.content.insert(
e9af9e0e0f31atagen2026-10-25 10:55 UTC953
        NewPatternedPushRule::new("deploy".to_owned(), "deploy".to_owned(), Vec::new()).into(),
e9af9e0e0f31atagen2026-10-25 10:55 UTC954
    );
e9af9e0e0f31atagen2026-10-25 10:55 UTC955
    server
e9af9e0e0f31atagen2026-10-25 10:55 UTC956
        .mock_sync()
e9af9e0e0f31atagen2026-10-25 10:55 UTC957
        .ok_and_run(&client, {
e9af9e0e0f31atagen2026-10-25 10:55 UTC958
            let own_user = own_user.clone();
e9af9e0e0f31atagen2026-10-25 10:55 UTC959
            move |builder| {
e9af9e0e0f31atagen2026-10-25 10:55 UTC960
                let factory = EventFactory::new().room(room_id);
e9af9e0e0f31atagen2026-10-25 10:55 UTC961
                builder.add_global_account_data(factory.push_rules(rules));
e9af9e0e0f31atagen2026-10-25 10:55 UTC962
                builder.add_joined_room(
e9af9e0e0f31atagen2026-10-25 10:55 UTC963
                    JoinedRoomBuilder::new(room_id).add_state_event(factory.member(&own_user)),
e9af9e0e0f31atagen2026-10-25 10:55 UTC964
                );
e9af9e0e0f31atagen2026-10-25 10:55 UTC965
            }
e9af9e0e0f31atagen2026-10-25 10:55 UTC966
        })
e9af9e0e0f31atagen2026-10-25 10:55 UTC967
        .await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC968
    mount_keyword_rule(&server, json!([])).await;
e9af9e0e0f31atagen2026-10-25 10:55 UTC969
    Mock::given(method("PUT"))
e9af9e0e0f31atagen2026-10-25 10:55 UTC970
        .and(path(
e9af9e0e0f31atagen2026-10-25 10:55 UTC971
            "/_matrix/client/v3/pushrules/global/content/deploy/actions",
e9af9e0e0f31atagen2026-10-25 10:55 UTC972
        ))
fbeaaa098107atagen2026-10-25 12:10 UTC973
        .and(actions_are(json!([{"set_tweak": "highlight"}])))
96680d38cc31atagen2026-10-25 09:20 UTC974
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
96680d38cc31atagen2026-10-25 09:20 UTC975
        .expect(1)
96680d38cc31atagen2026-10-25 09:20 UTC976
        .mount(server.server())
96680d38cc31atagen2026-10-25 09:20 UTC977
        .await;
96680d38cc31atagen2026-10-25 09:20 UTC978
    SlidingFeed::new(room_id.to_owned(), Vec::new())
96680d38cc31atagen2026-10-25 09:20 UTC979
        .mount(&server)
96680d38cc31atagen2026-10-25 09:20 UTC980
        .await;
96680d38cc31atagen2026-10-25 09:20 UTC981
74befbc72bafatagen2026-10-25 11:18 UTC982
    let (engine, handle, mut events) = Engine::from_client(
74befbc72bafatagen2026-10-25 11:18 UTC983
        client,
74befbc72bafatagen2026-10-25 11:18 UTC984
        EngineConfig::default().with_search_backfill_pages_per_room(0),
74befbc72bafatagen2026-10-25 11:18 UTC985
    )
74befbc72bafatagen2026-10-25 11:18 UTC986
    .await
74befbc72bafatagen2026-10-25 11:18 UTC987
    .unwrap();
96680d38cc31atagen2026-10-25 09:20 UTC988
    let running = tokio::spawn(engine.run());
96680d38cc31atagen2026-10-25 09:20 UTC989
    handle.add_notification_keyword("deploy").await.unwrap();
96680d38cc31atagen2026-10-25 09:20 UTC990
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC991
    server.server().verify().await;
cbb715e11963atagen2026-10-24 23:24 UTC992
}
cbb715e11963atagen2026-10-24 23:24 UTC993
cbb715e11963atagen2026-10-24 23:24 UTC994
#[cfg(feature = "sqlite")]
cbb715e11963atagen2026-10-24 23:24 UTC995
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC996
async fn sqlite_mentions_survive_engine_shutdown_and_session_resume() {
cbb715e11963atagen2026-10-24 23:24 UTC997
    static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);
cbb715e11963atagen2026-10-24 23:24 UTC998
cbb715e11963atagen2026-10-24 23:24 UTC999
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC1000
    server
cbb715e11963atagen2026-10-24 23:24 UTC1001
        .mock_versions()
cbb715e11963atagen2026-10-24 23:24 UTC1002
        .with_simplified_sliding_sync()
cbb715e11963atagen2026-10-24 23:24 UTC1003
        .ok()
cbb715e11963atagen2026-10-24 23:24 UTC1004
        .mount()
cbb715e11963atagen2026-10-24 23:24 UTC1005
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC1006
    server.mock_login().ok().mount().await;
cbb715e11963atagen2026-10-24 23:24 UTC1007
cbb715e11963atagen2026-10-24 23:24 UTC1008
    let root = std::env::temp_dir().join(format!(
cbb715e11963atagen2026-10-24 23:24 UTC1009
        "macltok-mentions-inbox-{}-{}",
cbb715e11963atagen2026-10-24 23:24 UTC1010
        std::process::id(),
cbb715e11963atagen2026-10-24 23:24 UTC1011
        NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)
cbb715e11963atagen2026-10-24 23:24 UTC1012
    ));
cbb715e11963atagen2026-10-24 23:24 UTC1013
    let store_path = root.join("store");
cbb715e11963atagen2026-10-24 23:24 UTC1014
    let sessions_path = root.join("records").join("session");
cbb715e11963atagen2026-10-24 23:24 UTC1015
    let sessions = Arc::new(EncryptedSessionStore::file(
cbb715e11963atagen2026-10-24 23:24 UTC1016
        &sessions_path,
cbb715e11963atagen2026-10-24 23:24 UTC1017
        StoreConfig::sqlite(&store_path, "mentions inbox test passphrase"),
cbb715e11963atagen2026-10-24 23:24 UTC1018
    ));
cbb715e11963atagen2026-10-24 23:24 UTC1019
    let room_id = room_id!("!mentions-resume:matrix.org");
cbb715e11963atagen2026-10-24 23:24 UTC1020
    let own_user = user_id!("@cheeky_monkey:matrix.org");
cbb715e11963atagen2026-10-24 23:24 UTC1021
    let feed = SlidingFeed::new(
cbb715e11963atagen2026-10-24 23:24 UTC1022
        room_id.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC1023
        vec![message(
cbb715e11963atagen2026-10-24 23:24 UTC1024
            "$durable-mention",
cbb715e11963atagen2026-10-24 23:24 UTC1025
            "@alice:matrix.org",
cbb715e11963atagen2026-10-24 23:24 UTC1026
            7_000,
cbb715e11963atagen2026-10-24 23:24 UTC1027
            "persist this mention",
cbb715e11963atagen2026-10-24 23:24 UTC1028
            Some(own_user.as_str()),
cbb715e11963atagen2026-10-24 23:24 UTC1029
        )],
cbb715e11963atagen2026-10-24 23:24 UTC1030
    );
cbb715e11963atagen2026-10-24 23:24 UTC1031
    feed.set_required_state(
cbb715e11963atagen2026-10-24 23:24 UTC1032
        room_id,
cbb715e11963atagen2026-10-24 23:24 UTC1033
        vec![json!({
cbb715e11963atagen2026-10-24 23:24 UTC1034
            "type": "m.room.member",
cbb715e11963atagen2026-10-24 23:24 UTC1035
            "event_id": "$joined-member",
cbb715e11963atagen2026-10-24 23:24 UTC1036
            "sender": own_user,
cbb715e11963atagen2026-10-24 23:24 UTC1037
            "state_key": own_user,
cbb715e11963atagen2026-10-24 23:24 UTC1038
            "origin_server_ts": 1,
cbb715e11963atagen2026-10-24 23:24 UTC1039
            "content": { "membership": "join" },
cbb715e11963atagen2026-10-24 23:24 UTC1040
        })],
cbb715e11963atagen2026-10-24 23:24 UTC1041
    );
cbb715e11963atagen2026-10-24 23:24 UTC1042
    feed.mount(&server).await;
cbb715e11963atagen2026-10-24 23:24 UTC1043
74befbc72bafatagen2026-10-25 11:18 UTC1044
    let config = EngineConfig::default()
74befbc72bafatagen2026-10-25 11:18 UTC1045
        .with_initial_history_limit(0)
74befbc72bafatagen2026-10-25 11:18 UTC1046
        .with_search_backfill_pages_per_room(0);
cbb715e11963atagen2026-10-24 23:24 UTC1047
    let session = Session::login_password(
cbb715e11963atagen2026-10-24 23:24 UTC1048
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
cbb715e11963atagen2026-10-24 23:24 UTC1049
        sessions.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC1050
    )
cbb715e11963atagen2026-10-24 23:24 UTC1051
    .await
cbb715e11963atagen2026-10-24 23:24 UTC1052
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1053
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1054
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC1055
    let first = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
cbb715e11963atagen2026-10-24 23:24 UTC1056
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC1057
        entry_ids(&first),
cbb715e11963atagen2026-10-24 23:24 UTC1058
        HashSet::from(["$durable-mention".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC1059
    );
cbb715e11963atagen2026-10-24 23:24 UTC1060
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC1061
    drop(handle);
cbb715e11963atagen2026-10-24 23:24 UTC1062
cbb715e11963atagen2026-10-24 23:24 UTC1063
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
cbb715e11963atagen2026-10-24 23:24 UTC1064
        panic!("the saved session did not resume");
cbb715e11963atagen2026-10-24 23:24 UTC1065
    };
cbb715e11963atagen2026-10-24 23:24 UTC1066
    let (engine, handle, mut events) = session.build(config).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1067
    let running = tokio::spawn(engine.run());
cbb715e11963atagen2026-10-24 23:24 UTC1068
    receive_until(&mut events, |event| {
cbb715e11963atagen2026-10-24 23:24 UTC1069
        matches!(event, Event::Authenticated { .. })
cbb715e11963atagen2026-10-24 23:24 UTC1070
    })
cbb715e11963atagen2026-10-24 23:24 UTC1071
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC1072
    let resumed = handle.mentions_page(None, 50).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1073
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC1074
        entry_ids(&resumed),
cbb715e11963atagen2026-10-24 23:24 UTC1075
        HashSet::from(["$durable-mention".to_owned()])
cbb715e11963atagen2026-10-24 23:24 UTC1076
    );
cbb715e11963atagen2026-10-24 23:24 UTC1077
    assert_eq!(resumed.unread_count, 1);
cbb715e11963atagen2026-10-24 23:24 UTC1078
    shut_down(&handle, &mut events, running).await;
cbb715e11963atagen2026-10-24 23:24 UTC1079
    drop(handle);
cbb715e11963atagen2026-10-24 23:24 UTC1080
cbb715e11963atagen2026-10-24 23:24 UTC1081
    sessions.discard().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1082
    std::fs::remove_dir_all(root).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1083
}
cbb715e11963atagen2026-10-24 23:24 UTC1084
cbb715e11963atagen2026-10-24 23:24 UTC1085
#[cfg(feature = "serde")]
cbb715e11963atagen2026-10-24 23:24 UTC1086
#[test]
cbb715e11963atagen2026-10-24 23:24 UTC1087
fn mentions_command_and_page_round_trip_through_serde() {
cbb715e11963atagen2026-10-24 23:24 UTC1088
    let command = MentionsCommand::ReadThrough(MentionsBoundary {
cbb715e11963atagen2026-10-24 23:24 UTC1089
        account: user_id!("@alice:example.org").to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC1090
        revision: 7,
cbb715e11963atagen2026-10-24 23:24 UTC1091
    });
cbb715e11963atagen2026-10-24 23:24 UTC1092
    let encoded_command = serde_json::to_value(&command).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1093
    let decoded_command: MentionsCommand = serde_json::from_value(encoded_command).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1094
    assert_eq!(decoded_command, command);
cbb715e11963atagen2026-10-24 23:24 UTC1095
cbb715e11963atagen2026-10-24 23:24 UTC1096
    let page = MentionsPage {
cbb715e11963atagen2026-10-24 23:24 UTC1097
        entries: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC1098
        next_cursor: Some("opaque cursor".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC1099
        end_reached: false,
cbb715e11963atagen2026-10-24 23:24 UTC1100
        coverage: MentionCoverage::default(),
cbb715e11963atagen2026-10-24 23:24 UTC1101
        unread_count: 0,
cbb715e11963atagen2026-10-24 23:24 UTC1102
        revision: 7,
cbb715e11963atagen2026-10-24 23:24 UTC1103
        boundary: MentionsBoundary {
cbb715e11963atagen2026-10-24 23:24 UTC1104
            account: user_id!("@alice:example.org").to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC1105
            revision: 7,
cbb715e11963atagen2026-10-24 23:24 UTC1106
        },
cbb715e11963atagen2026-10-24 23:24 UTC1107
    };
cbb715e11963atagen2026-10-24 23:24 UTC1108
    let encoded_page = serde_json::to_value(&page).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1109
    let decoded_page: MentionsPage = serde_json::from_value(encoded_page).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1110
    assert_eq!(decoded_page, page);
cbb715e11963atagen2026-10-24 23:24 UTC1111
}
b34c2df6d896atagen2026-10-25 13:52 UTC1112
b34c2df6d896atagen2026-10-25 13:52 UTC1113
#[tokio::test]
b34c2df6d896atagen2026-10-25 13:52 UTC1114
async fn inbox_rows_carry_sender_profiles_and_never_leave_replies_pending() {
b34c2df6d896atagen2026-10-25 13:52 UTC1115
    let server = MatrixMockServer::new().await;
b34c2df6d896atagen2026-10-25 13:52 UTC1116
    let room_id = room_id!("!inbox-profiles:localhost");
b34c2df6d896atagen2026-10-25 13:52 UTC1117
    let (client, own_user) = joined_client(&server, room_id).await;
b34c2df6d896atagen2026-10-25 13:52 UTC1118
    let sender = user_id!("@alice:localhost");
b34c2df6d896atagen2026-10-25 13:52 UTC1119
    let feed = SlidingFeed::new(
b34c2df6d896atagen2026-10-25 13:52 UTC1120
        room_id.to_owned(),
b34c2df6d896atagen2026-10-25 13:52 UTC1121
        vec![message("$parent", sender.as_str(), 1_000, "parent", None)],
b34c2df6d896atagen2026-10-25 13:52 UTC1122
    );
b34c2df6d896atagen2026-10-25 13:52 UTC1123
    feed.set_required_state(
b34c2df6d896atagen2026-10-25 13:52 UTC1124
        room_id,
b34c2df6d896atagen2026-10-25 13:52 UTC1125
        vec![json!({
b34c2df6d896atagen2026-10-25 13:52 UTC1126
            "type": "m.room.member",
b34c2df6d896atagen2026-10-25 13:52 UTC1127
            "state_key": sender,
b34c2df6d896atagen2026-10-25 13:52 UTC1128
            "sender": sender,
b34c2df6d896atagen2026-10-25 13:52 UTC1129
            "event_id": "$alice-member",
b34c2df6d896atagen2026-10-25 13:52 UTC1130
            "origin_server_ts": 1,
b34c2df6d896atagen2026-10-25 13:52 UTC1131
            "content": { "membership": "join", "displayname": "Alice" },
b34c2df6d896atagen2026-10-25 13:52 UTC1132
        })],
b34c2df6d896atagen2026-10-25 13:52 UTC1133
    );
b34c2df6d896atagen2026-10-25 13:52 UTC1134
    feed.mount(&server).await;
b34c2df6d896atagen2026-10-25 13:52 UTC1135
    let (engine, handle, mut events) = Engine::from_client(
b34c2df6d896atagen2026-10-25 13:52 UTC1136
        client,
b34c2df6d896atagen2026-10-25 13:52 UTC1137
        EngineConfig::default()
b34c2df6d896atagen2026-10-25 13:52 UTC1138
            .with_initial_history_limit(0)
b34c2df6d896atagen2026-10-25 13:52 UTC1139
            .with_search_backfill_pages_per_room(0),
b34c2df6d896atagen2026-10-25 13:52 UTC1140
    )
b34c2df6d896atagen2026-10-25 13:52 UTC1141
    .await
b34c2df6d896atagen2026-10-25 13:52 UTC1142
    .unwrap();
b34c2df6d896atagen2026-10-25 13:52 UTC1143
    let running = tokio::spawn(engine.run());
b34c2df6d896atagen2026-10-25 13:52 UTC1144
    wait_for_requests(&feed, 2).await;
b34c2df6d896atagen2026-10-25 13:52 UTC1145
b34c2df6d896atagen2026-10-25 13:52 UTC1146
    let reply = |event_id: &str, target: &str, timestamp| {
b34c2df6d896atagen2026-10-25 13:52 UTC1147
        let mut event = message(
b34c2df6d896atagen2026-10-25 13:52 UTC1148
            event_id,
b34c2df6d896atagen2026-10-25 13:52 UTC1149
            sender.as_str(),
b34c2df6d896atagen2026-10-25 13:52 UTC1150
            timestamp,
b34c2df6d896atagen2026-10-25 13:52 UTC1151
            "reply",
b34c2df6d896atagen2026-10-25 13:52 UTC1152
            Some(own_user.as_str()),
b34c2df6d896atagen2026-10-25 13:52 UTC1153
        );
b34c2df6d896atagen2026-10-25 13:52 UTC1154
        event["content"]["m.relates_to"] = json!({ "m.in_reply_to": { "event_id": target } });
b34c2df6d896atagen2026-10-25 13:52 UTC1155
        event
b34c2df6d896atagen2026-10-25 13:52 UTC1156
    };
6b04099c763catagen2026-10-25 14:17 UTC1157
    let answer = reply("$answer", "$parent", 2_000);
6b04099c763catagen2026-10-25 14:17 UTC1158
    let orphan = reply("$orphan", "$missing", 2_100);
6b04099c763catagen2026-10-25 14:17 UTC1159
    for event in [&answer, &orphan] {
6b04099c763catagen2026-10-25 14:17 UTC1160
        let mut event = event.clone();
6b04099c763catagen2026-10-25 14:17 UTC1161
        event["room_id"] = json!(room_id);
6b04099c763catagen2026-10-25 14:17 UTC1162
        server
6b04099c763catagen2026-10-25 14:17 UTC1163
            .mock_room_event_context()
6b04099c763catagen2026-10-25 14:17 UTC1164
            .room(room_id.to_owned())
6b04099c763catagen2026-10-25 14:17 UTC1165
            .match_event_id()
6b04099c763catagen2026-10-25 14:17 UTC1166
            .ok(RoomContextResponseTemplate::new(
6b04099c763catagen2026-10-25 14:17 UTC1167
                matrix_sdk::deserialized_responses::TimelineEvent::from_plaintext(
6b04099c763catagen2026-10-25 14:17 UTC1168
                    ruma::serde::Raw::new(&event).unwrap().cast_unchecked(),
6b04099c763catagen2026-10-25 14:17 UTC1169
                ),
6b04099c763catagen2026-10-25 14:17 UTC1170
            ))
6b04099c763catagen2026-10-25 14:17 UTC1171
            .mount()
6b04099c763catagen2026-10-25 14:17 UTC1172
            .await;
6b04099c763catagen2026-10-25 14:17 UTC1173
    }
6b04099c763catagen2026-10-25 14:17 UTC1174
    feed.replace(room_id, vec![answer, orphan]);
6b04099c763catagen2026-10-25 14:17 UTC1175
    let settled = |page: &MentionsPage| {
6b04099c763catagen2026-10-25 14:17 UTC1176
        page.entries.len() == 2
6b04099c763catagen2026-10-25 14:17 UTC1177
            && page.entries.iter().all(|entry| {
6b04099c763catagen2026-10-25 14:17 UTC1178
                matches!(&entry.event, macltok::TimelineItem::Event(event)
6b04099c763catagen2026-10-25 14:17 UTC1179
                if event.sender.display_name.is_some()
6b04099c763catagen2026-10-25 14:17 UTC1180
                    && event.reply_to.as_ref().is_some_and(|reply| {
6b04099c763catagen2026-10-25 14:17 UTC1181
                        !matches!(reply.details, macltok::ReplyDetails::Pending)
6b04099c763catagen2026-10-25 14:17 UTC1182
                    }))
6b04099c763catagen2026-10-25 14:17 UTC1183
            })
6b04099c763catagen2026-10-25 14:17 UTC1184
    };
6b04099c763catagen2026-10-25 14:17 UTC1185
    let page = mentions_page(&mut events, room_id, |page| {
6b04099c763catagen2026-10-25 14:17 UTC1186
        settled(page)
6b04099c763catagen2026-10-25 14:17 UTC1187
            && page.entries.iter().any(|entry| {
6b04099c763catagen2026-10-25 14:17 UTC1188
                matches!(&entry.event, macltok::TimelineItem::Event(event)
6b04099c763catagen2026-10-25 14:17 UTC1189
                    if matches!(event.reply_to.as_ref().unwrap().details,
6b04099c763catagen2026-10-25 14:17 UTC1190
                        macltok::ReplyDetails::Ready { .. }))
6b04099c763catagen2026-10-25 14:17 UTC1191
            })
6b04099c763catagen2026-10-25 14:17 UTC1192
    })
6b04099c763catagen2026-10-25 14:17 UTC1193
    .await;
b34c2df6d896atagen2026-10-25 13:52 UTC1194
    for entry in &page.entries {
b34c2df6d896atagen2026-10-25 13:52 UTC1195
        let macltok::TimelineItem::Event(event) = &entry.event else {
b34c2df6d896atagen2026-10-25 13:52 UTC1196
            panic!("a mention should be an event");
b34c2df6d896atagen2026-10-25 13:52 UTC1197
        };
b34c2df6d896atagen2026-10-25 13:52 UTC1198
        assert_eq!(event.sender.display_name.as_deref(), Some("Alice"));
b34c2df6d896atagen2026-10-25 13:52 UTC1199
        let details = &event
b34c2df6d896atagen2026-10-25 13:52 UTC1200
            .reply_to
b34c2df6d896atagen2026-10-25 13:52 UTC1201
            .as_ref()
b34c2df6d896atagen2026-10-25 13:52 UTC1202
            .expect("both mentions reply")
b34c2df6d896atagen2026-10-25 13:52 UTC1203
            .details;
b34c2df6d896atagen2026-10-25 13:52 UTC1204
        match entry.key.event_id.as_str() {
b34c2df6d896atagen2026-10-25 13:52 UTC1205
            "$answer" => assert!(matches!(details, macltok::ReplyDetails::Ready { .. })),
b34c2df6d896atagen2026-10-25 13:52 UTC1206
            _ => assert_eq!(details, &macltok::ReplyDetails::Unavailable),
b34c2df6d896atagen2026-10-25 13:52 UTC1207
        }
b34c2df6d896atagen2026-10-25 13:52 UTC1208
    }
b34c2df6d896atagen2026-10-25 13:52 UTC1209
    shut_down(&handle, &mut events, running).await;
b34c2df6d896atagen2026-10-25 13:52 UTC1210
}
6b04099c763catagen2026-10-25 14:17 UTC1211
6b04099c763catagen2026-10-25 14:17 UTC1212
#[cfg(feature = "sqlite")]
6b04099c763catagen2026-10-25 14:17 UTC1213
#[tokio::test]
6b04099c763catagen2026-10-25 14:17 UTC1214
async fn a_resumed_inbox_with_threaded_replies_still_answers_a_page() {
6b04099c763catagen2026-10-25 14:17 UTC1215
    static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);
6b04099c763catagen2026-10-25 14:17 UTC1216
6b04099c763catagen2026-10-25 14:17 UTC1217
    let server = MatrixMockServer::new().await;
6b04099c763catagen2026-10-25 14:17 UTC1218
    server
6b04099c763catagen2026-10-25 14:17 UTC1219
        .mock_versions()
6b04099c763catagen2026-10-25 14:17 UTC1220
        .with_simplified_sliding_sync()
6b04099c763catagen2026-10-25 14:17 UTC1221
        .ok()
6b04099c763catagen2026-10-25 14:17 UTC1222
        .mount()
6b04099c763catagen2026-10-25 14:17 UTC1223
        .await;
6b04099c763catagen2026-10-25 14:17 UTC1224
    server.mock_login().ok().mount().await;
6b04099c763catagen2026-10-25 14:17 UTC1225
    let root = std::env::temp_dir().join(format!(
6b04099c763catagen2026-10-25 14:17 UTC1226
        "macltok-mentions-threads-{}-{}",
6b04099c763catagen2026-10-25 14:17 UTC1227
        std::process::id(),
6b04099c763catagen2026-10-25 14:17 UTC1228
        NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)
6b04099c763catagen2026-10-25 14:17 UTC1229
    ));
6b04099c763catagen2026-10-25 14:17 UTC1230
    let sessions = Arc::new(EncryptedSessionStore::file(
6b04099c763catagen2026-10-25 14:17 UTC1231
        root.join("records").join("session"),
6b04099c763catagen2026-10-25 14:17 UTC1232
        StoreConfig::sqlite(root.join("store"), "mentions thread test passphrase"),
6b04099c763catagen2026-10-25 14:17 UTC1233
    ));
6b04099c763catagen2026-10-25 14:17 UTC1234
    let room_id = room_id!("!mentions-threads:matrix.org");
6b04099c763catagen2026-10-25 14:17 UTC1235
    let own_user = user_id!("@cheeky_monkey:matrix.org");
6b04099c763catagen2026-10-25 14:17 UTC1236
    let alice = "@alice:matrix.org";
6b04099c763catagen2026-10-25 14:17 UTC1237
    let threaded = |event_id: &str, timestamp| {
6b04099c763catagen2026-10-25 14:17 UTC1238
        let mut event = message(
6b04099c763catagen2026-10-25 14:17 UTC1239
            event_id,
6b04099c763catagen2026-10-25 14:17 UTC1240
            alice,
6b04099c763catagen2026-10-25 14:17 UTC1241
            timestamp,
6b04099c763catagen2026-10-25 14:17 UTC1242
            "threaded mention",
6b04099c763catagen2026-10-25 14:17 UTC1243
            Some(own_user.as_str()),
6b04099c763catagen2026-10-25 14:17 UTC1244
        );
6b04099c763catagen2026-10-25 14:17 UTC1245
        event["content"]["m.relates_to"] = json!({
6b04099c763catagen2026-10-25 14:17 UTC1246
            "rel_type": "m.thread",
6b04099c763catagen2026-10-25 14:17 UTC1247
            "event_id": "$root",
6b04099c763catagen2026-10-25 14:17 UTC1248
            "is_falling_back": true,
6b04099c763catagen2026-10-25 14:17 UTC1249
            "m.in_reply_to": { "event_id": "$root" },
6b04099c763catagen2026-10-25 14:17 UTC1250
        });
6b04099c763catagen2026-10-25 14:17 UTC1251
        event
6b04099c763catagen2026-10-25 14:17 UTC1252
    };
6b04099c763catagen2026-10-25 14:17 UTC1253
    let feed = SlidingFeed::new(
6b04099c763catagen2026-10-25 14:17 UTC1254
        room_id.to_owned(),
6b04099c763catagen2026-10-25 14:17 UTC1255
        vec![
6b04099c763catagen2026-10-25 14:17 UTC1256
            message("$root", alice, 6_000, "thread root", None),
6b04099c763catagen2026-10-25 14:17 UTC1257
            threaded("$thread-a", 7_000),
6b04099c763catagen2026-10-25 14:17 UTC1258
            threaded("$thread-b", 7_100),
6b04099c763catagen2026-10-25 14:17 UTC1259
            message(
6b04099c763catagen2026-10-25 14:17 UTC1260
                "$plain",
6b04099c763catagen2026-10-25 14:17 UTC1261
                alice,
6b04099c763catagen2026-10-25 14:17 UTC1262
                7_200,
6b04099c763catagen2026-10-25 14:17 UTC1263
                "plain mention",
6b04099c763catagen2026-10-25 14:17 UTC1264
                Some(own_user.as_str()),
6b04099c763catagen2026-10-25 14:17 UTC1265
            ),
6b04099c763catagen2026-10-25 14:17 UTC1266
        ],
6b04099c763catagen2026-10-25 14:17 UTC1267
    );
6b04099c763catagen2026-10-25 14:17 UTC1268
    feed.set_required_state(
6b04099c763catagen2026-10-25 14:17 UTC1269
        room_id,
6b04099c763catagen2026-10-25 14:17 UTC1270
        vec![json!({
6b04099c763catagen2026-10-25 14:17 UTC1271
            "type": "m.room.member",
6b04099c763catagen2026-10-25 14:17 UTC1272
            "event_id": "$joined-member",
6b04099c763catagen2026-10-25 14:17 UTC1273
            "sender": own_user,
6b04099c763catagen2026-10-25 14:17 UTC1274
            "state_key": own_user,
6b04099c763catagen2026-10-25 14:17 UTC1275
            "origin_server_ts": 1,
6b04099c763catagen2026-10-25 14:17 UTC1276
            "content": { "membership": "join" },
6b04099c763catagen2026-10-25 14:17 UTC1277
        })],
6b04099c763catagen2026-10-25 14:17 UTC1278
    );
6b04099c763catagen2026-10-25 14:17 UTC1279
    feed.mount(&server).await;
6b04099c763catagen2026-10-25 14:17 UTC1280
6b04099c763catagen2026-10-25 14:17 UTC1281
    let config = EngineConfig::default()
6b04099c763catagen2026-10-25 14:17 UTC1282
        .with_initial_history_limit(0)
6b04099c763catagen2026-10-25 14:17 UTC1283
        .with_search_backfill_pages_per_room(0);
6b04099c763catagen2026-10-25 14:17 UTC1284
    let session = Session::login_password(
6b04099c763catagen2026-10-25 14:17 UTC1285
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
6b04099c763catagen2026-10-25 14:17 UTC1286
        sessions.clone(),
6b04099c763catagen2026-10-25 14:17 UTC1287
    )
6b04099c763catagen2026-10-25 14:17 UTC1288
    .await
6b04099c763catagen2026-10-25 14:17 UTC1289
    .unwrap();
6b04099c763catagen2026-10-25 14:17 UTC1290
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
6b04099c763catagen2026-10-25 14:17 UTC1291
    let running = tokio::spawn(engine.run());
6b04099c763catagen2026-10-25 14:17 UTC1292
    mentions_page(&mut events, room_id, |page| page.entries.len() == 3).await;
6b04099c763catagen2026-10-25 14:17 UTC1293
    shut_down(&handle, &mut events, running).await;
6b04099c763catagen2026-10-25 14:17 UTC1294
    drop(handle);
6b04099c763catagen2026-10-25 14:17 UTC1295
6b04099c763catagen2026-10-25 14:17 UTC1296
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
6b04099c763catagen2026-10-25 14:17 UTC1297
        panic!("the saved session did not resume");
6b04099c763catagen2026-10-25 14:17 UTC1298
    };
6b04099c763catagen2026-10-25 14:17 UTC1299
    let (engine, handle, mut events) = session.build(config).await.unwrap();
6b04099c763catagen2026-10-25 14:17 UTC1300
    let running = tokio::spawn(engine.run());
6b04099c763catagen2026-10-25 14:17 UTC1301
    receive_until(&mut events, |event| {
6b04099c763catagen2026-10-25 14:17 UTC1302
        matches!(event, Event::Authenticated { .. })
6b04099c763catagen2026-10-25 14:17 UTC1303
    })
6b04099c763catagen2026-10-25 14:17 UTC1304
    .await;
6b04099c763catagen2026-10-25 14:17 UTC1305
    wait_for_requests(&feed, 4).await;
6b04099c763catagen2026-10-25 14:17 UTC1306
    let resumed = tokio::time::timeout(Duration::from_secs(5), handle.mentions_page(None, 50))
6b04099c763catagen2026-10-25 14:17 UTC1307
        .await
6b04099c763catagen2026-10-25 14:17 UTC1308
        .expect("the resumed inbox did not answer a page")
6b04099c763catagen2026-10-25 14:17 UTC1309
        .unwrap();
6b04099c763catagen2026-10-25 14:17 UTC1310
    assert_eq!(resumed.entries.len(), 3);
6b04099c763catagen2026-10-25 14:17 UTC1311
    shut_down(&handle, &mut events, running).await;
6b04099c763catagen2026-10-25 14:17 UTC1312
    drop(handle);
6b04099c763catagen2026-10-25 14:17 UTC1313
6b04099c763catagen2026-10-25 14:17 UTC1314
    sessions.discard().await.unwrap();
6b04099c763catagen2026-10-25 14:17 UTC1315
    std::fs::remove_dir_all(root).unwrap();
6b04099c763catagen2026-10-25 14:17 UTC1316
}
8d93a5d23addatagen2026-10-26 05:28 UTC1317
8d93a5d23addatagen2026-10-26 05:28 UTC1318
#[tokio::test]
8d93a5d23addatagen2026-10-26 05:28 UTC1319
async fn a_resumed_room_with_only_memberships_still_opens() {
8d93a5d23addatagen2026-10-26 05:28 UTC1320
    static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);
8d93a5d23addatagen2026-10-26 05:28 UTC1321
8d93a5d23addatagen2026-10-26 05:28 UTC1322
    let server = MatrixMockServer::new().await;
8d93a5d23addatagen2026-10-26 05:28 UTC1323
    server
8d93a5d23addatagen2026-10-26 05:28 UTC1324
        .mock_versions()
8d93a5d23addatagen2026-10-26 05:28 UTC1325
        .with_simplified_sliding_sync()
8d93a5d23addatagen2026-10-26 05:28 UTC1326
        .ok()
8d93a5d23addatagen2026-10-26 05:28 UTC1327
        .mount()
8d93a5d23addatagen2026-10-26 05:28 UTC1328
        .await;
8d93a5d23addatagen2026-10-26 05:28 UTC1329
    server.mock_login().ok().mount().await;
8d93a5d23addatagen2026-10-26 05:28 UTC1330
    server
8d93a5d23addatagen2026-10-26 05:28 UTC1331
        .mock_room_messages()
8d93a5d23addatagen2026-10-26 05:28 UTC1332
        .ok(matrix_sdk::test_utils::mocks::RoomMessagesResponseTemplate::default())
8d93a5d23addatagen2026-10-26 05:28 UTC1333
        .mount()
8d93a5d23addatagen2026-10-26 05:28 UTC1334
        .await;
8d93a5d23addatagen2026-10-26 05:28 UTC1335
    let root = std::env::temp_dir().join(format!(
8d93a5d23addatagen2026-10-26 05:28 UTC1336
        "macltok-membership-room-{}-{}",
8d93a5d23addatagen2026-10-26 05:28 UTC1337
        std::process::id(),
8d93a5d23addatagen2026-10-26 05:28 UTC1338
        NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)
8d93a5d23addatagen2026-10-26 05:28 UTC1339
    ));
8d93a5d23addatagen2026-10-26 05:28 UTC1340
    let sessions = Arc::new(EncryptedSessionStore::file(
8d93a5d23addatagen2026-10-26 05:28 UTC1341
        root.join("records").join("session"),
8d93a5d23addatagen2026-10-26 05:28 UTC1342
        StoreConfig::sqlite(root.join("store"), "membership room test passphrase"),
8d93a5d23addatagen2026-10-26 05:28 UTC1343
    ));
8d93a5d23addatagen2026-10-26 05:28 UTC1344
    let room_id = room_id!("!memberships-resume:matrix.org");
8d93a5d23addatagen2026-10-26 05:28 UTC1345
    let own_user = user_id!("@cheeky_monkey:matrix.org");
8d93a5d23addatagen2026-10-26 05:28 UTC1346
    let member = |event_id: &str, user: &str, ts: u64| {
8d93a5d23addatagen2026-10-26 05:28 UTC1347
        json!({
8d93a5d23addatagen2026-10-26 05:28 UTC1348
            "type": "m.room.member",
8d93a5d23addatagen2026-10-26 05:28 UTC1349
            "event_id": event_id,
8d93a5d23addatagen2026-10-26 05:28 UTC1350
            "sender": user,
8d93a5d23addatagen2026-10-26 05:28 UTC1351
            "state_key": user,
8d93a5d23addatagen2026-10-26 05:28 UTC1352
            "origin_server_ts": ts,
8d93a5d23addatagen2026-10-26 05:28 UTC1353
            "content": { "membership": "join" },
8d93a5d23addatagen2026-10-26 05:28 UTC1354
        })
8d93a5d23addatagen2026-10-26 05:28 UTC1355
    };
8d93a5d23addatagen2026-10-26 05:28 UTC1356
    let feed = SlidingFeed::new(
8d93a5d23addatagen2026-10-26 05:28 UTC1357
        room_id.to_owned(),
8d93a5d23addatagen2026-10-26 05:28 UTC1358
        vec![
8d93a5d23addatagen2026-10-26 05:28 UTC1359
            json!({
8d93a5d23addatagen2026-10-26 05:28 UTC1360
                "type": "m.room.create",
8d93a5d23addatagen2026-10-26 05:28 UTC1361
                "event_id": "$created",
8d93a5d23addatagen2026-10-26 05:28 UTC1362
                "sender": own_user,
8d93a5d23addatagen2026-10-26 05:28 UTC1363
                "state_key": "",
8d93a5d23addatagen2026-10-26 05:28 UTC1364
                "origin_server_ts": 1,
8d93a5d23addatagen2026-10-26 05:28 UTC1365
                "content": { "room_version": "11" },
8d93a5d23addatagen2026-10-26 05:28 UTC1366
            }),
8d93a5d23addatagen2026-10-26 05:28 UTC1367
            member("$own-joined", own_user.as_str(), 2),
8d93a5d23addatagen2026-10-26 05:28 UTC1368
            member("$alice-joined", "@alice:matrix.org", 3),
8d93a5d23addatagen2026-10-26 05:28 UTC1369
        ],
8d93a5d23addatagen2026-10-26 05:28 UTC1370
    );
8d93a5d23addatagen2026-10-26 05:28 UTC1371
    feed.set_required_state(room_id, vec![member("$own-joined", own_user.as_str(), 2)]);
8d93a5d23addatagen2026-10-26 05:28 UTC1372
    feed.mount(&server).await;
8d93a5d23addatagen2026-10-26 05:28 UTC1373
8d93a5d23addatagen2026-10-26 05:28 UTC1374
    let config = EngineConfig::default()
8d93a5d23addatagen2026-10-26 05:28 UTC1375
        .with_initial_history_limit(0)
8d93a5d23addatagen2026-10-26 05:28 UTC1376
        .with_search_backfill_pages_per_room(0);
8d93a5d23addatagen2026-10-26 05:28 UTC1377
    let session = Session::login_password(
8d93a5d23addatagen2026-10-26 05:28 UTC1378
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
8d93a5d23addatagen2026-10-26 05:28 UTC1379
        sessions.clone(),
8d93a5d23addatagen2026-10-26 05:28 UTC1380
    )
8d93a5d23addatagen2026-10-26 05:28 UTC1381
    .await
8d93a5d23addatagen2026-10-26 05:28 UTC1382
    .unwrap();
8d93a5d23addatagen2026-10-26 05:28 UTC1383
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
8d93a5d23addatagen2026-10-26 05:28 UTC1384
    let running = tokio::spawn(engine.run());
8d93a5d23addatagen2026-10-26 05:28 UTC1385
    tokio::time::timeout(Duration::from_secs(10), async {
8d93a5d23addatagen2026-10-26 05:28 UTC1386
        while !handle
8d93a5d23addatagen2026-10-26 05:28 UTC1387
            .rooms()
8d93a5d23addatagen2026-10-26 05:28 UTC1388
            .await
8d93a5d23addatagen2026-10-26 05:28 UTC1389
            .iter()
8d93a5d23addatagen2026-10-26 05:28 UTC1390
            .any(|room| room.room_id == room_id)
8d93a5d23addatagen2026-10-26 05:28 UTC1391
        {
8d93a5d23addatagen2026-10-26 05:28 UTC1392
            tokio::time::sleep(Duration::from_millis(20)).await;
8d93a5d23addatagen2026-10-26 05:28 UTC1393
        }
8d93a5d23addatagen2026-10-26 05:28 UTC1394
    })
8d93a5d23addatagen2026-10-26 05:28 UTC1395
    .await
8d93a5d23addatagen2026-10-26 05:28 UTC1396
    .expect("the room never synced");
8d93a5d23addatagen2026-10-26 05:28 UTC1397
    tokio::time::timeout(
8d93a5d23addatagen2026-10-26 05:28 UTC1398
        Duration::from_secs(10),
8d93a5d23addatagen2026-10-26 05:28 UTC1399
        handle.open_room(room_id.to_owned()),
8d93a5d23addatagen2026-10-26 05:28 UTC1400
    )
8d93a5d23addatagen2026-10-26 05:28 UTC1401
    .await
8d93a5d23addatagen2026-10-26 05:28 UTC1402
    .expect("opening the room never answered before the restart")
8d93a5d23addatagen2026-10-26 05:28 UTC1403
    .unwrap();
8d93a5d23addatagen2026-10-26 05:28 UTC1404
    shut_down(&handle, &mut events, running).await;
8d93a5d23addatagen2026-10-26 05:28 UTC1405
    drop(handle);
8d93a5d23addatagen2026-10-26 05:28 UTC1406
8d93a5d23addatagen2026-10-26 05:28 UTC1407
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
8d93a5d23addatagen2026-10-26 05:28 UTC1408
        panic!("the saved session did not resume");
8d93a5d23addatagen2026-10-26 05:28 UTC1409
    };
8d93a5d23addatagen2026-10-26 05:28 UTC1410
    let (engine, handle, mut events) = session.build(config).await.unwrap();
8d93a5d23addatagen2026-10-26 05:28 UTC1411
    let running = tokio::spawn(engine.run());
8d93a5d23addatagen2026-10-26 05:28 UTC1412
    receive_until(&mut events, |event| {
8d93a5d23addatagen2026-10-26 05:28 UTC1413
        matches!(event, Event::Authenticated { .. })
8d93a5d23addatagen2026-10-26 05:28 UTC1414
    })
8d93a5d23addatagen2026-10-26 05:28 UTC1415
    .await;
8d93a5d23addatagen2026-10-26 05:28 UTC1416
    tokio::time::timeout(
8d93a5d23addatagen2026-10-26 05:28 UTC1417
        Duration::from_secs(10),
8d93a5d23addatagen2026-10-26 05:28 UTC1418
        handle.open_room(room_id.to_owned()),
8d93a5d23addatagen2026-10-26 05:28 UTC1419
    )
8d93a5d23addatagen2026-10-26 05:28 UTC1420
    .await
8d93a5d23addatagen2026-10-26 05:28 UTC1421
    .expect("opening the resumed room never answered")
8d93a5d23addatagen2026-10-26 05:28 UTC1422
    .unwrap();
8d93a5d23addatagen2026-10-26 05:28 UTC1423
    shut_down(&handle, &mut events, running).await;
8d93a5d23addatagen2026-10-26 05:28 UTC1424
    drop(handle);
8d93a5d23addatagen2026-10-26 05:28 UTC1425
8d93a5d23addatagen2026-10-26 05:28 UTC1426
    sessions.discard().await.unwrap();
8d93a5d23addatagen2026-10-26 05:28 UTC1427
    std::fs::remove_dir_all(root).unwrap();
8d93a5d23addatagen2026-10-26 05:28 UTC1428
}