atagen

macltok

public
Star0
Watch0
7cdaa86b9489a6130b716572fa5d91650681196d
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
7cdaa86b9489atagen2026-10-25 08:43 UTC75
fn mention_batch(
7cdaa86b9489atagen2026-10-25 08:43 UTC76
    room_id: &ruma::RoomId,
7cdaa86b9489atagen2026-10-25 08:43 UTC77
    own_user: &ruma::UserId,
7cdaa86b9489atagen2026-10-25 08:43 UTC78
    event: &str,
7cdaa86b9489atagen2026-10-25 08:43 UTC79
    previous_pos: Option<&str>,
7cdaa86b9489atagen2026-10-25 08:43 UTC80
    next_pos: &str,
7cdaa86b9489atagen2026-10-25 08:43 UTC81
) -> ResponseCommitBatch {
cbb715e11963atagen2026-10-24 23:24 UTC82
    let event = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC83
        .room(room_id)
7cdaa86b9489atagen2026-10-25 08:43 UTC84
        .text_msg(event)
7cdaa86b9489atagen2026-10-25 08:43 UTC85
        .mentions(Mentions::with_user_ids([own_user.to_owned()]))
7cdaa86b9489atagen2026-10-25 08:43 UTC86
        .event_id(&ruma::EventId::parse(format!("${event}:localhost")).unwrap())
cbb715e11963atagen2026-10-24 23:24 UTC87
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC88
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC89
    let raw_room = serde_json::from_value(json!({
cbb715e11963atagen2026-10-24 23:24 UTC90
        "timeline": [serde_json::to_value(event).unwrap()],
cbb715e11963atagen2026-10-24 23:24 UTC91
    }))
cbb715e11963atagen2026-10-24 23:24 UTC92
    .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC93
    ResponseCommitBatch {
cbb715e11963atagen2026-10-24 23:24 UTC94
        connection_id: "mentions-connection".to_owned(),
7cdaa86b9489atagen2026-10-25 08:43 UTC95
        previous_pos: previous_pos.map(str::to_owned),
7cdaa86b9489atagen2026-10-25 08:43 UTC96
        next_pos: next_pos.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC97
        response: Default::default(),
cbb715e11963atagen2026-10-24 23:24 UTC98
        raw_rooms: [(room_id.to_owned(), raw_room)].into(),
7cdaa86b9489atagen2026-10-25 08:43 UTC99
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC100
}
7cdaa86b9489atagen2026-10-25 08:43 UTC101
7cdaa86b9489atagen2026-10-25 08:43 UTC102
fn quiet_batch(previous_pos: &str, next_pos: &str) -> ResponseCommitBatch {
7cdaa86b9489atagen2026-10-25 08:43 UTC103
    ResponseCommitBatch {
7cdaa86b9489atagen2026-10-25 08:43 UTC104
        connection_id: "mentions-connection".to_owned(),
7cdaa86b9489atagen2026-10-25 08:43 UTC105
        previous_pos: Some(previous_pos.to_owned()),
7cdaa86b9489atagen2026-10-25 08:43 UTC106
        next_pos: next_pos.to_owned(),
7cdaa86b9489atagen2026-10-25 08:43 UTC107
        response: Default::default(),
7cdaa86b9489atagen2026-10-25 08:43 UTC108
        raw_rooms: Default::default(),
7cdaa86b9489atagen2026-10-25 08:43 UTC109
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC110
}
7cdaa86b9489atagen2026-10-25 08:43 UTC111
7cdaa86b9489atagen2026-10-25 08:43 UTC112
async fn service(client: &Client, store: Option<Result<MentionsStore, Error>>) -> Service {
cbb715e11963atagen2026-10-24 23:24 UTC113
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC114
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC115
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC116
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC117
    ));
7cdaa86b9489atagen2026-10-25 08:43 UTC118
    let (_, mut service, _) = Service::new(client.clone(), media, store, EngineConfig::default())
7cdaa86b9489atagen2026-10-25 08:43 UTC119
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC120
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC121
    service.recovery_budget = 0;
7cdaa86b9489atagen2026-10-25 08:43 UTC122
    service.latest_scan = false;
7cdaa86b9489atagen2026-10-25 08:43 UTC123
    service
7cdaa86b9489atagen2026-10-25 08:43 UTC124
}
7cdaa86b9489atagen2026-10-25 08:43 UTC125
7cdaa86b9489atagen2026-10-25 08:43 UTC126
fn rows(connection: &Connection) -> BTreeMap<String, Vec<u8>> {
7cdaa86b9489atagen2026-10-25 08:43 UTC127
    let mut statement = connection
7cdaa86b9489atagen2026-10-25 08:43 UTC128
        .prepare("SELECT key, value FROM records WHERE key NOT IN ('!cipher', '!head')")
7cdaa86b9489atagen2026-10-25 08:43 UTC129
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC130
    statement
7cdaa86b9489atagen2026-10-25 08:43 UTC131
        .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
7cdaa86b9489atagen2026-10-25 08:43 UTC132
        .unwrap()
7cdaa86b9489atagen2026-10-25 08:43 UTC133
        .map(Result::unwrap)
7cdaa86b9489atagen2026-10-25 08:43 UTC134
        .collect()
7cdaa86b9489atagen2026-10-25 08:43 UTC135
}
7cdaa86b9489atagen2026-10-25 08:43 UTC136
7cdaa86b9489atagen2026-10-25 08:43 UTC137
fn discontinuities(service: &Service) -> usize {
7cdaa86b9489atagen2026-10-25 08:43 UTC138
    service
7cdaa86b9489atagen2026-10-25 08:43 UTC139
        .data
7cdaa86b9489atagen2026-10-25 08:43 UTC140
        .coverage
7cdaa86b9489atagen2026-10-25 08:43 UTC141
        .gaps
7cdaa86b9489atagen2026-10-25 08:43 UTC142
        .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC143
        .filter(|gap| gap.reason == MentionGapReason::SyncDiscontinuity)
7cdaa86b9489atagen2026-10-25 08:43 UTC144
        .count()
7cdaa86b9489atagen2026-10-25 08:43 UTC145
}
7cdaa86b9489atagen2026-10-25 08:43 UTC146
7cdaa86b9489atagen2026-10-25 08:43 UTC147
#[tokio::test]
7cdaa86b9489atagen2026-10-25 08:43 UTC148
async fn failed_write_acks_and_keeps_the_inbox_in_memory_until_a_later_write() {
7cdaa86b9489atagen2026-10-25 08:43 UTC149
    let server = MatrixMockServer::new().await;
7cdaa86b9489atagen2026-10-25 08:43 UTC150
    let room_id = room_id!("!mentions:localhost");
7cdaa86b9489atagen2026-10-25 08:43 UTC151
    let (client, own_user) = joined_client(&server, room_id).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC152
    let config = config();
7cdaa86b9489atagen2026-10-25 08:43 UTC153
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC154
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC155
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC156
    let mut service = service(&client, Some(Ok(store))).await;
cbb715e11963atagen2026-10-24 23:24 UTC157
cbb715e11963atagen2026-10-24 23:24 UTC158
    let connection = Connection::open(config.mentions_location()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC159
    connection
cbb715e11963atagen2026-10-24 23:24 UTC160
        .execute_batch(
cbb715e11963atagen2026-10-24 23:24 UTC161
            "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 UTC162
             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 UTC163
        )
cbb715e11963atagen2026-10-24 23:24 UTC164
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC165
7cdaa86b9489atagen2026-10-25 08:43 UTC166
    service
7cdaa86b9489atagen2026-10-25 08:43 UTC167
        .batch(mention_batch(
7cdaa86b9489atagen2026-10-25 08:43 UTC168
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC169
            &own_user,
7cdaa86b9489atagen2026-10-25 08:43 UTC170
            "first",
7cdaa86b9489atagen2026-10-25 08:43 UTC171
            None,
7cdaa86b9489atagen2026-10-25 08:43 UTC172
            "position-1",
7cdaa86b9489atagen2026-10-25 08:43 UTC173
        ))
7cdaa86b9489atagen2026-10-25 08:43 UTC174
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC175
    assert!(service.dirty);
7cdaa86b9489atagen2026-10-25 08:43 UTC176
    assert_eq!(service.data.entries.len(), 1);
7cdaa86b9489atagen2026-10-25 08:43 UTC177
    assert!(
7cdaa86b9489atagen2026-10-25 08:43 UTC178
        service
7cdaa86b9489atagen2026-10-25 08:43 UTC179
            .data
7cdaa86b9489atagen2026-10-25 08:43 UTC180
            .coverage
7cdaa86b9489atagen2026-10-25 08:43 UTC181
            .gaps
7cdaa86b9489atagen2026-10-25 08:43 UTC182
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC183
            .any(|gap| gap.reason == MentionGapReason::StoreFailed)
7cdaa86b9489atagen2026-10-25 08:43 UTC184
    );
7cdaa86b9489atagen2026-10-25 08:43 UTC185
    assert_eq!(service.updates.borrow().entries.len(), 1);
7cdaa86b9489atagen2026-10-25 08:43 UTC186
    assert!(rows(&connection).is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC187
cbb715e11963atagen2026-10-24 23:24 UTC188
    connection
cbb715e11963atagen2026-10-24 23:24 UTC189
        .execute_batch(
cbb715e11963atagen2026-10-24 23:24 UTC190
            "DROP TRIGGER fail_mentions_inbox_insert;
cbb715e11963atagen2026-10-24 23:24 UTC191
             DROP TRIGGER fail_mentions_inbox_update;",
cbb715e11963atagen2026-10-24 23:24 UTC192
        )
cbb715e11963atagen2026-10-24 23:24 UTC193
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC194
    service.flush_after = 0;
7cdaa86b9489atagen2026-10-25 08:43 UTC195
    service.batch(quiet_batch("position-1", "position-2")).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC196
    assert!(!service.dirty);
7cdaa86b9489atagen2026-10-25 08:43 UTC197
    drop(service);
7cdaa86b9489atagen2026-10-25 08:43 UTC198
7cdaa86b9489atagen2026-10-25 08:43 UTC199
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC200
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC201
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC202
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC203
    let sync: SyncState = reopened.read("sync").unwrap().unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC204
    assert_eq!(persisted.entries.len(), 1);
7cdaa86b9489atagen2026-10-25 08:43 UTC205
    assert_eq!(sync.inbox_revision, persisted.revision);
7cdaa86b9489atagen2026-10-25 08:43 UTC206
    assert_eq!(sync.checkpoints["mentions-connection"].pos, "position-2");
7cdaa86b9489atagen2026-10-25 08:43 UTC207
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC208
    drop(connection);
7cdaa86b9489atagen2026-10-25 08:43 UTC209
    config.delete().await.unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC210
}
cbb715e11963atagen2026-10-24 23:24 UTC211
7cdaa86b9489atagen2026-10-25 08:43 UTC212
#[tokio::test]
7cdaa86b9489atagen2026-10-25 08:43 UTC213
async fn a_quiet_response_writes_only_the_checkpoint() {
7cdaa86b9489atagen2026-10-25 08:43 UTC214
    let server = MatrixMockServer::new().await;
7cdaa86b9489atagen2026-10-25 08:43 UTC215
    let room_id = room_id!("!mentions:localhost");
7cdaa86b9489atagen2026-10-25 08:43 UTC216
    let (client, own_user) = joined_client(&server, room_id).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC217
    let config = config();
7cdaa86b9489atagen2026-10-25 08:43 UTC218
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC219
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC220
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC221
    let mut service = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC222
    service
7cdaa86b9489atagen2026-10-25 08:43 UTC223
        .batch(mention_batch(
7cdaa86b9489atagen2026-10-25 08:43 UTC224
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC225
            &own_user,
7cdaa86b9489atagen2026-10-25 08:43 UTC226
            "first",
7cdaa86b9489atagen2026-10-25 08:43 UTC227
            None,
7cdaa86b9489atagen2026-10-25 08:43 UTC228
            "position-1",
7cdaa86b9489atagen2026-10-25 08:43 UTC229
        ))
7cdaa86b9489atagen2026-10-25 08:43 UTC230
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC231
    let connection = Connection::open(config.mentions_location()).unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC232
    let before = rows(&connection);
7cdaa86b9489atagen2026-10-25 08:43 UTC233
    assert_eq!(before.len(), 2);
7cdaa86b9489atagen2026-10-25 08:43 UTC234
7cdaa86b9489atagen2026-10-25 08:43 UTC235
    service.batch(quiet_batch("position-1", "position-2")).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC236
    let after = rows(&connection);
7cdaa86b9489atagen2026-10-25 08:43 UTC237
    assert_eq!(after.len(), 2);
7cdaa86b9489atagen2026-10-25 08:43 UTC238
    assert_eq!(
7cdaa86b9489atagen2026-10-25 08:43 UTC239
        before
7cdaa86b9489atagen2026-10-25 08:43 UTC240
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC241
            .filter(|(key, value)| after.get(*key) != Some(value))
7cdaa86b9489atagen2026-10-25 08:43 UTC242
            .count(),
7cdaa86b9489atagen2026-10-25 08:43 UTC243
        1
7cdaa86b9489atagen2026-10-25 08:43 UTC244
    );
cbb715e11963atagen2026-10-24 23:24 UTC245
    assert_eq!(service.data.revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC246
    assert_eq!(
7cdaa86b9489atagen2026-10-25 08:43 UTC247
        service.sync.checkpoints["mentions-connection"].pos,
7cdaa86b9489atagen2026-10-25 08:43 UTC248
        "position-2"
cbb715e11963atagen2026-10-24 23:24 UTC249
    );
7cdaa86b9489atagen2026-10-25 08:43 UTC250
    drop(service);
7cdaa86b9489atagen2026-10-25 08:43 UTC251
    drop(connection);
7cdaa86b9489atagen2026-10-25 08:43 UTC252
    config.delete().await.unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC253
}
cbb715e11963atagen2026-10-24 23:24 UTC254
7cdaa86b9489atagen2026-10-25 08:43 UTC255
#[tokio::test]
7cdaa86b9489atagen2026-10-25 08:43 UTC256
async fn a_replay_from_the_previous_checkpoint_is_not_a_discontinuity() {
7cdaa86b9489atagen2026-10-25 08:43 UTC257
    let server = MatrixMockServer::new().await;
7cdaa86b9489atagen2026-10-25 08:43 UTC258
    let room_id = room_id!("!mentions:localhost");
7cdaa86b9489atagen2026-10-25 08:43 UTC259
    let (client, own_user) = joined_client(&server, room_id).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC260
    let config = config();
7cdaa86b9489atagen2026-10-25 08:43 UTC261
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC262
        .await
cbb715e11963atagen2026-10-24 23:24 UTC263
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC264
    let mut first = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC265
    first
7cdaa86b9489atagen2026-10-25 08:43 UTC266
        .batch(mention_batch(room_id, &own_user, "one", None, "position-1"))
7cdaa86b9489atagen2026-10-25 08:43 UTC267
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC268
    first
7cdaa86b9489atagen2026-10-25 08:43 UTC269
        .batch(mention_batch(
7cdaa86b9489atagen2026-10-25 08:43 UTC270
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC271
            &own_user,
7cdaa86b9489atagen2026-10-25 08:43 UTC272
            "two",
7cdaa86b9489atagen2026-10-25 08:43 UTC273
            Some("position-1"),
7cdaa86b9489atagen2026-10-25 08:43 UTC274
            "position-2",
7cdaa86b9489atagen2026-10-25 08:43 UTC275
        ))
7cdaa86b9489atagen2026-10-25 08:43 UTC276
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC277
    drop(first);
7cdaa86b9489atagen2026-10-25 08:43 UTC278
7cdaa86b9489atagen2026-10-25 08:43 UTC279
    // the sdk never saved position-2, so it asks again from position-1
7cdaa86b9489atagen2026-10-25 08:43 UTC280
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC281
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC282
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC283
    let mut restarted = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC284
    restarted
7cdaa86b9489atagen2026-10-25 08:43 UTC285
        .batch(mention_batch(
7cdaa86b9489atagen2026-10-25 08:43 UTC286
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC287
            &own_user,
7cdaa86b9489atagen2026-10-25 08:43 UTC288
            "three",
7cdaa86b9489atagen2026-10-25 08:43 UTC289
            Some("position-1"),
7cdaa86b9489atagen2026-10-25 08:43 UTC290
            "position-3",
7cdaa86b9489atagen2026-10-25 08:43 UTC291
        ))
7cdaa86b9489atagen2026-10-25 08:43 UTC292
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC293
    assert_eq!(discontinuities(&restarted), 0);
7cdaa86b9489atagen2026-10-25 08:43 UTC294
    assert_eq!(restarted.data.entries.len(), 3);
cbb715e11963atagen2026-10-24 23:24 UTC295
    assert_eq!(
7cdaa86b9489atagen2026-10-25 08:43 UTC296
        restarted.sync.checkpoints["mentions-connection"],
7cdaa86b9489atagen2026-10-25 08:43 UTC297
        Checkpoint {
7cdaa86b9489atagen2026-10-25 08:43 UTC298
            pos: "position-3".to_owned(),
7cdaa86b9489atagen2026-10-25 08:43 UTC299
            previous: Some("position-1".to_owned()),
7cdaa86b9489atagen2026-10-25 08:43 UTC300
        }
cbb715e11963atagen2026-10-24 23:24 UTC301
    );
cbb715e11963atagen2026-10-24 23:24 UTC302
7cdaa86b9489atagen2026-10-25 08:43 UTC303
    restarted
7cdaa86b9489atagen2026-10-25 08:43 UTC304
        .batch(quiet_batch("position-3", "position-4"))
7cdaa86b9489atagen2026-10-25 08:43 UTC305
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC306
    assert_eq!(discontinuities(&restarted), 0);
7cdaa86b9489atagen2026-10-25 08:43 UTC307
    restarted
7cdaa86b9489atagen2026-10-25 08:43 UTC308
        .batch(quiet_batch("position-1", "position-5"))
7cdaa86b9489atagen2026-10-25 08:43 UTC309
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC310
    assert_eq!(discontinuities(&restarted), 1);
7cdaa86b9489atagen2026-10-25 08:43 UTC311
    drop(restarted);
cbb715e11963atagen2026-10-24 23:24 UTC312
    config.delete().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC313
}
cbb715e11963atagen2026-10-24 23:24 UTC314
7cdaa86b9489atagen2026-10-25 08:43 UTC315
#[tokio::test]
7cdaa86b9489atagen2026-10-25 08:43 UTC316
async fn an_unreadable_store_resets_behind_a_gap() {
7cdaa86b9489atagen2026-10-25 08:43 UTC317
    let server = MatrixMockServer::new().await;
7cdaa86b9489atagen2026-10-25 08:43 UTC318
    let room_id = room_id!("!mentions:localhost");
7cdaa86b9489atagen2026-10-25 08:43 UTC319
    let (client, own_user) = joined_client(&server, room_id).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC320
    let config = config();
7cdaa86b9489atagen2026-10-25 08:43 UTC321
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC322
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC323
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC324
    let mut first = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC325
    first
7cdaa86b9489atagen2026-10-25 08:43 UTC326
        .batch(mention_batch(room_id, &own_user, "one", None, "position-1"))
7cdaa86b9489atagen2026-10-25 08:43 UTC327
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC328
    drop(first);
7cdaa86b9489atagen2026-10-25 08:43 UTC329
    let connection = Connection::open(config.mentions_location()).unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC330
    connection
7cdaa86b9489atagen2026-10-25 08:43 UTC331
        .execute(
7cdaa86b9489atagen2026-10-25 08:43 UTC332
            "UPDATE records SET value = x'00' WHERE key NOT IN ('!cipher', '!head')",
7cdaa86b9489atagen2026-10-25 08:43 UTC333
            [],
7cdaa86b9489atagen2026-10-25 08:43 UTC334
        )
7cdaa86b9489atagen2026-10-25 08:43 UTC335
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC336
    drop(connection);
80381e06a573atagen2026-10-25 03:02 UTC337
7cdaa86b9489atagen2026-10-25 08:43 UTC338
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC339
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC340
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC341
    let restarted = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC342
    assert!(restarted.data.entries.is_empty());
7cdaa86b9489atagen2026-10-25 08:43 UTC343
    assert!(
7cdaa86b9489atagen2026-10-25 08:43 UTC344
        restarted
7cdaa86b9489atagen2026-10-25 08:43 UTC345
            .data
7cdaa86b9489atagen2026-10-25 08:43 UTC346
            .coverage
7cdaa86b9489atagen2026-10-25 08:43 UTC347
            .gaps
7cdaa86b9489atagen2026-10-25 08:43 UTC348
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC349
            .any(|gap| gap.reason == MentionGapReason::StoreReset)
7cdaa86b9489atagen2026-10-25 08:43 UTC350
    );
7cdaa86b9489atagen2026-10-25 08:43 UTC351
    drop(restarted);
7cdaa86b9489atagen2026-10-25 08:43 UTC352
    config.delete().await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC353
}
80381e06a573atagen2026-10-25 03:02 UTC354
80381e06a573atagen2026-10-25 03:02 UTC355
#[tokio::test]
80381e06a573atagen2026-10-25 03:02 UTC356
async fn repair_policy_and_coverage_roundtrip_through_the_inbox() {
80381e06a573atagen2026-10-25 03:02 UTC357
    let server = MatrixMockServer::new().await;
80381e06a573atagen2026-10-25 03:02 UTC358
    let room_id = room_id!("!repair-coverage:localhost");
80381e06a573atagen2026-10-25 03:02 UTC359
    let (client, _) = joined_client(&server, room_id).await;
80381e06a573atagen2026-10-25 03:02 UTC360
    let config = config();
80381e06a573atagen2026-10-25 03:02 UTC361
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
80381e06a573atagen2026-10-25 03:02 UTC362
        .await
80381e06a573atagen2026-10-25 03:02 UTC363
        .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC364
    let media = Arc::new(MediaService::new(
80381e06a573atagen2026-10-25 03:02 UTC365
        client.clone(),
80381e06a573atagen2026-10-25 03:02 UTC366
        EngineConfig::default().max_media_bytes,
80381e06a573atagen2026-10-25 03:02 UTC367
        EngineConfig::default().max_media_sources,
80381e06a573atagen2026-10-25 03:02 UTC368
    ));
7cdaa86b9489atagen2026-10-25 08:43 UTC369
    let (handle, mut service, _) = Service::new(
7cdaa86b9489atagen2026-10-25 08:43 UTC370
        client.clone(),
7cdaa86b9489atagen2026-10-25 08:43 UTC371
        media,
7cdaa86b9489atagen2026-10-25 08:43 UTC372
        Some(Ok(store)),
7cdaa86b9489atagen2026-10-25 08:43 UTC373
        EngineConfig::default(),
7cdaa86b9489atagen2026-10-25 08:43 UTC374
    )
7cdaa86b9489atagen2026-10-25 08:43 UTC375
    .await
7cdaa86b9489atagen2026-10-25 08:43 UTC376
    .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC377
    service.recovery_budget = 0;
80381e06a573atagen2026-10-25 03:02 UTC378
    service.latest_scan = false;
80381e06a573atagen2026-10-25 03:02 UTC379
    let service_task = tokio::spawn(service.run());
80381e06a573atagen2026-10-25 03:02 UTC380
80381e06a573atagen2026-10-25 03:02 UTC381
    let default_policy = HistoryRepairPolicy::default();
80381e06a573atagen2026-10-25 03:02 UTC382
    assert_eq!(handle.repair_policy().await.unwrap(), default_policy);
80381e06a573atagen2026-10-25 03:02 UTC383
    let stored_policy = HistoryRepairPolicy::Bridge;
80381e06a573atagen2026-10-25 03:02 UTC384
    handle
80381e06a573atagen2026-10-25 03:02 UTC385
        .set_repair_policy(stored_policy.clone())
80381e06a573atagen2026-10-25 03:02 UTC386
        .await
80381e06a573atagen2026-10-25 03:02 UTC387
        .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC388
    handle.repair_resolved(room_id.to_owned()).await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC389
    let coverage = handle.repair_coverage(room_id.to_owned()).await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC390
    assert!(coverage.last_confirmed_ms.is_some());
80381e06a573atagen2026-10-25 03:02 UTC391
    assert!(!coverage.reliable);
80381e06a573atagen2026-10-25 03:02 UTC392
    assert_eq!(coverage.duration_ms(), None);
80381e06a573atagen2026-10-25 03:02 UTC393
80381e06a573atagen2026-10-25 03:02 UTC394
    handle.stop().await;
80381e06a573atagen2026-10-25 03:02 UTC395
    service_task.await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC396
    drop(handle);
80381e06a573atagen2026-10-25 03:02 UTC397
80381e06a573atagen2026-10-25 03:02 UTC398
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
80381e06a573atagen2026-10-25 03:02 UTC399
        .await
80381e06a573atagen2026-10-25 03:02 UTC400
        .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC401
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
80381e06a573atagen2026-10-25 03:02 UTC402
    assert_eq!(persisted.repair_policy, stored_policy);
7cdaa86b9489atagen2026-10-25 08:43 UTC403
    let sync: SyncState = reopened.read("sync").unwrap().unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC404
    let coverage = sync.repair_coverage.get(room_id).unwrap();
80381e06a573atagen2026-10-25 03:02 UTC405
    assert!(coverage.last_confirmed_ms.is_some());
80381e06a573atagen2026-10-25 03:02 UTC406
    assert!(coverage.boundary_ms.is_none());
80381e06a573atagen2026-10-25 03:02 UTC407
    assert!(!coverage.unresolved);
80381e06a573atagen2026-10-25 03:02 UTC408
    assert!(!coverage.reliable);
80381e06a573atagen2026-10-25 03:02 UTC409
    drop(reopened);
80381e06a573atagen2026-10-25 03:02 UTC410
    config.delete().await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC411
}
80381e06a573atagen2026-10-25 03:02 UTC412
80381e06a573atagen2026-10-25 03:02 UTC413
#[tokio::test]
80381e06a573atagen2026-10-25 03:02 UTC414
async fn repair_limited_sync_freezes_the_last_room_checkpoint() {
80381e06a573atagen2026-10-25 03:02 UTC415
    let server = MatrixMockServer::new().await;
80381e06a573atagen2026-10-25 03:02 UTC416
    let room_id = room_id!("!repair-boundary:localhost");
80381e06a573atagen2026-10-25 03:02 UTC417
    let (client, _) = joined_client(&server, room_id).await;
80381e06a573atagen2026-10-25 03:02 UTC418
    let media = Arc::new(MediaService::new(
80381e06a573atagen2026-10-25 03:02 UTC419
        client.clone(),
80381e06a573atagen2026-10-25 03:02 UTC420
        EngineConfig::default().max_media_bytes,
80381e06a573atagen2026-10-25 03:02 UTC421
        EngineConfig::default().max_media_sources,
80381e06a573atagen2026-10-25 03:02 UTC422
    ));
80381e06a573atagen2026-10-25 03:02 UTC423
    let (_handle, mut service, _) = Service::new(client, media, None, EngineConfig::default())
80381e06a573atagen2026-10-25 03:02 UTC424
        .await
80381e06a573atagen2026-10-25 03:02 UTC425
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC426
    service.sync.checkpoints.insert(
80381e06a573atagen2026-10-25 03:02 UTC427
        "mentions-connection".to_owned(),
7cdaa86b9489atagen2026-10-25 08:43 UTC428
        Checkpoint {
7cdaa86b9489atagen2026-10-25 08:43 UTC429
            pos: "position-before".to_owned(),
7cdaa86b9489atagen2026-10-25 08:43 UTC430
            previous: None,
7cdaa86b9489atagen2026-10-25 08:43 UTC431
        },
80381e06a573atagen2026-10-25 03:02 UTC432
    );
7cdaa86b9489atagen2026-10-25 08:43 UTC433
    service.sync.observed_rooms.insert(room_id.to_owned());
80381e06a573atagen2026-10-25 03:02 UTC434
    service
7cdaa86b9489atagen2026-10-25 08:43 UTC435
        .sync
80381e06a573atagen2026-10-25 03:02 UTC436
        .repair_coverage
80381e06a573atagen2026-10-25 03:02 UTC437
        .entry(room_id.to_owned())
80381e06a573atagen2026-10-25 03:02 UTC438
        .or_default()
80381e06a573atagen2026-10-25 03:02 UTC439
        .last_confirmed_ms = Some(1_000);
80381e06a573atagen2026-10-25 03:02 UTC440
80381e06a573atagen2026-10-25 03:02 UTC441
    let events = [
80381e06a573atagen2026-10-25 03:02 UTC442
        EventFactory::new()
80381e06a573atagen2026-10-25 03:02 UTC443
            .room(room_id)
80381e06a573atagen2026-10-25 03:02 UTC444
            .text_msg("older event")
80381e06a573atagen2026-10-25 03:02 UTC445
            .sender(user_id!("@sender:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC446
            .event_id(event_id!("$repair-older:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC447
            .into_raw_sync(),
80381e06a573atagen2026-10-25 03:02 UTC448
        EventFactory::new()
80381e06a573atagen2026-10-25 03:02 UTC449
            .room(room_id)
80381e06a573atagen2026-10-25 03:02 UTC450
            .text_msg("latest event")
80381e06a573atagen2026-10-25 03:02 UTC451
            .sender(user_id!("@sender:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC452
            .event_id(event_id!("$repair-latest:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC453
            .into_raw_sync(),
80381e06a573atagen2026-10-25 03:02 UTC454
    ];
80381e06a573atagen2026-10-25 03:02 UTC455
    let timeline: Vec<_> = events
80381e06a573atagen2026-10-25 03:02 UTC456
        .iter()
80381e06a573atagen2026-10-25 03:02 UTC457
        .map(|event| serde_json::to_value(event).unwrap())
80381e06a573atagen2026-10-25 03:02 UTC458
        .collect();
80381e06a573atagen2026-10-25 03:02 UTC459
    let raw_room = serde_json::from_value(json!({
80381e06a573atagen2026-10-25 03:02 UTC460
        "limited": true,
80381e06a573atagen2026-10-25 03:02 UTC461
        "prev_batch": "repair-gap-token",
80381e06a573atagen2026-10-25 03:02 UTC462
        "timeline": timeline,
80381e06a573atagen2026-10-25 03:02 UTC463
    }))
80381e06a573atagen2026-10-25 03:02 UTC464
    .unwrap();
80381e06a573atagen2026-10-25 03:02 UTC465
    let batch = ResponseCommitBatch {
80381e06a573atagen2026-10-25 03:02 UTC466
        connection_id: "mentions-connection".to_owned(),
80381e06a573atagen2026-10-25 03:02 UTC467
        previous_pos: Some("position-before".to_owned()),
80381e06a573atagen2026-10-25 03:02 UTC468
        next_pos: "position-after".to_owned(),
80381e06a573atagen2026-10-25 03:02 UTC469
        response: Default::default(),
80381e06a573atagen2026-10-25 03:02 UTC470
        raw_rooms: [(room_id.to_owned(), raw_room)].into(),
80381e06a573atagen2026-10-25 03:02 UTC471
    };
80381e06a573atagen2026-10-25 03:02 UTC472
7cdaa86b9489atagen2026-10-25 08:43 UTC473
    service.batch(batch).await;
80381e06a573atagen2026-10-25 03:02 UTC474
7cdaa86b9489atagen2026-10-25 08:43 UTC475
    let coverage = service.sync.repair_coverage.get(room_id).unwrap();
80381e06a573atagen2026-10-25 03:02 UTC476
    assert_eq!(coverage.last_confirmed_ms, Some(1_000));
80381e06a573atagen2026-10-25 03:02 UTC477
    assert!(
80381e06a573atagen2026-10-25 03:02 UTC478
        coverage
80381e06a573atagen2026-10-25 03:02 UTC479
            .boundary_ms
80381e06a573atagen2026-10-25 03:02 UTC480
            .is_some_and(|boundary| boundary > 1_000)
80381e06a573atagen2026-10-25 03:02 UTC481
    );
80381e06a573atagen2026-10-25 03:02 UTC482
    assert!(coverage.unresolved);
80381e06a573atagen2026-10-25 03:02 UTC483
    assert!(!coverage.reliable);
80381e06a573atagen2026-10-25 03:02 UTC484
    assert_eq!(coverage.duration_ms(), None);
80381e06a573atagen2026-10-25 03:02 UTC485
    assert_eq!(
80381e06a573atagen2026-10-25 03:02 UTC486
        coverage.latest_anchor.as_deref(),
80381e06a573atagen2026-10-25 03:02 UTC487
        Some(event_id!("$repair-latest:localhost"))
80381e06a573atagen2026-10-25 03:02 UTC488
    );
80381e06a573atagen2026-10-25 03:02 UTC489
}
80381e06a573atagen2026-10-25 03:02 UTC490
cbb715e11963atagen2026-10-24 23:24 UTC491
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC492
async fn service_recovers_notification_history_into_durable_unread_inbox() {
cbb715e11963atagen2026-10-24 23:24 UTC493
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC494
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC495
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC496
    let first = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC497
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC498
        .text_msg("history mention one")
cbb715e11963atagen2026-10-24 23:24 UTC499
        .mentions(Mentions::with_user_ids([own_user.clone()]))
cbb715e11963atagen2026-10-24 23:24 UTC500
        .event_id(event_id!("$history-one:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC501
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC502
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC503
    let second = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC504
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC505
        .text_msg("history mention two")
cbb715e11963atagen2026-10-24 23:24 UTC506
        .mentions(Mentions::with_user_ids([own_user]))
cbb715e11963atagen2026-10-24 23:24 UTC507
        .event_id(event_id!("$history-two:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC508
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC509
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC510
    let first = serde_json::from_str::<serde_json::Value>(first.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC511
    let second = serde_json::from_str::<serde_json::Value>(second.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC512
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC513
        .and(path("/_matrix/client/v3/notifications"))
cbb715e11963atagen2026-10-24 23:24 UTC514
        .and(query_param_is_missing("from"))
cbb715e11963atagen2026-10-24 23:24 UTC515
        .and(query_param("limit", "50"))
cbb715e11963atagen2026-10-24 23:24 UTC516
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC517
            "notifications": [{
cbb715e11963atagen2026-10-24 23:24 UTC518
                "actions": ["notify"],
cbb715e11963atagen2026-10-24 23:24 UTC519
                "event": first,
cbb715e11963atagen2026-10-24 23:24 UTC520
                "read": true,
cbb715e11963atagen2026-10-24 23:24 UTC521
                "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC522
                "ts": 1,
cbb715e11963atagen2026-10-24 23:24 UTC523
            }],
cbb715e11963atagen2026-10-24 23:24 UTC524
            "next_token": "history-page-two",
cbb715e11963atagen2026-10-24 23:24 UTC525
        })))
cbb715e11963atagen2026-10-24 23:24 UTC526
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC527
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC528
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC529
        .and(path("/_matrix/client/v3/notifications"))
cbb715e11963atagen2026-10-24 23:24 UTC530
        .and(query_param("from", "history-page-two"))
cbb715e11963atagen2026-10-24 23:24 UTC531
        .and(query_param("limit", "50"))
cbb715e11963atagen2026-10-24 23:24 UTC532
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC533
            "notifications": [{
cbb715e11963atagen2026-10-24 23:24 UTC534
                "actions": ["notify"],
cbb715e11963atagen2026-10-24 23:24 UTC535
                "event": second,
cbb715e11963atagen2026-10-24 23:24 UTC536
                "read": true,
cbb715e11963atagen2026-10-24 23:24 UTC537
                "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC538
                "ts": 2,
cbb715e11963atagen2026-10-24 23:24 UTC539
            }],
cbb715e11963atagen2026-10-24 23:24 UTC540
        })))
cbb715e11963atagen2026-10-24 23:24 UTC541
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC542
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC543
cbb715e11963atagen2026-10-24 23:24 UTC544
    let config = config();
cbb715e11963atagen2026-10-24 23:24 UTC545
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC546
        .await
cbb715e11963atagen2026-10-24 23:24 UTC547
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC548
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC549
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC550
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC551
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC552
    ));
7cdaa86b9489atagen2026-10-25 08:43 UTC553
    let (handle, service, mut updates) = Service::new(
7cdaa86b9489atagen2026-10-25 08:43 UTC554
        client.clone(),
7cdaa86b9489atagen2026-10-25 08:43 UTC555
        media,
7cdaa86b9489atagen2026-10-25 08:43 UTC556
        Some(Ok(store)),
7cdaa86b9489atagen2026-10-25 08:43 UTC557
        EngineConfig::default(),
7cdaa86b9489atagen2026-10-25 08:43 UTC558
    )
7cdaa86b9489atagen2026-10-25 08:43 UTC559
    .await
7cdaa86b9489atagen2026-10-25 08:43 UTC560
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC561
    let service_task = tokio::spawn(service.run());
cbb715e11963atagen2026-10-24 23:24 UTC562
cbb715e11963atagen2026-10-24 23:24 UTC563
    let discovered = tokio::time::timeout(Duration::from_secs(10), async {
cbb715e11963atagen2026-10-24 23:24 UTC564
        loop {
cbb715e11963atagen2026-10-24 23:24 UTC565
            let page = updates.borrow_and_update().clone();
cbb715e11963atagen2026-10-24 23:24 UTC566
            if page.entries.len() == 2 && !page.coverage.recovering {
cbb715e11963atagen2026-10-24 23:24 UTC567
                break page;
cbb715e11963atagen2026-10-24 23:24 UTC568
            }
cbb715e11963atagen2026-10-24 23:24 UTC569
            updates.changed().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC570
        }
cbb715e11963atagen2026-10-24 23:24 UTC571
    })
cbb715e11963atagen2026-10-24 23:24 UTC572
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC573
    handle.stop().await;
cbb715e11963atagen2026-10-24 23:24 UTC574
    service_task.await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC575
    let discovered = discovered.expect("notification history should be recovered");
cbb715e11963atagen2026-10-24 23:24 UTC576
    assert_eq!(discovered.entries.len(), 2);
cbb715e11963atagen2026-10-24 23:24 UTC577
    assert!(discovered.entries.iter().all(|entry| !entry.read));
cbb715e11963atagen2026-10-24 23:24 UTC578
cbb715e11963atagen2026-10-24 23:24 UTC579
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC580
        .await
cbb715e11963atagen2026-10-24 23:24 UTC581
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC582
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC583
    let sync: SyncState = reopened.read("sync").unwrap().unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC584
    assert_eq!(sync.inbox_revision, persisted.revision);
cbb715e11963atagen2026-10-24 23:24 UTC585
    assert_eq!(persisted.entries.len(), 2);
cbb715e11963atagen2026-10-24 23:24 UTC586
    assert!(persisted.entries.values().all(|entry| !entry.read));
cbb715e11963atagen2026-10-24 23:24 UTC587
    assert!(persisted.latest_cursor.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC588
    assert!(persisted.history_cursor.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC589
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC590
cbb715e11963atagen2026-10-24 23:24 UTC591
    config.delete().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC592
}
cbb715e11963atagen2026-10-24 23:24 UTC593
cbb715e11963atagen2026-10-24 23:24 UTC594
fn message_body(item: &TimelineItem) -> Option<&str> {
cbb715e11963atagen2026-10-24 23:24 UTC595
    match item {
cbb715e11963atagen2026-10-24 23:24 UTC596
        TimelineItem::Event(event) => match &event.content {
cbb715e11963atagen2026-10-24 23:24 UTC597
            TimelineContent::Message(message) => Some(&message.body),
cbb715e11963atagen2026-10-24 23:24 UTC598
            _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC599
        },
cbb715e11963atagen2026-10-24 23:24 UTC600
        _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC601
    }
cbb715e11963atagen2026-10-24 23:24 UTC602
}
cbb715e11963atagen2026-10-24 23:24 UTC603
cbb715e11963atagen2026-10-24 23:24 UTC604
fn media_registration() -> MediaRegistration {
cbb715e11963atagen2026-10-24 23:24 UTC605
    MediaRegistration {
cbb715e11963atagen2026-10-24 23:24 UTC606
        id: MediaId::new("test-scope#image".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC607
        kind: MediaKind::Image,
cbb715e11963atagen2026-10-24 23:24 UTC608
        full: MediaSourceInfo {
cbb715e11963atagen2026-10-24 23:24 UTC609
            source: MediaSource::Plain(ruma::mxc_uri!("mxc://mentions.example/image").to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC610
            mimetype: Some("image/png".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC611
            declared_size: Some(1234),
cbb715e11963atagen2026-10-24 23:24 UTC612
        },
cbb715e11963atagen2026-10-24 23:24 UTC613
        thumbnail: None,
cbb715e11963atagen2026-10-24 23:24 UTC614
    }
cbb715e11963atagen2026-10-24 23:24 UTC615
}
cbb715e11963atagen2026-10-24 23:24 UTC616
cbb715e11963atagen2026-10-24 23:24 UTC617
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC618
async fn live_context_survives_capture_then_edit_and_redaction() {
cbb715e11963atagen2026-10-24 23:24 UTC619
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC620
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC621
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC622
    let mention = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC623
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC624
        .text_msg("direct mention")
cbb715e11963atagen2026-10-24 23:24 UTC625
        .mentions(Mentions::with_user_ids([own_user]))
cbb715e11963atagen2026-10-24 23:24 UTC626
        .event_id(event_id!("$live-mention:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC627
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC628
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC629
    let mut mention: serde_json::Value = serde_json::from_str(mention.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC630
    mention["origin_server_ts"] = json!(10);
cbb715e11963atagen2026-10-24 23:24 UTC631
    let following = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC632
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC633
        .text_msg("following")
cbb715e11963atagen2026-10-24 23:24 UTC634
        .event_id(event_id!("$live-following:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC635
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC636
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC637
    let mut following: serde_json::Value = serde_json::from_str(following.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC638
    following["origin_server_ts"] = json!(11);
cbb715e11963atagen2026-10-24 23:24 UTC639
cbb715e11963atagen2026-10-24 23:24 UTC640
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC641
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC642
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC643
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC644
    ));
cbb715e11963atagen2026-10-24 23:24 UTC645
    let (_handle, mut service, _updates) =
cbb715e11963atagen2026-10-24 23:24 UTC646
        Service::new(client, media, None, EngineConfig::default())
cbb715e11963atagen2026-10-24 23:24 UTC647
            .await
cbb715e11963atagen2026-10-24 23:24 UTC648
            .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC649
    let room_id = room_id.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC650
    let mention_key = MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC651
        room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC652
        event_id: event_id!("$live-mention:localhost").to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC653
    };
cbb715e11963atagen2026-10-24 23:24 UTC654
    let following_id = format!("{room_id}\n$live-following:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC655
    let mut data = Data::default();
cbb715e11963atagen2026-10-24 23:24 UTC656
7cdaa86b9489atagen2026-10-25 08:43 UTC657
    service.observe(&mut data, &room_id, mention).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC658
    service.observe(&mut data, &room_id, following).await;
cbb715e11963atagen2026-10-24 23:24 UTC659
cbb715e11963atagen2026-10-24 23:24 UTC660
    let mention_identity = id(&mention_key);
cbb715e11963atagen2026-10-24 23:24 UTC661
    let entry = data.entries.get(&mention_identity).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC662
    let revision = entry.revision;
cbb715e11963atagen2026-10-24 23:24 UTC663
    let target = entry.event.clone();
cbb715e11963atagen2026-10-24 23:24 UTC664
    service.captured(
cbb715e11963atagen2026-10-24 23:24 UTC665
        &mut data,
cbb715e11963atagen2026-10-24 23:24 UTC666
        mention_identity.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC667
        revision,
cbb715e11963atagen2026-10-24 23:24 UTC668
        Ok((
cbb715e11963atagen2026-10-24 23:24 UTC669
            crate::EventContext {
cbb715e11963atagen2026-10-24 23:24 UTC670
                events_before: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC671
                event: Some(target),
cbb715e11963atagen2026-10-24 23:24 UTC672
                events_after: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC673
                start_token: None,
cbb715e11963atagen2026-10-24 23:24 UTC674
                end_token: None,
cbb715e11963atagen2026-10-24 23:24 UTC675
            },
cbb715e11963atagen2026-10-24 23:24 UTC676
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC677
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC678
        )),
cbb715e11963atagen2026-10-24 23:24 UTC679
    );
cbb715e11963atagen2026-10-24 23:24 UTC680
cbb715e11963atagen2026-10-24 23:24 UTC681
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC682
        data.entries[&mention_identity].capture,
cbb715e11963atagen2026-10-24 23:24 UTC683
        MentionCaptureState::Partial
cbb715e11963atagen2026-10-24 23:24 UTC684
    );
cbb715e11963atagen2026-10-24 23:24 UTC685
    assert!(data.events.contains_key(&following_id));
cbb715e11963atagen2026-10-24 23:24 UTC686
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC687
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC688
    assert_eq!(context.events_after.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC689
    assert_eq!(message_body(&context.events_after[0]), Some("following"));
cbb715e11963atagen2026-10-24 23:24 UTC690
cbb715e11963atagen2026-10-24 23:24 UTC691
    let edit = json!({
cbb715e11963atagen2026-10-24 23:24 UTC692
        "content": {
cbb715e11963atagen2026-10-24 23:24 UTC693
            "body": "* edited following",
cbb715e11963atagen2026-10-24 23:24 UTC694
            "m.new_content": {"body": "edited following", "msgtype": "m.text"},
cbb715e11963atagen2026-10-24 23:24 UTC695
            "m.relates_to": {"event_id": "$live-following:localhost", "rel_type": "m.replace"},
cbb715e11963atagen2026-10-24 23:24 UTC696
            "msgtype": "m.text"
cbb715e11963atagen2026-10-24 23:24 UTC697
        },
cbb715e11963atagen2026-10-24 23:24 UTC698
        "event_id": "$live-following-edit:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC699
        "origin_server_ts": 12,
cbb715e11963atagen2026-10-24 23:24 UTC700
        "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC701
        "sender": "@bob:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC702
        "type": "m.room.message"
cbb715e11963atagen2026-10-24 23:24 UTC703
    });
7cdaa86b9489atagen2026-10-25 08:43 UTC704
    service.observe(&mut data, &room_id, edit).await;
cbb715e11963atagen2026-10-24 23:24 UTC705
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC706
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC707
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC708
        message_body(&context.events_after[0]),
cbb715e11963atagen2026-10-24 23:24 UTC709
        Some("edited following")
cbb715e11963atagen2026-10-24 23:24 UTC710
    );
cbb715e11963atagen2026-10-24 23:24 UTC711
cbb715e11963atagen2026-10-24 23:24 UTC712
    let saved_following = data.events.get_mut(&following_id).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC713
    assert!(saved_following.raw.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC714
    saved_following.media.push(media_registration());
cbb715e11963atagen2026-10-24 23:24 UTC715
    assert_eq!(saved_following.media.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC716
    let redaction = json!({
cbb715e11963atagen2026-10-24 23:24 UTC717
        "content": {},
cbb715e11963atagen2026-10-24 23:24 UTC718
        "event_id": "$live-following-redaction:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC719
        "origin_server_ts": 13,
cbb715e11963atagen2026-10-24 23:24 UTC720
        "redacts": "$live-following:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC721
        "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC722
        "sender": "@bob:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC723
        "type": "m.room.redaction"
cbb715e11963atagen2026-10-24 23:24 UTC724
    });
7cdaa86b9489atagen2026-10-25 08:43 UTC725
    service.observe(&mut data, &room_id, redaction).await;
cbb715e11963atagen2026-10-24 23:24 UTC726
cbb715e11963atagen2026-10-24 23:24 UTC727
    let saved_following = data.events.get(&following_id).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC728
    assert!(matches!(
cbb715e11963atagen2026-10-24 23:24 UTC729
        &saved_following.item,
cbb715e11963atagen2026-10-24 23:24 UTC730
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
cbb715e11963atagen2026-10-24 23:24 UTC731
    ));
cbb715e11963atagen2026-10-24 23:24 UTC732
    assert!(saved_following.raw.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC733
    assert!(saved_following.media.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC734
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC735
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC736
    assert!(matches!(
cbb715e11963atagen2026-10-24 23:24 UTC737
        &context.events_after[0],
cbb715e11963atagen2026-10-24 23:24 UTC738
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
cbb715e11963atagen2026-10-24 23:24 UTC739
    ));
cbb715e11963atagen2026-10-24 23:24 UTC740
    data.windows.remove(&mention_identity);
cbb715e11963atagen2026-10-24 23:24 UTC741
    service.data = data;
cbb715e11963atagen2026-10-24 23:24 UTC742
    let CommandOutput::MentionContext(context) = service
cbb715e11963atagen2026-10-24 23:24 UTC743
        .command(MentionsCommand::RetryContext(mention_key))
cbb715e11963atagen2026-10-24 23:24 UTC744
        .await
cbb715e11963atagen2026-10-24 23:24 UTC745
        .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC746
    else {
cbb715e11963atagen2026-10-24 23:24 UTC747
        panic!("retry should return saved context");
cbb715e11963atagen2026-10-24 23:24 UTC748
    };
cbb715e11963atagen2026-10-24 23:24 UTC749
    assert_eq!(context.state, MentionCaptureState::Pending);
cbb715e11963atagen2026-10-24 23:24 UTC750
    assert!(context.context.unwrap().event.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC751
}
cbb715e11963atagen2026-10-24 23:24 UTC752
cbb715e11963atagen2026-10-24 23:24 UTC753
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC754
async fn explicit_recovery_rechecks_server_support() {
cbb715e11963atagen2026-10-24 23:24 UTC755
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC756
    let (client, _) = joined_client(&server, room_id!("!mentions:localhost")).await;
cbb715e11963atagen2026-10-24 23:24 UTC757
    let config = EngineConfig::default();
cbb715e11963atagen2026-10-24 23:24 UTC758
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC759
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC760
        config.max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC761
        config.max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC762
    ));
cbb715e11963atagen2026-10-24 23:24 UTC763
    let (_, mut service, _) = Service::new(client, media, None, config).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC764
    service.data.coverage.historical_supported = Some(false);
cbb715e11963atagen2026-10-24 23:24 UTC765
    service.recovery_budget = 0;
cbb715e11963atagen2026-10-24 23:24 UTC766
    service.command(MentionsCommand::Recover).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC767
    assert_eq!(service.data.coverage.historical_supported, None);
cbb715e11963atagen2026-10-24 23:24 UTC768
    assert!(service.data.coverage.recovering);
cbb715e11963atagen2026-10-24 23:24 UTC769
    assert_eq!(service.recovery_budget, 20);
cbb715e11963atagen2026-10-24 23:24 UTC770
}