atagen

macltok

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

Blame: src/mentions/service_tests.rs

cbb715e11963atagen2026-10-24 23:24 UTC1
use std::{
cbb715e11963atagen2026-10-24 23:24 UTC2
    sync::{
cbb715e11963atagen2026-10-24 23:24 UTC3
        Arc,
cbb715e11963atagen2026-10-24 23:24 UTC4
        atomic::{AtomicUsize, Ordering},
cbb715e11963atagen2026-10-24 23:24 UTC5
    },
cbb715e11963atagen2026-10-24 23:24 UTC6
    time::Duration,
cbb715e11963atagen2026-10-24 23:24 UTC7
};
cbb715e11963atagen2026-10-24 23:24 UTC8
cbb715e11963atagen2026-10-24 23:24 UTC9
use matrix_sdk::ruma::events::room::MediaSource;
cbb715e11963atagen2026-10-24 23:24 UTC10
use matrix_sdk::{
cbb715e11963atagen2026-10-24 23:24 UTC11
    ruma::{event_id, events::Mentions, push::Ruleset, room_id, user_id},
cbb715e11963atagen2026-10-24 23:24 UTC12
    test_utils::mocks::MatrixMockServer,
cbb715e11963atagen2026-10-24 23:24 UTC13
};
cbb715e11963atagen2026-10-24 23:24 UTC14
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
cbb715e11963atagen2026-10-24 23:24 UTC15
use rusqlite::Connection;
cbb715e11963atagen2026-10-24 23:24 UTC16
use serde_json::json;
cbb715e11963atagen2026-10-24 23:24 UTC17
use wiremock::{
cbb715e11963atagen2026-10-24 23:24 UTC18
    Mock, ResponseTemplate,
cbb715e11963atagen2026-10-24 23:24 UTC19
    matchers::{method, path, query_param, query_param_is_missing},
cbb715e11963atagen2026-10-24 23:24 UTC20
};
cbb715e11963atagen2026-10-24 23:24 UTC21
cbb715e11963atagen2026-10-24 23:24 UTC22
use super::super::store::{Account, MentionsStore};
cbb715e11963atagen2026-10-24 23:24 UTC23
use super::*;
cbb715e11963atagen2026-10-24 23:24 UTC24
use crate::{
cbb715e11963atagen2026-10-24 23:24 UTC25
    MediaId, MediaKind, StoreConfig, TimelineContent, TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC26
    normalise::{MediaRegistration, MediaSourceInfo},
cbb715e11963atagen2026-10-24 23:24 UTC27
};
cbb715e11963atagen2026-10-24 23:24 UTC28
cbb715e11963atagen2026-10-24 23:24 UTC29
static STORES: AtomicUsize = AtomicUsize::new(0);
cbb715e11963atagen2026-10-24 23:24 UTC30
const MAX_BYTES: usize = 64 * 1024 * 1024;
cbb715e11963atagen2026-10-24 23:24 UTC31
cbb715e11963atagen2026-10-24 23:24 UTC32
fn config() -> StoreConfig {
cbb715e11963atagen2026-10-24 23:24 UTC33
    let number = STORES.fetch_add(1, Ordering::Relaxed);
cbb715e11963atagen2026-10-24 23:24 UTC34
    StoreConfig::sqlite(
cbb715e11963atagen2026-10-24 23:24 UTC35
        std::env::temp_dir().join(format!(
cbb715e11963atagen2026-10-24 23:24 UTC36
            "macltok-mentions-service-{}-{number}",
cbb715e11963atagen2026-10-24 23:24 UTC37
            std::process::id()
cbb715e11963atagen2026-10-24 23:24 UTC38
        )),
cbb715e11963atagen2026-10-24 23:24 UTC39
        "service commit test passphrase",
cbb715e11963atagen2026-10-24 23:24 UTC40
    )
cbb715e11963atagen2026-10-24 23:24 UTC41
}
cbb715e11963atagen2026-10-24 23:24 UTC42
cbb715e11963atagen2026-10-24 23:24 UTC43
fn account(client: &Client) -> Account {
cbb715e11963atagen2026-10-24 23:24 UTC44
    Account {
cbb715e11963atagen2026-10-24 23:24 UTC45
        homeserver: client.homeserver().to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC46
        user_id: client.user_id().unwrap().to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC47
    }
cbb715e11963atagen2026-10-24 23:24 UTC48
}
cbb715e11963atagen2026-10-24 23:24 UTC49
cbb715e11963atagen2026-10-24 23:24 UTC50
async fn joined_client(
cbb715e11963atagen2026-10-24 23:24 UTC51
    server: &MatrixMockServer,
cbb715e11963atagen2026-10-24 23:24 UTC52
    room_id: &ruma::RoomId,
cbb715e11963atagen2026-10-24 23:24 UTC53
) -> (Client, ruma::OwnedUserId) {
cbb715e11963atagen2026-10-24 23:24 UTC54
    let client = server.client_builder().build().await;
cbb715e11963atagen2026-10-24 23:24 UTC55
    let own_user = client.user_id().unwrap().to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC56
    let room_id = room_id.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC57
    server
cbb715e11963atagen2026-10-24 23:24 UTC58
        .mock_sync()
cbb715e11963atagen2026-10-24 23:24 UTC59
        .ok_and_run(&client, {
cbb715e11963atagen2026-10-24 23:24 UTC60
            let own_user = own_user.clone();
cbb715e11963atagen2026-10-24 23:24 UTC61
            move |builder| {
cbb715e11963atagen2026-10-24 23:24 UTC62
                let factory = EventFactory::new().room(&room_id);
cbb715e11963atagen2026-10-24 23:24 UTC63
                builder.add_global_account_data(
cbb715e11963atagen2026-10-24 23:24 UTC64
                    factory.push_rules(Ruleset::server_default(&own_user)),
cbb715e11963atagen2026-10-24 23:24 UTC65
                );
cbb715e11963atagen2026-10-24 23:24 UTC66
                builder.add_joined_room(
cbb715e11963atagen2026-10-24 23:24 UTC67
                    JoinedRoomBuilder::new(&room_id).add_state_event(factory.member(&own_user)),
cbb715e11963atagen2026-10-24 23:24 UTC68
                );
cbb715e11963atagen2026-10-24 23:24 UTC69
            }
cbb715e11963atagen2026-10-24 23:24 UTC70
        })
cbb715e11963atagen2026-10-24 23:24 UTC71
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC72
    (client, own_user)
cbb715e11963atagen2026-10-24 23:24 UTC73
}
cbb715e11963atagen2026-10-24 23:24 UTC74
cbb715e11963atagen2026-10-24 23:24 UTC75
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC76
async fn failed_batch_keeps_inbox_and_checkpoint_at_the_same_commit_boundary() {
cbb715e11963atagen2026-10-24 23:24 UTC77
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC78
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC79
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC80
    let event = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC81
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC82
        .text_msg("commit boundary")
cbb715e11963atagen2026-10-24 23:24 UTC83
        .mentions(Mentions::with_user_ids([own_user.clone()]))
cbb715e11963atagen2026-10-24 23:24 UTC84
        .event_id(event_id!("$commit-boundary:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC85
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC86
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC87
    let raw_room = serde_json::from_value(json!({
cbb715e11963atagen2026-10-24 23:24 UTC88
        "initial": true,
cbb715e11963atagen2026-10-24 23:24 UTC89
        "timeline": [serde_json::to_value(event).unwrap()],
cbb715e11963atagen2026-10-24 23:24 UTC90
    }))
cbb715e11963atagen2026-10-24 23:24 UTC91
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC92
    let batch = ResponseCommitBatch {
cbb715e11963atagen2026-10-24 23:24 UTC93
        connection_id: "mentions-connection".to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC94
        previous_pos: None,
cbb715e11963atagen2026-10-24 23:24 UTC95
        next_pos: "mentions-position-1".to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC96
        response: Default::default(),
cbb715e11963atagen2026-10-24 23:24 UTC97
        raw_rooms: [(room_id.to_owned(), raw_room)].into(),
cbb715e11963atagen2026-10-24 23:24 UTC98
    };
cbb715e11963atagen2026-10-24 23:24 UTC99
    let config = config();
cbb715e11963atagen2026-10-24 23:24 UTC100
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC101
        .await
cbb715e11963atagen2026-10-24 23:24 UTC102
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC103
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC104
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC105
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC106
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC107
    ));
cbb715e11963atagen2026-10-24 23:24 UTC108
    let (_handle, mut service, updates) =
cbb715e11963atagen2026-10-24 23:24 UTC109
        Service::new(client, media, Some(store), EngineConfig::default())
cbb715e11963atagen2026-10-24 23:24 UTC110
            .await
cbb715e11963atagen2026-10-24 23:24 UTC111
            .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC112
cbb715e11963atagen2026-10-24 23:24 UTC113
    let connection = Connection::open(config.mentions_location()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC114
    connection
cbb715e11963atagen2026-10-24 23:24 UTC115
        .execute_batch(
cbb715e11963atagen2026-10-24 23:24 UTC116
            "CREATE TRIGGER fail_mentions_inbox_insert BEFORE INSERT ON records WHEN NEW.key NOT IN ('!cipher', '!head') BEGIN SELECT RAISE(ABORT, 'injected inbox write failure'); END;
cbb715e11963atagen2026-10-24 23:24 UTC117
             CREATE TRIGGER fail_mentions_inbox_update BEFORE UPDATE ON records WHEN OLD.key NOT IN ('!cipher', '!head') BEGIN SELECT RAISE(ABORT, 'injected inbox write failure'); END;",
cbb715e11963atagen2026-10-24 23:24 UTC118
        )
cbb715e11963atagen2026-10-24 23:24 UTC119
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC120
cbb715e11963atagen2026-10-24 23:24 UTC121
    assert!(service.batch(batch.clone()).await.is_err());
cbb715e11963atagen2026-10-24 23:24 UTC122
    assert_eq!(service.data.revision, 0);
cbb715e11963atagen2026-10-24 23:24 UTC123
    assert!(service.data.entries.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC124
    assert!(service.data.checkpoints.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC125
    assert_eq!(updates.borrow().revision, 0);
cbb715e11963atagen2026-10-24 23:24 UTC126
    assert!(updates.borrow().entries.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC127
cbb715e11963atagen2026-10-24 23:24 UTC128
    let reopened = MentionsStore::open(&config, account(&service.client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC129
        .await
cbb715e11963atagen2026-10-24 23:24 UTC130
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC131
    assert_eq!(reopened.revision(), 0);
cbb715e11963atagen2026-10-24 23:24 UTC132
    assert!(reopened.read::<Data>("inbox").unwrap().is_none());
cbb715e11963atagen2026-10-24 23:24 UTC133
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC134
cbb715e11963atagen2026-10-24 23:24 UTC135
    connection
cbb715e11963atagen2026-10-24 23:24 UTC136
        .execute_batch(
cbb715e11963atagen2026-10-24 23:24 UTC137
            "DROP TRIGGER fail_mentions_inbox_insert;
cbb715e11963atagen2026-10-24 23:24 UTC138
             DROP TRIGGER fail_mentions_inbox_update;",
cbb715e11963atagen2026-10-24 23:24 UTC139
        )
cbb715e11963atagen2026-10-24 23:24 UTC140
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC141
    drop(connection);
cbb715e11963atagen2026-10-24 23:24 UTC142
cbb715e11963atagen2026-10-24 23:24 UTC143
    service.batch(batch.clone()).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC144
    assert_eq!(service.data.revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC145
    assert_eq!(service.data.entries.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC146
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC147
        service.data.checkpoints.get("mentions-connection"),
cbb715e11963atagen2026-10-24 23:24 UTC148
        Some(&batch.next_pos)
cbb715e11963atagen2026-10-24 23:24 UTC149
    );
cbb715e11963atagen2026-10-24 23:24 UTC150
    assert_eq!(updates.borrow().revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC151
    assert_eq!(updates.borrow().entries.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC152
cbb715e11963atagen2026-10-24 23:24 UTC153
    let reopened = MentionsStore::open(&config, account(&service.client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC154
        .await
cbb715e11963atagen2026-10-24 23:24 UTC155
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC156
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC157
    assert_eq!(reopened.revision(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC158
    assert_eq!(persisted.revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC159
    assert_eq!(persisted.entries.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC160
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC161
        persisted.checkpoints.get("mentions-connection"),
cbb715e11963atagen2026-10-24 23:24 UTC162
        Some(&batch.next_pos)
cbb715e11963atagen2026-10-24 23:24 UTC163
    );
cbb715e11963atagen2026-10-24 23:24 UTC164
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC165
cbb715e11963atagen2026-10-24 23:24 UTC166
    service.batch(batch).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC167
    assert_eq!(service.data.revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC168
    assert_eq!(service.store.as_ref().unwrap().revision(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC169
cbb715e11963atagen2026-10-24 23:24 UTC170
    drop(service);
cbb715e11963atagen2026-10-24 23:24 UTC171
    config.delete().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC172
}
cbb715e11963atagen2026-10-24 23:24 UTC173
80381e06a573atagen2026-10-25 03:02 UTC174
#[test]
80381e06a573atagen2026-10-25 03:02 UTC175
fn repair_fields_default_when_loading_an_older_inbox() {
80381e06a573atagen2026-10-25 03:02 UTC176
    let mut inbox = serde_json::to_value(Data::default()).unwrap();
80381e06a573atagen2026-10-25 03:02 UTC177
    inbox.as_object_mut().unwrap().remove("repair_policy");
80381e06a573atagen2026-10-25 03:02 UTC178
    inbox.as_object_mut().unwrap().remove("repair_coverage");
80381e06a573atagen2026-10-25 03:02 UTC179
80381e06a573atagen2026-10-25 03:02 UTC180
    let restored: Data = serde_json::from_value(inbox).unwrap();
80381e06a573atagen2026-10-25 03:02 UTC181
    assert_eq!(restored.repair_policy, HistoryRepairPolicy::default());
80381e06a573atagen2026-10-25 03:02 UTC182
    assert!(restored.repair_coverage.is_empty());
80381e06a573atagen2026-10-25 03:02 UTC183
}
80381e06a573atagen2026-10-25 03:02 UTC184
80381e06a573atagen2026-10-25 03:02 UTC185
#[tokio::test]
80381e06a573atagen2026-10-25 03:02 UTC186
async fn repair_policy_and_coverage_roundtrip_through_the_inbox() {
80381e06a573atagen2026-10-25 03:02 UTC187
    let server = MatrixMockServer::new().await;
80381e06a573atagen2026-10-25 03:02 UTC188
    let room_id = room_id!("!repair-coverage:localhost");
80381e06a573atagen2026-10-25 03:02 UTC189
    let (client, _) = joined_client(&server, room_id).await;
80381e06a573atagen2026-10-25 03:02 UTC190
    let config = config();
80381e06a573atagen2026-10-25 03:02 UTC191
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
80381e06a573atagen2026-10-25 03:02 UTC192
        .await
80381e06a573atagen2026-10-25 03:02 UTC193
        .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC194
    let media = Arc::new(MediaService::new(
80381e06a573atagen2026-10-25 03:02 UTC195
        client.clone(),
80381e06a573atagen2026-10-25 03:02 UTC196
        EngineConfig::default().max_media_bytes,
80381e06a573atagen2026-10-25 03:02 UTC197
        EngineConfig::default().max_media_sources,
80381e06a573atagen2026-10-25 03:02 UTC198
    ));
80381e06a573atagen2026-10-25 03:02 UTC199
    let (handle, mut service, _) =
80381e06a573atagen2026-10-25 03:02 UTC200
        Service::new(client.clone(), media, Some(store), EngineConfig::default())
80381e06a573atagen2026-10-25 03:02 UTC201
            .await
80381e06a573atagen2026-10-25 03:02 UTC202
            .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC203
    service.recovery_budget = 0;
80381e06a573atagen2026-10-25 03:02 UTC204
    service.latest_scan = false;
80381e06a573atagen2026-10-25 03:02 UTC205
    let service_task = tokio::spawn(service.run());
80381e06a573atagen2026-10-25 03:02 UTC206
80381e06a573atagen2026-10-25 03:02 UTC207
    let default_policy = HistoryRepairPolicy::default();
80381e06a573atagen2026-10-25 03:02 UTC208
    assert_eq!(handle.repair_policy().await.unwrap(), default_policy);
80381e06a573atagen2026-10-25 03:02 UTC209
    let stored_policy = HistoryRepairPolicy::Bridge;
80381e06a573atagen2026-10-25 03:02 UTC210
    handle
80381e06a573atagen2026-10-25 03:02 UTC211
        .set_repair_policy(stored_policy.clone())
80381e06a573atagen2026-10-25 03:02 UTC212
        .await
80381e06a573atagen2026-10-25 03:02 UTC213
        .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC214
    handle.repair_resolved(room_id.to_owned()).await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC215
    let coverage = handle.repair_coverage(room_id.to_owned()).await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC216
    assert!(coverage.last_confirmed_ms.is_some());
80381e06a573atagen2026-10-25 03:02 UTC217
    assert!(!coverage.reliable);
80381e06a573atagen2026-10-25 03:02 UTC218
    assert_eq!(coverage.duration_ms(), None);
80381e06a573atagen2026-10-25 03:02 UTC219
80381e06a573atagen2026-10-25 03:02 UTC220
    handle.stop().await;
80381e06a573atagen2026-10-25 03:02 UTC221
    service_task.await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC222
    drop(handle);
80381e06a573atagen2026-10-25 03:02 UTC223
80381e06a573atagen2026-10-25 03:02 UTC224
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
80381e06a573atagen2026-10-25 03:02 UTC225
        .await
80381e06a573atagen2026-10-25 03:02 UTC226
        .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC227
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
80381e06a573atagen2026-10-25 03:02 UTC228
    assert_eq!(persisted.repair_policy, stored_policy);
80381e06a573atagen2026-10-25 03:02 UTC229
    let coverage = persisted.repair_coverage.get(room_id).unwrap();
80381e06a573atagen2026-10-25 03:02 UTC230
    assert!(coverage.last_confirmed_ms.is_some());
80381e06a573atagen2026-10-25 03:02 UTC231
    assert!(coverage.boundary_ms.is_none());
80381e06a573atagen2026-10-25 03:02 UTC232
    assert!(!coverage.unresolved);
80381e06a573atagen2026-10-25 03:02 UTC233
    assert!(!coverage.reliable);
80381e06a573atagen2026-10-25 03:02 UTC234
    drop(reopened);
80381e06a573atagen2026-10-25 03:02 UTC235
    config.delete().await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC236
}
80381e06a573atagen2026-10-25 03:02 UTC237
80381e06a573atagen2026-10-25 03:02 UTC238
#[tokio::test]
80381e06a573atagen2026-10-25 03:02 UTC239
async fn repair_limited_sync_freezes_the_last_room_checkpoint() {
80381e06a573atagen2026-10-25 03:02 UTC240
    let server = MatrixMockServer::new().await;
80381e06a573atagen2026-10-25 03:02 UTC241
    let room_id = room_id!("!repair-boundary:localhost");
80381e06a573atagen2026-10-25 03:02 UTC242
    let (client, _) = joined_client(&server, room_id).await;
80381e06a573atagen2026-10-25 03:02 UTC243
    let media = Arc::new(MediaService::new(
80381e06a573atagen2026-10-25 03:02 UTC244
        client.clone(),
80381e06a573atagen2026-10-25 03:02 UTC245
        EngineConfig::default().max_media_bytes,
80381e06a573atagen2026-10-25 03:02 UTC246
        EngineConfig::default().max_media_sources,
80381e06a573atagen2026-10-25 03:02 UTC247
    ));
80381e06a573atagen2026-10-25 03:02 UTC248
    let (_handle, mut service, _) = Service::new(client, media, None, EngineConfig::default())
80381e06a573atagen2026-10-25 03:02 UTC249
        .await
80381e06a573atagen2026-10-25 03:02 UTC250
        .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC251
    service.data.checkpoints.insert(
80381e06a573atagen2026-10-25 03:02 UTC252
        "mentions-connection".to_owned(),
80381e06a573atagen2026-10-25 03:02 UTC253
        "position-before".to_owned(),
80381e06a573atagen2026-10-25 03:02 UTC254
    );
80381e06a573atagen2026-10-25 03:02 UTC255
    service.data.observed_rooms.insert(room_id.to_owned());
80381e06a573atagen2026-10-25 03:02 UTC256
    service
80381e06a573atagen2026-10-25 03:02 UTC257
        .data
80381e06a573atagen2026-10-25 03:02 UTC258
        .repair_coverage
80381e06a573atagen2026-10-25 03:02 UTC259
        .entry(room_id.to_owned())
80381e06a573atagen2026-10-25 03:02 UTC260
        .or_default()
80381e06a573atagen2026-10-25 03:02 UTC261
        .last_confirmed_ms = Some(1_000);
80381e06a573atagen2026-10-25 03:02 UTC262
80381e06a573atagen2026-10-25 03:02 UTC263
    let events = [
80381e06a573atagen2026-10-25 03:02 UTC264
        EventFactory::new()
80381e06a573atagen2026-10-25 03:02 UTC265
            .room(room_id)
80381e06a573atagen2026-10-25 03:02 UTC266
            .text_msg("older event")
80381e06a573atagen2026-10-25 03:02 UTC267
            .sender(user_id!("@sender:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC268
            .event_id(event_id!("$repair-older:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC269
            .into_raw_sync(),
80381e06a573atagen2026-10-25 03:02 UTC270
        EventFactory::new()
80381e06a573atagen2026-10-25 03:02 UTC271
            .room(room_id)
80381e06a573atagen2026-10-25 03:02 UTC272
            .text_msg("latest event")
80381e06a573atagen2026-10-25 03:02 UTC273
            .sender(user_id!("@sender:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC274
            .event_id(event_id!("$repair-latest:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC275
            .into_raw_sync(),
80381e06a573atagen2026-10-25 03:02 UTC276
    ];
80381e06a573atagen2026-10-25 03:02 UTC277
    let timeline: Vec<_> = events
80381e06a573atagen2026-10-25 03:02 UTC278
        .iter()
80381e06a573atagen2026-10-25 03:02 UTC279
        .map(|event| serde_json::to_value(event).unwrap())
80381e06a573atagen2026-10-25 03:02 UTC280
        .collect();
80381e06a573atagen2026-10-25 03:02 UTC281
    let raw_room = serde_json::from_value(json!({
80381e06a573atagen2026-10-25 03:02 UTC282
        "limited": true,
80381e06a573atagen2026-10-25 03:02 UTC283
        "prev_batch": "repair-gap-token",
80381e06a573atagen2026-10-25 03:02 UTC284
        "timeline": timeline,
80381e06a573atagen2026-10-25 03:02 UTC285
    }))
80381e06a573atagen2026-10-25 03:02 UTC286
    .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC287
    let batch = ResponseCommitBatch {
80381e06a573atagen2026-10-25 03:02 UTC288
        connection_id: "mentions-connection".to_owned(),
80381e06a573atagen2026-10-25 03:02 UTC289
        previous_pos: Some("position-before".to_owned()),
80381e06a573atagen2026-10-25 03:02 UTC290
        next_pos: "position-after".to_owned(),
80381e06a573atagen2026-10-25 03:02 UTC291
        response: Default::default(),
80381e06a573atagen2026-10-25 03:02 UTC292
        raw_rooms: [(room_id.to_owned(), raw_room)].into(),
80381e06a573atagen2026-10-25 03:02 UTC293
    };
80381e06a573atagen2026-10-25 03:02 UTC294
80381e06a573atagen2026-10-25 03:02 UTC295
    service.batch(batch).await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC296
80381e06a573atagen2026-10-25 03:02 UTC297
    let coverage = service.data.repair_coverage.get(room_id).unwrap();
80381e06a573atagen2026-10-25 03:02 UTC298
    assert_eq!(coverage.last_confirmed_ms, Some(1_000));
80381e06a573atagen2026-10-25 03:02 UTC299
    assert!(
80381e06a573atagen2026-10-25 03:02 UTC300
        coverage
80381e06a573atagen2026-10-25 03:02 UTC301
            .boundary_ms
80381e06a573atagen2026-10-25 03:02 UTC302
            .is_some_and(|boundary| boundary > 1_000)
80381e06a573atagen2026-10-25 03:02 UTC303
    );
80381e06a573atagen2026-10-25 03:02 UTC304
    assert!(coverage.unresolved);
80381e06a573atagen2026-10-25 03:02 UTC305
    assert!(!coverage.reliable);
80381e06a573atagen2026-10-25 03:02 UTC306
    assert_eq!(coverage.duration_ms(), None);
80381e06a573atagen2026-10-25 03:02 UTC307
    assert_eq!(
80381e06a573atagen2026-10-25 03:02 UTC308
        coverage.latest_anchor.as_deref(),
80381e06a573atagen2026-10-25 03:02 UTC309
        Some(event_id!("$repair-latest:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC310
    );
80381e06a573atagen2026-10-25 03:02 UTC311
}
80381e06a573atagen2026-10-25 03:02 UTC312
cbb715e11963atagen2026-10-24 23:24 UTC313
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC314
async fn service_recovers_notification_history_into_durable_unread_inbox() {
cbb715e11963atagen2026-10-24 23:24 UTC315
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC316
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC317
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC318
    let first = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC319
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC320
        .text_msg("history mention one")
cbb715e11963atagen2026-10-24 23:24 UTC321
        .mentions(Mentions::with_user_ids([own_user.clone()]))
cbb715e11963atagen2026-10-24 23:24 UTC322
        .event_id(event_id!("$history-one:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC323
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC324
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC325
    let second = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC326
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC327
        .text_msg("history mention two")
cbb715e11963atagen2026-10-24 23:24 UTC328
        .mentions(Mentions::with_user_ids([own_user]))
cbb715e11963atagen2026-10-24 23:24 UTC329
        .event_id(event_id!("$history-two:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC330
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC331
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC332
    let first = serde_json::from_str::<serde_json::Value>(first.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC333
    let second = serde_json::from_str::<serde_json::Value>(second.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC334
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC335
        .and(path("/_matrix/client/v3/notifications"))
cbb715e11963atagen2026-10-24 23:24 UTC336
        .and(query_param_is_missing("from"))
cbb715e11963atagen2026-10-24 23:24 UTC337
        .and(query_param("limit", "50"))
cbb715e11963atagen2026-10-24 23:24 UTC338
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC339
            "notifications": [{
cbb715e11963atagen2026-10-24 23:24 UTC340
                "actions": ["notify"],
cbb715e11963atagen2026-10-24 23:24 UTC341
                "event": first,
cbb715e11963atagen2026-10-24 23:24 UTC342
                "read": true,
cbb715e11963atagen2026-10-24 23:24 UTC343
                "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC344
                "ts": 1,
cbb715e11963atagen2026-10-24 23:24 UTC345
            }],
cbb715e11963atagen2026-10-24 23:24 UTC346
            "next_token": "history-page-two",
cbb715e11963atagen2026-10-24 23:24 UTC347
        })))
cbb715e11963atagen2026-10-24 23:24 UTC348
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC349
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC350
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC351
        .and(path("/_matrix/client/v3/notifications"))
cbb715e11963atagen2026-10-24 23:24 UTC352
        .and(query_param("from", "history-page-two"))
cbb715e11963atagen2026-10-24 23:24 UTC353
        .and(query_param("limit", "50"))
cbb715e11963atagen2026-10-24 23:24 UTC354
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC355
            "notifications": [{
cbb715e11963atagen2026-10-24 23:24 UTC356
                "actions": ["notify"],
cbb715e11963atagen2026-10-24 23:24 UTC357
                "event": second,
cbb715e11963atagen2026-10-24 23:24 UTC358
                "read": true,
cbb715e11963atagen2026-10-24 23:24 UTC359
                "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC360
                "ts": 2,
cbb715e11963atagen2026-10-24 23:24 UTC361
            }],
cbb715e11963atagen2026-10-24 23:24 UTC362
        })))
cbb715e11963atagen2026-10-24 23:24 UTC363
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC364
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC365
cbb715e11963atagen2026-10-24 23:24 UTC366
    let config = config();
cbb715e11963atagen2026-10-24 23:24 UTC367
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC368
        .await
cbb715e11963atagen2026-10-24 23:24 UTC369
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC370
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC371
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC372
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC373
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC374
    ));
cbb715e11963atagen2026-10-24 23:24 UTC375
    let (handle, service, mut updates) =
cbb715e11963atagen2026-10-24 23:24 UTC376
        Service::new(client.clone(), media, Some(store), EngineConfig::default())
cbb715e11963atagen2026-10-24 23:24 UTC377
            .await
cbb715e11963atagen2026-10-24 23:24 UTC378
            .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC379
    let service_task = tokio::spawn(service.run());
cbb715e11963atagen2026-10-24 23:24 UTC380
cbb715e11963atagen2026-10-24 23:24 UTC381
    let discovered = tokio::time::timeout(Duration::from_secs(10), async {
cbb715e11963atagen2026-10-24 23:24 UTC382
        loop {
cbb715e11963atagen2026-10-24 23:24 UTC383
            let page = updates.borrow_and_update().clone();
cbb715e11963atagen2026-10-24 23:24 UTC384
            if page.entries.len() == 2 && !page.coverage.recovering {
cbb715e11963atagen2026-10-24 23:24 UTC385
                break page;
cbb715e11963atagen2026-10-24 23:24 UTC386
            }
cbb715e11963atagen2026-10-24 23:24 UTC387
            updates.changed().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC388
        }
cbb715e11963atagen2026-10-24 23:24 UTC389
    })
cbb715e11963atagen2026-10-24 23:24 UTC390
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC391
    handle.stop().await;
cbb715e11963atagen2026-10-24 23:24 UTC392
    service_task.await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC393
    let discovered = discovered.expect("notification history should be recovered");
cbb715e11963atagen2026-10-24 23:24 UTC394
    assert_eq!(discovered.entries.len(), 2);
cbb715e11963atagen2026-10-24 23:24 UTC395
    assert!(discovered.entries.iter().all(|entry| !entry.read));
cbb715e11963atagen2026-10-24 23:24 UTC396
cbb715e11963atagen2026-10-24 23:24 UTC397
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC398
        .await
cbb715e11963atagen2026-10-24 23:24 UTC399
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC400
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC401
    assert_eq!(reopened.revision(), persisted.revision);
cbb715e11963atagen2026-10-24 23:24 UTC402
    assert_eq!(persisted.entries.len(), 2);
cbb715e11963atagen2026-10-24 23:24 UTC403
    assert!(persisted.entries.values().all(|entry| !entry.read));
cbb715e11963atagen2026-10-24 23:24 UTC404
    assert!(persisted.latest_cursor.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC405
    assert!(persisted.history_cursor.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC406
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC407
cbb715e11963atagen2026-10-24 23:24 UTC408
    config.delete().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC409
}
cbb715e11963atagen2026-10-24 23:24 UTC410
cbb715e11963atagen2026-10-24 23:24 UTC411
fn message_body(item: &TimelineItem) -> Option<&str> {
cbb715e11963atagen2026-10-24 23:24 UTC412
    match item {
cbb715e11963atagen2026-10-24 23:24 UTC413
        TimelineItem::Event(event) => match &event.content {
cbb715e11963atagen2026-10-24 23:24 UTC414
            TimelineContent::Message(message) => Some(&message.body),
cbb715e11963atagen2026-10-24 23:24 UTC415
            _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC416
        },
cbb715e11963atagen2026-10-24 23:24 UTC417
        _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC418
    }
cbb715e11963atagen2026-10-24 23:24 UTC419
}
cbb715e11963atagen2026-10-24 23:24 UTC420
cbb715e11963atagen2026-10-24 23:24 UTC421
fn media_registration() -> MediaRegistration {
cbb715e11963atagen2026-10-24 23:24 UTC422
    MediaRegistration {
cbb715e11963atagen2026-10-24 23:24 UTC423
        id: MediaId::new("test-scope#image".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC424
        kind: MediaKind::Image,
cbb715e11963atagen2026-10-24 23:24 UTC425
        full: MediaSourceInfo {
cbb715e11963atagen2026-10-24 23:24 UTC426
            source: MediaSource::Plain(ruma::mxc_uri!("mxc://mentions.example/image").to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC427
            mimetype: Some("image/png".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC428
            declared_size: Some(1234),
cbb715e11963atagen2026-10-24 23:24 UTC429
        },
cbb715e11963atagen2026-10-24 23:24 UTC430
        thumbnail: None,
cbb715e11963atagen2026-10-24 23:24 UTC431
    }
cbb715e11963atagen2026-10-24 23:24 UTC432
}
cbb715e11963atagen2026-10-24 23:24 UTC433
cbb715e11963atagen2026-10-24 23:24 UTC434
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC435
async fn live_context_survives_capture_then_edit_and_redaction() {
cbb715e11963atagen2026-10-24 23:24 UTC436
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC437
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC438
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC439
    let mention = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC440
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC441
        .text_msg("direct mention")
cbb715e11963atagen2026-10-24 23:24 UTC442
        .mentions(Mentions::with_user_ids([own_user]))
cbb715e11963atagen2026-10-24 23:24 UTC443
        .event_id(event_id!("$live-mention:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC444
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC445
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC446
    let mut mention: serde_json::Value = serde_json::from_str(mention.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC447
    mention["origin_server_ts"] = json!(10);
cbb715e11963atagen2026-10-24 23:24 UTC448
    let following = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC449
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC450
        .text_msg("following")
cbb715e11963atagen2026-10-24 23:24 UTC451
        .event_id(event_id!("$live-following:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC452
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC453
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC454
    let mut following: serde_json::Value = serde_json::from_str(following.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC455
    following["origin_server_ts"] = json!(11);
cbb715e11963atagen2026-10-24 23:24 UTC456
cbb715e11963atagen2026-10-24 23:24 UTC457
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC458
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC459
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC460
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC461
    ));
cbb715e11963atagen2026-10-24 23:24 UTC462
    let (_handle, mut service, _updates) =
cbb715e11963atagen2026-10-24 23:24 UTC463
        Service::new(client, media, None, EngineConfig::default())
cbb715e11963atagen2026-10-24 23:24 UTC464
            .await
cbb715e11963atagen2026-10-24 23:24 UTC465
            .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC466
    let room_id = room_id.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC467
    let mention_key = MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC468
        room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC469
        event_id: event_id!("$live-mention:localhost").to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC470
    };
cbb715e11963atagen2026-10-24 23:24 UTC471
    let following_id = format!("{room_id}\n$live-following:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC472
    let mut data = Data::default();
cbb715e11963atagen2026-10-24 23:24 UTC473
cbb715e11963atagen2026-10-24 23:24 UTC474
    service.observe(&mut data, &room_id, mention).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC475
    service
cbb715e11963atagen2026-10-24 23:24 UTC476
        .observe(&mut data, &room_id, following)
cbb715e11963atagen2026-10-24 23:24 UTC477
        .await
cbb715e11963atagen2026-10-24 23:24 UTC478
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC479
cbb715e11963atagen2026-10-24 23:24 UTC480
    let mention_identity = id(&mention_key);
cbb715e11963atagen2026-10-24 23:24 UTC481
    let entry = data.entries.get(&mention_identity).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC482
    let revision = entry.revision;
cbb715e11963atagen2026-10-24 23:24 UTC483
    let target = entry.event.clone();
cbb715e11963atagen2026-10-24 23:24 UTC484
    service.captured(
cbb715e11963atagen2026-10-24 23:24 UTC485
        &mut data,
cbb715e11963atagen2026-10-24 23:24 UTC486
        mention_identity.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC487
        revision,
cbb715e11963atagen2026-10-24 23:24 UTC488
        Ok((
cbb715e11963atagen2026-10-24 23:24 UTC489
            crate::EventContext {
cbb715e11963atagen2026-10-24 23:24 UTC490
                events_before: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC491
                event: Some(target),
cbb715e11963atagen2026-10-24 23:24 UTC492
                events_after: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC493
                start_token: None,
cbb715e11963atagen2026-10-24 23:24 UTC494
                end_token: None,
cbb715e11963atagen2026-10-24 23:24 UTC495
            },
cbb715e11963atagen2026-10-24 23:24 UTC496
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC497
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC498
        )),
cbb715e11963atagen2026-10-24 23:24 UTC499
    );
cbb715e11963atagen2026-10-24 23:24 UTC500
cbb715e11963atagen2026-10-24 23:24 UTC501
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC502
        data.entries[&mention_identity].capture,
cbb715e11963atagen2026-10-24 23:24 UTC503
        MentionCaptureState::Partial
cbb715e11963atagen2026-10-24 23:24 UTC504
    );
cbb715e11963atagen2026-10-24 23:24 UTC505
    assert!(data.events.contains_key(&following_id));
cbb715e11963atagen2026-10-24 23:24 UTC506
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC507
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC508
    assert_eq!(context.events_after.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC509
    assert_eq!(message_body(&context.events_after[0]), Some("following"));
cbb715e11963atagen2026-10-24 23:24 UTC510
cbb715e11963atagen2026-10-24 23:24 UTC511
    let edit = json!({
cbb715e11963atagen2026-10-24 23:24 UTC512
        "content": {
cbb715e11963atagen2026-10-24 23:24 UTC513
            "body": "* edited following",
cbb715e11963atagen2026-10-24 23:24 UTC514
            "m.new_content": {"body": "edited following", "msgtype": "m.text"},
cbb715e11963atagen2026-10-24 23:24 UTC515
            "m.relates_to": {"event_id": "$live-following:localhost", "rel_type": "m.replace"},
cbb715e11963atagen2026-10-24 23:24 UTC516
            "msgtype": "m.text"
cbb715e11963atagen2026-10-24 23:24 UTC517
        },
cbb715e11963atagen2026-10-24 23:24 UTC518
        "event_id": "$live-following-edit:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC519
        "origin_server_ts": 12,
cbb715e11963atagen2026-10-24 23:24 UTC520
        "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC521
        "sender": "@bob:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC522
        "type": "m.room.message"
cbb715e11963atagen2026-10-24 23:24 UTC523
    });
cbb715e11963atagen2026-10-24 23:24 UTC524
    service.observe(&mut data, &room_id, edit).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC525
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC526
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC527
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC528
        message_body(&context.events_after[0]),
cbb715e11963atagen2026-10-24 23:24 UTC529
        Some("edited following")
cbb715e11963atagen2026-10-24 23:24 UTC530
    );
cbb715e11963atagen2026-10-24 23:24 UTC531
cbb715e11963atagen2026-10-24 23:24 UTC532
    let saved_following = data.events.get_mut(&following_id).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC533
    assert!(saved_following.raw.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC534
    saved_following.media.push(media_registration());
cbb715e11963atagen2026-10-24 23:24 UTC535
    assert_eq!(saved_following.media.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC536
    let redaction = json!({
cbb715e11963atagen2026-10-24 23:24 UTC537
        "content": {},
cbb715e11963atagen2026-10-24 23:24 UTC538
        "event_id": "$live-following-redaction:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC539
        "origin_server_ts": 13,
cbb715e11963atagen2026-10-24 23:24 UTC540
        "redacts": "$live-following:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC541
        "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC542
        "sender": "@bob:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC543
        "type": "m.room.redaction"
cbb715e11963atagen2026-10-24 23:24 UTC544
    });
cbb715e11963atagen2026-10-24 23:24 UTC545
    service
cbb715e11963atagen2026-10-24 23:24 UTC546
        .observe(&mut data, &room_id, redaction)
cbb715e11963atagen2026-10-24 23:24 UTC547
        .await
cbb715e11963atagen2026-10-24 23:24 UTC548
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC549
cbb715e11963atagen2026-10-24 23:24 UTC550
    let saved_following = data.events.get(&following_id).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC551
    assert!(matches!(
cbb715e11963atagen2026-10-24 23:24 UTC552
        &saved_following.item,
cbb715e11963atagen2026-10-24 23:24 UTC553
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
cbb715e11963atagen2026-10-24 23:24 UTC554
    ));
cbb715e11963atagen2026-10-24 23:24 UTC555
    assert!(saved_following.raw.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC556
    assert!(saved_following.media.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC557
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC558
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC559
    assert!(matches!(
cbb715e11963atagen2026-10-24 23:24 UTC560
        &context.events_after[0],
cbb715e11963atagen2026-10-24 23:24 UTC561
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
cbb715e11963atagen2026-10-24 23:24 UTC562
    ));
cbb715e11963atagen2026-10-24 23:24 UTC563
    data.windows.remove(&mention_identity);
cbb715e11963atagen2026-10-24 23:24 UTC564
    service.data = data;
cbb715e11963atagen2026-10-24 23:24 UTC565
    let CommandOutput::MentionContext(context) = service
cbb715e11963atagen2026-10-24 23:24 UTC566
        .command(MentionsCommand::RetryContext(mention_key))
cbb715e11963atagen2026-10-24 23:24 UTC567
        .await
cbb715e11963atagen2026-10-24 23:24 UTC568
        .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC569
    else {
cbb715e11963atagen2026-10-24 23:24 UTC570
        panic!("retry should return saved context");
cbb715e11963atagen2026-10-24 23:24 UTC571
    };
cbb715e11963atagen2026-10-24 23:24 UTC572
    assert_eq!(context.state, MentionCaptureState::Pending);
cbb715e11963atagen2026-10-24 23:24 UTC573
    assert!(context.context.unwrap().event.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC574
}
cbb715e11963atagen2026-10-24 23:24 UTC575
cbb715e11963atagen2026-10-24 23:24 UTC576
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC577
async fn explicit_recovery_rechecks_server_support() {
cbb715e11963atagen2026-10-24 23:24 UTC578
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC579
    let (client, _) = joined_client(&server, room_id!("!mentions:localhost")).await;
cbb715e11963atagen2026-10-24 23:24 UTC580
    let config = EngineConfig::default();
cbb715e11963atagen2026-10-24 23:24 UTC581
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC582
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC583
        config.max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC584
        config.max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC585
    ));
cbb715e11963atagen2026-10-24 23:24 UTC586
    let (_, mut service, _) = Service::new(client, media, None, config).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC587
    service.data.coverage.historical_supported = Some(false);
cbb715e11963atagen2026-10-24 23:24 UTC588
    service.recovery_budget = 0;
cbb715e11963atagen2026-10-24 23:24 UTC589
    service.command(MentionsCommand::Recover).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC590
    assert_eq!(service.data.coverage.historical_supported, None);
cbb715e11963atagen2026-10-24 23:24 UTC591
    assert!(service.data.coverage.recovering);
cbb715e11963atagen2026-10-24 23:24 UTC592
    assert_eq!(service.recovery_budget, 20);
cbb715e11963atagen2026-10-24 23:24 UTC593
}