atagen

macltok

public
Star0
Watch0
5b7fee5557101b4156546295abfcb1f6e242e3e5
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
82d37c369846atagen2026-10-25 10:55 UTC112
async fn service(client: &Client, store: Option<Result<MentionsStore, Unopened>>) -> 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,
da8896938cf0atagen2026-10-25 10:04 UTC116
        EngineConfig::default().max_context_media,
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);
82d37c369846atagen2026-10-25 10:55 UTC233
    assert_eq!(before.len(), 3);
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);
82d37c369846atagen2026-10-25 10:55 UTC237
    assert_eq!(after.len(), 3);
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]
82d37c369846atagen2026-10-25 10:55 UTC256
async fn a_failed_command_write_leaves_a_store_that_still_opens() {
82d37c369846atagen2026-10-25 10:55 UTC257
    let server = MatrixMockServer::new().await;
82d37c369846atagen2026-10-25 10:55 UTC258
    let room_id = room_id!("!mentions:localhost");
82d37c369846atagen2026-10-25 10:55 UTC259
    let (client, own_user) = joined_client(&server, room_id).await;
82d37c369846atagen2026-10-25 10:55 UTC260
    let config = config();
82d37c369846atagen2026-10-25 10:55 UTC261
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
82d37c369846atagen2026-10-25 10:55 UTC262
        .await
82d37c369846atagen2026-10-25 10:55 UTC263
        .unwrap();
82d37c369846atagen2026-10-25 10:55 UTC264
    let mut first = service(&client, Some(Ok(store))).await;
82d37c369846atagen2026-10-25 10:55 UTC265
    first
82d37c369846atagen2026-10-25 10:55 UTC266
        .batch(mention_batch(
82d37c369846atagen2026-10-25 10:55 UTC267
            room_id,
82d37c369846atagen2026-10-25 10:55 UTC268
            &own_user,
82d37c369846atagen2026-10-25 10:55 UTC269
            "first",
82d37c369846atagen2026-10-25 10:55 UTC270
            None,
82d37c369846atagen2026-10-25 10:55 UTC271
            "position-1",
82d37c369846atagen2026-10-25 10:55 UTC272
        ))
82d37c369846atagen2026-10-25 10:55 UTC273
        .await;
82d37c369846atagen2026-10-25 10:55 UTC274
    let key = first.data.entries.values().next().unwrap().key.clone();
82d37c369846atagen2026-10-25 10:55 UTC275
82d37c369846atagen2026-10-25 10:55 UTC276
    let connection = Connection::open(config.mentions_location()).unwrap();
82d37c369846atagen2026-10-25 10:55 UTC277
    connection
82d37c369846atagen2026-10-25 10:55 UTC278
        .execute_batch(
82d37c369846atagen2026-10-25 10:55 UTC279
            "CREATE TRIGGER fail_mentions_update BEFORE UPDATE ON records WHEN OLD.key NOT IN ('!cipher', '!head') BEGIN SELECT RAISE(ABORT, 'injected write failure'); END;",
82d37c369846atagen2026-10-25 10:55 UTC280
        )
82d37c369846atagen2026-10-25 10:55 UTC281
        .unwrap();
82d37c369846atagen2026-10-25 10:55 UTC282
    assert!(
82d37c369846atagen2026-10-25 10:55 UTC283
        first
82d37c369846atagen2026-10-25 10:55 UTC284
            .command(MentionsCommand::SetRead {
82d37c369846atagen2026-10-25 10:55 UTC285
                keys: vec![key],
82d37c369846atagen2026-10-25 10:55 UTC286
                read: true,
82d37c369846atagen2026-10-25 10:55 UTC287
            })
82d37c369846atagen2026-10-25 10:55 UTC288
            .await
82d37c369846atagen2026-10-25 10:55 UTC289
            .is_err()
82d37c369846atagen2026-10-25 10:55 UTC290
    );
82d37c369846atagen2026-10-25 10:55 UTC291
    connection
82d37c369846atagen2026-10-25 10:55 UTC292
        .execute_batch("DROP TRIGGER fail_mentions_update;")
82d37c369846atagen2026-10-25 10:55 UTC293
        .unwrap();
82d37c369846atagen2026-10-25 10:55 UTC294
    drop(connection);
82d37c369846atagen2026-10-25 10:55 UTC295
    first.batch(quiet_batch("position-1", "position-2")).await;
82d37c369846atagen2026-10-25 10:55 UTC296
    drop(first);
82d37c369846atagen2026-10-25 10:55 UTC297
82d37c369846atagen2026-10-25 10:55 UTC298
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
82d37c369846atagen2026-10-25 10:55 UTC299
        .await
82d37c369846atagen2026-10-25 10:55 UTC300
        .unwrap();
82d37c369846atagen2026-10-25 10:55 UTC301
    let restarted = service(&client, Some(Ok(store))).await;
82d37c369846atagen2026-10-25 10:55 UTC302
    assert_eq!(restarted.data.entries.len(), 1);
82d37c369846atagen2026-10-25 10:55 UTC303
    assert!(
82d37c369846atagen2026-10-25 10:55 UTC304
        restarted
82d37c369846atagen2026-10-25 10:55 UTC305
            .data
82d37c369846atagen2026-10-25 10:55 UTC306
            .coverage
82d37c369846atagen2026-10-25 10:55 UTC307
            .gaps
82d37c369846atagen2026-10-25 10:55 UTC308
            .iter()
82d37c369846atagen2026-10-25 10:55 UTC309
            .all(|gap| gap.reason != MentionGapReason::StoreReset)
82d37c369846atagen2026-10-25 10:55 UTC310
    );
82d37c369846atagen2026-10-25 10:55 UTC311
    assert_eq!(
82d37c369846atagen2026-10-25 10:55 UTC312
        restarted.sync.checkpoints["mentions-connection"].pos,
82d37c369846atagen2026-10-25 10:55 UTC313
        "position-2"
82d37c369846atagen2026-10-25 10:55 UTC314
    );
82d37c369846atagen2026-10-25 10:55 UTC315
    drop(restarted);
82d37c369846atagen2026-10-25 10:55 UTC316
    config.delete().await.unwrap();
82d37c369846atagen2026-10-25 10:55 UTC317
}
82d37c369846atagen2026-10-25 10:55 UTC318
82d37c369846atagen2026-10-25 10:55 UTC319
#[tokio::test]
7cdaa86b9489atagen2026-10-25 08:43 UTC320
async fn a_replay_from_the_previous_checkpoint_is_not_a_discontinuity() {
7cdaa86b9489atagen2026-10-25 08:43 UTC321
    let server = MatrixMockServer::new().await;
7cdaa86b9489atagen2026-10-25 08:43 UTC322
    let room_id = room_id!("!mentions:localhost");
7cdaa86b9489atagen2026-10-25 08:43 UTC323
    let (client, own_user) = joined_client(&server, room_id).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC324
    let config = config();
7cdaa86b9489atagen2026-10-25 08:43 UTC325
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC326
        .await
cbb715e11963atagen2026-10-24 23:24 UTC327
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC328
    let mut first = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC329
    first
7cdaa86b9489atagen2026-10-25 08:43 UTC330
        .batch(mention_batch(room_id, &own_user, "one", None, "position-1"))
7cdaa86b9489atagen2026-10-25 08:43 UTC331
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC332
    first
7cdaa86b9489atagen2026-10-25 08:43 UTC333
        .batch(mention_batch(
7cdaa86b9489atagen2026-10-25 08:43 UTC334
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC335
            &own_user,
7cdaa86b9489atagen2026-10-25 08:43 UTC336
            "two",
7cdaa86b9489atagen2026-10-25 08:43 UTC337
            Some("position-1"),
7cdaa86b9489atagen2026-10-25 08:43 UTC338
            "position-2",
7cdaa86b9489atagen2026-10-25 08:43 UTC339
        ))
7cdaa86b9489atagen2026-10-25 08:43 UTC340
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC341
    drop(first);
7cdaa86b9489atagen2026-10-25 08:43 UTC342
7cdaa86b9489atagen2026-10-25 08:43 UTC343
    // the sdk never saved position-2, so it asks again from position-1
7cdaa86b9489atagen2026-10-25 08:43 UTC344
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC345
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC346
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC347
    let mut restarted = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC348
    restarted
7cdaa86b9489atagen2026-10-25 08:43 UTC349
        .batch(mention_batch(
7cdaa86b9489atagen2026-10-25 08:43 UTC350
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC351
            &own_user,
7cdaa86b9489atagen2026-10-25 08:43 UTC352
            "three",
7cdaa86b9489atagen2026-10-25 08:43 UTC353
            Some("position-1"),
7cdaa86b9489atagen2026-10-25 08:43 UTC354
            "position-3",
7cdaa86b9489atagen2026-10-25 08:43 UTC355
        ))
7cdaa86b9489atagen2026-10-25 08:43 UTC356
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC357
    assert_eq!(discontinuities(&restarted), 0);
7cdaa86b9489atagen2026-10-25 08:43 UTC358
    assert_eq!(restarted.data.entries.len(), 3);
cbb715e11963atagen2026-10-24 23:24 UTC359
    assert_eq!(
7cdaa86b9489atagen2026-10-25 08:43 UTC360
        restarted.sync.checkpoints["mentions-connection"],
7cdaa86b9489atagen2026-10-25 08:43 UTC361
        Checkpoint {
7cdaa86b9489atagen2026-10-25 08:43 UTC362
            pos: "position-3".to_owned(),
7cdaa86b9489atagen2026-10-25 08:43 UTC363
            previous: Some("position-1".to_owned()),
7cdaa86b9489atagen2026-10-25 08:43 UTC364
        }
cbb715e11963atagen2026-10-24 23:24 UTC365
    );
cbb715e11963atagen2026-10-24 23:24 UTC366
7cdaa86b9489atagen2026-10-25 08:43 UTC367
    restarted
7cdaa86b9489atagen2026-10-25 08:43 UTC368
        .batch(quiet_batch("position-3", "position-4"))
7cdaa86b9489atagen2026-10-25 08:43 UTC369
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC370
    assert_eq!(discontinuities(&restarted), 0);
7cdaa86b9489atagen2026-10-25 08:43 UTC371
    restarted
7cdaa86b9489atagen2026-10-25 08:43 UTC372
        .batch(quiet_batch("position-1", "position-5"))
7cdaa86b9489atagen2026-10-25 08:43 UTC373
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC374
    assert_eq!(discontinuities(&restarted), 1);
7cdaa86b9489atagen2026-10-25 08:43 UTC375
    drop(restarted);
cbb715e11963atagen2026-10-24 23:24 UTC376
    config.delete().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC377
}
cbb715e11963atagen2026-10-24 23:24 UTC378
7cdaa86b9489atagen2026-10-25 08:43 UTC379
#[tokio::test]
7cdaa86b9489atagen2026-10-25 08:43 UTC380
async fn an_unreadable_store_resets_behind_a_gap() {
7cdaa86b9489atagen2026-10-25 08:43 UTC381
    let server = MatrixMockServer::new().await;
7cdaa86b9489atagen2026-10-25 08:43 UTC382
    let room_id = room_id!("!mentions:localhost");
7cdaa86b9489atagen2026-10-25 08:43 UTC383
    let (client, own_user) = joined_client(&server, room_id).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC384
    let config = config();
7cdaa86b9489atagen2026-10-25 08:43 UTC385
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC386
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC387
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC388
    let mut first = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC389
    first
7cdaa86b9489atagen2026-10-25 08:43 UTC390
        .batch(mention_batch(room_id, &own_user, "one", None, "position-1"))
7cdaa86b9489atagen2026-10-25 08:43 UTC391
        .await;
7cdaa86b9489atagen2026-10-25 08:43 UTC392
    drop(first);
7cdaa86b9489atagen2026-10-25 08:43 UTC393
    let connection = Connection::open(config.mentions_location()).unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC394
    connection
7cdaa86b9489atagen2026-10-25 08:43 UTC395
        .execute(
7cdaa86b9489atagen2026-10-25 08:43 UTC396
            "UPDATE records SET value = x'00' WHERE key NOT IN ('!cipher', '!head')",
7cdaa86b9489atagen2026-10-25 08:43 UTC397
            [],
7cdaa86b9489atagen2026-10-25 08:43 UTC398
        )
7cdaa86b9489atagen2026-10-25 08:43 UTC399
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC400
    drop(connection);
80381e06a573atagen2026-10-25 03:02 UTC401
7cdaa86b9489atagen2026-10-25 08:43 UTC402
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
7cdaa86b9489atagen2026-10-25 08:43 UTC403
        .await
7cdaa86b9489atagen2026-10-25 08:43 UTC404
        .unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC405
    let restarted = service(&client, Some(Ok(store))).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC406
    assert!(restarted.data.entries.is_empty());
7cdaa86b9489atagen2026-10-25 08:43 UTC407
    assert!(
7cdaa86b9489atagen2026-10-25 08:43 UTC408
        restarted
7cdaa86b9489atagen2026-10-25 08:43 UTC409
            .data
7cdaa86b9489atagen2026-10-25 08:43 UTC410
            .coverage
7cdaa86b9489atagen2026-10-25 08:43 UTC411
            .gaps
7cdaa86b9489atagen2026-10-25 08:43 UTC412
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC413
            .any(|gap| gap.reason == MentionGapReason::StoreReset)
7cdaa86b9489atagen2026-10-25 08:43 UTC414
    );
7cdaa86b9489atagen2026-10-25 08:43 UTC415
    drop(restarted);
7cdaa86b9489atagen2026-10-25 08:43 UTC416
    config.delete().await.unwrap();
80381e06a573atagen2026-10-25 03:02 UTC417
}
80381e06a573atagen2026-10-25 03:02 UTC418
80381e06a573atagen2026-10-25 03:02 UTC419
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC420
async fn service_recovers_notification_history_into_durable_unread_inbox() {
cbb715e11963atagen2026-10-24 23:24 UTC421
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC422
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC423
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC424
    let first = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC425
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC426
        .text_msg("history mention one")
cbb715e11963atagen2026-10-24 23:24 UTC427
        .mentions(Mentions::with_user_ids([own_user.clone()]))
cbb715e11963atagen2026-10-24 23:24 UTC428
        .event_id(event_id!("$history-one:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC429
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC430
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC431
    let second = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC432
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC433
        .text_msg("history mention two")
cbb715e11963atagen2026-10-24 23:24 UTC434
        .mentions(Mentions::with_user_ids([own_user]))
cbb715e11963atagen2026-10-24 23:24 UTC435
        .event_id(event_id!("$history-two:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC436
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC437
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC438
    let first = serde_json::from_str::<serde_json::Value>(first.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC439
    let second = serde_json::from_str::<serde_json::Value>(second.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC440
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC441
        .and(path("/_matrix/client/v3/notifications"))
cbb715e11963atagen2026-10-24 23:24 UTC442
        .and(query_param_is_missing("from"))
cbb715e11963atagen2026-10-24 23:24 UTC443
        .and(query_param("limit", "50"))
cbb715e11963atagen2026-10-24 23:24 UTC444
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC445
            "notifications": [{
cbb715e11963atagen2026-10-24 23:24 UTC446
                "actions": ["notify"],
cbb715e11963atagen2026-10-24 23:24 UTC447
                "event": first,
cbb715e11963atagen2026-10-24 23:24 UTC448
                "read": true,
cbb715e11963atagen2026-10-24 23:24 UTC449
                "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC450
                "ts": 1,
cbb715e11963atagen2026-10-24 23:24 UTC451
            }],
cbb715e11963atagen2026-10-24 23:24 UTC452
            "next_token": "history-page-two",
cbb715e11963atagen2026-10-24 23:24 UTC453
        })))
cbb715e11963atagen2026-10-24 23:24 UTC454
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC455
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC456
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC457
        .and(path("/_matrix/client/v3/notifications"))
cbb715e11963atagen2026-10-24 23:24 UTC458
        .and(query_param("from", "history-page-two"))
cbb715e11963atagen2026-10-24 23:24 UTC459
        .and(query_param("limit", "50"))
cbb715e11963atagen2026-10-24 23:24 UTC460
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC461
            "notifications": [{
cbb715e11963atagen2026-10-24 23:24 UTC462
                "actions": ["notify"],
cbb715e11963atagen2026-10-24 23:24 UTC463
                "event": second,
cbb715e11963atagen2026-10-24 23:24 UTC464
                "read": true,
cbb715e11963atagen2026-10-24 23:24 UTC465
                "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC466
                "ts": 2,
cbb715e11963atagen2026-10-24 23:24 UTC467
            }],
cbb715e11963atagen2026-10-24 23:24 UTC468
        })))
cbb715e11963atagen2026-10-24 23:24 UTC469
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC470
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC471
cbb715e11963atagen2026-10-24 23:24 UTC472
    let config = config();
cbb715e11963atagen2026-10-24 23:24 UTC473
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC474
        .await
cbb715e11963atagen2026-10-24 23:24 UTC475
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC476
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC477
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC478
        EngineConfig::default().max_media_bytes,
da8896938cf0atagen2026-10-25 10:04 UTC479
        EngineConfig::default().max_context_media,
cbb715e11963atagen2026-10-24 23:24 UTC480
    ));
7cdaa86b9489atagen2026-10-25 08:43 UTC481
    let (handle, service, mut updates) = Service::new(
7cdaa86b9489atagen2026-10-25 08:43 UTC482
        client.clone(),
7cdaa86b9489atagen2026-10-25 08:43 UTC483
        media,
7cdaa86b9489atagen2026-10-25 08:43 UTC484
        Some(Ok(store)),
7cdaa86b9489atagen2026-10-25 08:43 UTC485
        EngineConfig::default(),
7cdaa86b9489atagen2026-10-25 08:43 UTC486
    )
7cdaa86b9489atagen2026-10-25 08:43 UTC487
    .await
7cdaa86b9489atagen2026-10-25 08:43 UTC488
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC489
    let service_task = tokio::spawn(service.run());
cbb715e11963atagen2026-10-24 23:24 UTC490
cbb715e11963atagen2026-10-24 23:24 UTC491
    let discovered = tokio::time::timeout(Duration::from_secs(10), async {
cbb715e11963atagen2026-10-24 23:24 UTC492
        loop {
cbb715e11963atagen2026-10-24 23:24 UTC493
            let page = updates.borrow_and_update().clone();
cbb715e11963atagen2026-10-24 23:24 UTC494
            if page.entries.len() == 2 && !page.coverage.recovering {
cbb715e11963atagen2026-10-24 23:24 UTC495
                break page;
cbb715e11963atagen2026-10-24 23:24 UTC496
            }
cbb715e11963atagen2026-10-24 23:24 UTC497
            updates.changed().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC498
        }
cbb715e11963atagen2026-10-24 23:24 UTC499
    })
cbb715e11963atagen2026-10-24 23:24 UTC500
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC501
    handle.stop().await;
cbb715e11963atagen2026-10-24 23:24 UTC502
    service_task.await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC503
    let discovered = discovered.expect("notification history should be recovered");
cbb715e11963atagen2026-10-24 23:24 UTC504
    assert_eq!(discovered.entries.len(), 2);
cbb715e11963atagen2026-10-24 23:24 UTC505
    assert!(discovered.entries.iter().all(|entry| !entry.read));
cbb715e11963atagen2026-10-24 23:24 UTC506
cbb715e11963atagen2026-10-24 23:24 UTC507
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC508
        .await
cbb715e11963atagen2026-10-24 23:24 UTC509
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC510
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC511
    let sync: SyncState = reopened.read("sync").unwrap().unwrap();
7cdaa86b9489atagen2026-10-25 08:43 UTC512
    assert_eq!(sync.inbox_revision, persisted.revision);
cbb715e11963atagen2026-10-24 23:24 UTC513
    assert_eq!(persisted.entries.len(), 2);
cbb715e11963atagen2026-10-24 23:24 UTC514
    assert!(persisted.entries.values().all(|entry| !entry.read));
cbb715e11963atagen2026-10-24 23:24 UTC515
    assert!(persisted.latest_cursor.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC516
    assert!(persisted.history_cursor.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC517
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC518
cbb715e11963atagen2026-10-24 23:24 UTC519
    config.delete().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC520
}
cbb715e11963atagen2026-10-24 23:24 UTC521
cbb715e11963atagen2026-10-24 23:24 UTC522
fn message_body(item: &TimelineItem) -> Option<&str> {
cbb715e11963atagen2026-10-24 23:24 UTC523
    match item {
cbb715e11963atagen2026-10-24 23:24 UTC524
        TimelineItem::Event(event) => match &event.content {
cbb715e11963atagen2026-10-24 23:24 UTC525
            TimelineContent::Message(message) => Some(&message.body),
cbb715e11963atagen2026-10-24 23:24 UTC526
            _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC527
        },
cbb715e11963atagen2026-10-24 23:24 UTC528
        _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC529
    }
cbb715e11963atagen2026-10-24 23:24 UTC530
}
cbb715e11963atagen2026-10-24 23:24 UTC531
cbb715e11963atagen2026-10-24 23:24 UTC532
fn media_registration() -> MediaRegistration {
cbb715e11963atagen2026-10-24 23:24 UTC533
    MediaRegistration {
cbb715e11963atagen2026-10-24 23:24 UTC534
        id: MediaId::new("test-scope#image".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC535
        kind: MediaKind::Image,
cbb715e11963atagen2026-10-24 23:24 UTC536
        full: MediaSourceInfo {
cbb715e11963atagen2026-10-24 23:24 UTC537
            source: MediaSource::Plain(ruma::mxc_uri!("mxc://mentions.example/image").to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC538
            mimetype: Some("image/png".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC539
            declared_size: Some(1234),
cbb715e11963atagen2026-10-24 23:24 UTC540
        },
cbb715e11963atagen2026-10-24 23:24 UTC541
        thumbnail: None,
cbb715e11963atagen2026-10-24 23:24 UTC542
    }
cbb715e11963atagen2026-10-24 23:24 UTC543
}
cbb715e11963atagen2026-10-24 23:24 UTC544
cbb715e11963atagen2026-10-24 23:24 UTC545
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC546
async fn live_context_survives_capture_then_edit_and_redaction() {
cbb715e11963atagen2026-10-24 23:24 UTC547
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC548
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC549
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC550
    let mention = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC551
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC552
        .text_msg("direct mention")
cbb715e11963atagen2026-10-24 23:24 UTC553
        .mentions(Mentions::with_user_ids([own_user]))
cbb715e11963atagen2026-10-24 23:24 UTC554
        .event_id(event_id!("$live-mention:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC555
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC556
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC557
    let mut mention: serde_json::Value = serde_json::from_str(mention.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC558
    mention["origin_server_ts"] = json!(10);
cbb715e11963atagen2026-10-24 23:24 UTC559
    let following = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC560
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC561
        .text_msg("following")
cbb715e11963atagen2026-10-24 23:24 UTC562
        .event_id(event_id!("$live-following:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC563
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC564
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC565
    let mut following: serde_json::Value = serde_json::from_str(following.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC566
    following["origin_server_ts"] = json!(11);
cbb715e11963atagen2026-10-24 23:24 UTC567
cbb715e11963atagen2026-10-24 23:24 UTC568
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC569
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC570
        EngineConfig::default().max_media_bytes,
da8896938cf0atagen2026-10-25 10:04 UTC571
        EngineConfig::default().max_context_media,
cbb715e11963atagen2026-10-24 23:24 UTC572
    ));
cbb715e11963atagen2026-10-24 23:24 UTC573
    let (_handle, mut service, _updates) =
cbb715e11963atagen2026-10-24 23:24 UTC574
        Service::new(client, media, None, EngineConfig::default())
cbb715e11963atagen2026-10-24 23:24 UTC575
            .await
cbb715e11963atagen2026-10-24 23:24 UTC576
            .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC577
    let room_id = room_id.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC578
    let mention_key = MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC579
        room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC580
        event_id: event_id!("$live-mention:localhost").to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC581
    };
cbb715e11963atagen2026-10-24 23:24 UTC582
    let following_id = format!("{room_id}\n$live-following:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC583
    let mut data = Data::default();
cbb715e11963atagen2026-10-24 23:24 UTC584
7cdaa86b9489atagen2026-10-25 08:43 UTC585
    service.observe(&mut data, &room_id, mention).await;
7cdaa86b9489atagen2026-10-25 08:43 UTC586
    service.observe(&mut data, &room_id, following).await;
cbb715e11963atagen2026-10-24 23:24 UTC587
cbb715e11963atagen2026-10-24 23:24 UTC588
    let mention_identity = id(&mention_key);
cbb715e11963atagen2026-10-24 23:24 UTC589
    let entry = data.entries.get(&mention_identity).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC590
    let revision = entry.revision;
cbb715e11963atagen2026-10-24 23:24 UTC591
    let target = entry.event.clone();
cbb715e11963atagen2026-10-24 23:24 UTC592
    service.captured(
cbb715e11963atagen2026-10-24 23:24 UTC593
        &mut data,
cbb715e11963atagen2026-10-24 23:24 UTC594
        mention_identity.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC595
        revision,
cbb715e11963atagen2026-10-24 23:24 UTC596
        Ok((
cbb715e11963atagen2026-10-24 23:24 UTC597
            crate::EventContext {
cbb715e11963atagen2026-10-24 23:24 UTC598
                events_before: Vec::new(),
258c08ec2168atagen2026-10-25 09:15 UTC599
                event: target,
cbb715e11963atagen2026-10-24 23:24 UTC600
                events_after: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC601
                start_token: None,
cbb715e11963atagen2026-10-24 23:24 UTC602
                end_token: None,
cbb715e11963atagen2026-10-24 23:24 UTC603
            },
cbb715e11963atagen2026-10-24 23:24 UTC604
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC605
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC606
        )),
cbb715e11963atagen2026-10-24 23:24 UTC607
    );
cbb715e11963atagen2026-10-24 23:24 UTC608
cbb715e11963atagen2026-10-24 23:24 UTC609
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC610
        data.entries[&mention_identity].capture,
cbb715e11963atagen2026-10-24 23:24 UTC611
        MentionCaptureState::Partial
cbb715e11963atagen2026-10-24 23:24 UTC612
    );
cbb715e11963atagen2026-10-24 23:24 UTC613
    assert!(data.events.contains_key(&following_id));
cbb715e11963atagen2026-10-24 23:24 UTC614
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC615
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC616
    assert_eq!(context.events_after.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC617
    assert_eq!(message_body(&context.events_after[0]), Some("following"));
cbb715e11963atagen2026-10-24 23:24 UTC618
cbb715e11963atagen2026-10-24 23:24 UTC619
    let edit = json!({
cbb715e11963atagen2026-10-24 23:24 UTC620
        "content": {
cbb715e11963atagen2026-10-24 23:24 UTC621
            "body": "* edited following",
cbb715e11963atagen2026-10-24 23:24 UTC622
            "m.new_content": {"body": "edited following", "msgtype": "m.text"},
cbb715e11963atagen2026-10-24 23:24 UTC623
            "m.relates_to": {"event_id": "$live-following:localhost", "rel_type": "m.replace"},
cbb715e11963atagen2026-10-24 23:24 UTC624
            "msgtype": "m.text"
cbb715e11963atagen2026-10-24 23:24 UTC625
        },
cbb715e11963atagen2026-10-24 23:24 UTC626
        "event_id": "$live-following-edit:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC627
        "origin_server_ts": 12,
cbb715e11963atagen2026-10-24 23:24 UTC628
        "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC629
        "sender": "@bob:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC630
        "type": "m.room.message"
cbb715e11963atagen2026-10-24 23:24 UTC631
    });
7cdaa86b9489atagen2026-10-25 08:43 UTC632
    service.observe(&mut data, &room_id, edit).await;
cbb715e11963atagen2026-10-24 23:24 UTC633
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC634
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC635
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC636
        message_body(&context.events_after[0]),
cbb715e11963atagen2026-10-24 23:24 UTC637
        Some("edited following")
cbb715e11963atagen2026-10-24 23:24 UTC638
    );
cbb715e11963atagen2026-10-24 23:24 UTC639
cbb715e11963atagen2026-10-24 23:24 UTC640
    let saved_following = data.events.get_mut(&following_id).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC641
    assert!(saved_following.raw.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC642
    saved_following.media.push(media_registration());
cbb715e11963atagen2026-10-24 23:24 UTC643
    assert_eq!(saved_following.media.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC644
    let redaction = json!({
cbb715e11963atagen2026-10-24 23:24 UTC645
        "content": {},
cbb715e11963atagen2026-10-24 23:24 UTC646
        "event_id": "$live-following-redaction:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC647
        "origin_server_ts": 13,
cbb715e11963atagen2026-10-24 23:24 UTC648
        "redacts": "$live-following:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC649
        "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC650
        "sender": "@bob:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC651
        "type": "m.room.redaction"
cbb715e11963atagen2026-10-24 23:24 UTC652
    });
7cdaa86b9489atagen2026-10-25 08:43 UTC653
    service.observe(&mut data, &room_id, redaction).await;
cbb715e11963atagen2026-10-24 23:24 UTC654
cbb715e11963atagen2026-10-24 23:24 UTC655
    let saved_following = data.events.get(&following_id).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC656
    assert!(matches!(
cbb715e11963atagen2026-10-24 23:24 UTC657
        &saved_following.item,
cbb715e11963atagen2026-10-24 23:24 UTC658
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
cbb715e11963atagen2026-10-24 23:24 UTC659
    ));
cbb715e11963atagen2026-10-24 23:24 UTC660
    assert!(saved_following.raw.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC661
    assert!(saved_following.media.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC662
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC663
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC664
    assert!(matches!(
cbb715e11963atagen2026-10-24 23:24 UTC665
        &context.events_after[0],
cbb715e11963atagen2026-10-24 23:24 UTC666
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
cbb715e11963atagen2026-10-24 23:24 UTC667
    ));
cbb715e11963atagen2026-10-24 23:24 UTC668
    data.windows.remove(&mention_identity);
cbb715e11963atagen2026-10-24 23:24 UTC669
    service.data = data;
cbb715e11963atagen2026-10-24 23:24 UTC670
    let CommandOutput::MentionContext(context) = service
cbb715e11963atagen2026-10-24 23:24 UTC671
        .command(MentionsCommand::RetryContext(mention_key))
cbb715e11963atagen2026-10-24 23:24 UTC672
        .await
cbb715e11963atagen2026-10-24 23:24 UTC673
        .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC674
    else {
cbb715e11963atagen2026-10-24 23:24 UTC675
        panic!("retry should return saved context");
cbb715e11963atagen2026-10-24 23:24 UTC676
    };
cbb715e11963atagen2026-10-24 23:24 UTC677
    assert_eq!(context.state, MentionCaptureState::Pending);
258c08ec2168atagen2026-10-25 09:15 UTC678
    assert!(context.context.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC679
}
cbb715e11963atagen2026-10-24 23:24 UTC680
cbb715e11963atagen2026-10-24 23:24 UTC681
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC682
async fn explicit_recovery_rechecks_server_support() {
cbb715e11963atagen2026-10-24 23:24 UTC683
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC684
    let (client, _) = joined_client(&server, room_id!("!mentions:localhost")).await;
cbb715e11963atagen2026-10-24 23:24 UTC685
    let config = EngineConfig::default();
cbb715e11963atagen2026-10-24 23:24 UTC686
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC687
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC688
        config.max_media_bytes,
da8896938cf0atagen2026-10-25 10:04 UTC689
        config.max_context_media,
cbb715e11963atagen2026-10-24 23:24 UTC690
    ));
cbb715e11963atagen2026-10-24 23:24 UTC691
    let (_, mut service, _) = Service::new(client, media, None, config).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC692
    service.data.coverage.historical_supported = Some(false);
cbb715e11963atagen2026-10-24 23:24 UTC693
    service.recovery_budget = 0;
cbb715e11963atagen2026-10-24 23:24 UTC694
    service.command(MentionsCommand::Recover).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC695
    assert_eq!(service.data.coverage.historical_supported, None);
cbb715e11963atagen2026-10-24 23:24 UTC696
    assert!(service.data.coverage.recovering);
cbb715e11963atagen2026-10-24 23:24 UTC697
    assert_eq!(service.recovery_budget, 20);
cbb715e11963atagen2026-10-24 23:24 UTC698
}
93d4735d70cfatagen2026-10-26 01:32 UTC699
93d4735d70cfatagen2026-10-26 01:32 UTC700
#[tokio::test]
93d4735d70cfatagen2026-10-26 01:32 UTC701
async fn observing_again_keeps_the_profiles_a_capture_fetched() {
93d4735d70cfatagen2026-10-26 01:32 UTC702
    let server = MatrixMockServer::new().await;
93d4735d70cfatagen2026-10-26 01:32 UTC703
    let room_id = room_id!("!mentions:localhost");
93d4735d70cfatagen2026-10-26 01:32 UTC704
    let (client, own_user) = joined_client(&server, room_id).await;
93d4735d70cfatagen2026-10-26 01:32 UTC705
    let raw = |event: matrix_sdk::ruma::serde::Raw<_>, timestamp: u64| {
93d4735d70cfatagen2026-10-26 01:32 UTC706
        let mut value: serde_json::Value = serde_json::from_str(event.json().get()).unwrap();
93d4735d70cfatagen2026-10-26 01:32 UTC707
        value["origin_server_ts"] = json!(timestamp);
93d4735d70cfatagen2026-10-26 01:32 UTC708
        value
93d4735d70cfatagen2026-10-26 01:32 UTC709
    };
038cc6faeb14atagen2026-10-26 02:05 UTC710
    let factory = EventFactory::new()
038cc6faeb14atagen2026-10-26 02:05 UTC711
        .room(room_id)
038cc6faeb14atagen2026-10-26 02:05 UTC712
        .sender(user_id!("@bob:localhost"));
93d4735d70cfatagen2026-10-26 01:32 UTC713
    let mention = raw(
93d4735d70cfatagen2026-10-26 01:32 UTC714
        factory
93d4735d70cfatagen2026-10-26 01:32 UTC715
            .text_msg("ping")
93d4735d70cfatagen2026-10-26 01:32 UTC716
            .mentions(Mentions::with_user_ids([own_user]))
93d4735d70cfatagen2026-10-26 01:32 UTC717
            .event_id(event_id!("$ping:localhost"))
93d4735d70cfatagen2026-10-26 01:32 UTC718
            .into_raw_sync(),
93d4735d70cfatagen2026-10-26 01:32 UTC719
        10,
93d4735d70cfatagen2026-10-26 01:32 UTC720
    );
93d4735d70cfatagen2026-10-26 01:32 UTC721
    let following = raw(
93d4735d70cfatagen2026-10-26 01:32 UTC722
        factory
93d4735d70cfatagen2026-10-26 01:32 UTC723
            .text_msg("following")
93d4735d70cfatagen2026-10-26 01:32 UTC724
            .event_id(event_id!("$following:localhost"))
93d4735d70cfatagen2026-10-26 01:32 UTC725
            .into_raw_sync(),
93d4735d70cfatagen2026-10-26 01:32 UTC726
        11,
93d4735d70cfatagen2026-10-26 01:32 UTC727
    );
93d4735d70cfatagen2026-10-26 01:32 UTC728
    let service = service(&client, None).await;
93d4735d70cfatagen2026-10-26 01:32 UTC729
    let room_id = room_id.to_owned();
93d4735d70cfatagen2026-10-26 01:32 UTC730
    let mut data = Data::default();
93d4735d70cfatagen2026-10-26 01:32 UTC731
    service.observe(&mut data, &room_id, mention.clone()).await;
93d4735d70cfatagen2026-10-26 01:32 UTC732
    let ping = format!("{room_id}\n$ping:localhost");
93d4735d70cfatagen2026-10-26 01:32 UTC733
    let following_id = format!("{room_id}\n$following:localhost");
93d4735d70cfatagen2026-10-26 01:32 UTC734
93d4735d70cfatagen2026-10-26 01:32 UTC735
    let profiled = |item: &TimelineItem| {
93d4735d70cfatagen2026-10-26 01:32 UTC736
        let mut item = item.clone();
93d4735d70cfatagen2026-10-26 01:32 UTC737
        if let TimelineItem::Event(event) = &mut item {
93d4735d70cfatagen2026-10-26 01:32 UTC738
            event.sender.display_name = Some("Bob".to_owned());
93d4735d70cfatagen2026-10-26 01:32 UTC739
        }
93d4735d70cfatagen2026-10-26 01:32 UTC740
        item
93d4735d70cfatagen2026-10-26 01:32 UTC741
    };
93d4735d70cfatagen2026-10-26 01:32 UTC742
    let target = profiled(&data.entries[&ping].event);
038cc6faeb14atagen2026-10-26 02:05 UTC743
    let mut neighbour = normalise_observed(&following, None, &following_id)
038cc6faeb14atagen2026-10-26 02:05 UTC744
        .unwrap()
038cc6faeb14atagen2026-10-26 02:05 UTC745
        .0;
93d4735d70cfatagen2026-10-26 01:32 UTC746
    neighbour = profiled(&neighbour);
93d4735d70cfatagen2026-10-26 01:32 UTC747
    let revision = data.entries[&ping].revision;
93d4735d70cfatagen2026-10-26 01:32 UTC748
    service.captured(
93d4735d70cfatagen2026-10-26 01:32 UTC749
        &mut data,
93d4735d70cfatagen2026-10-26 01:32 UTC750
        ping.clone(),
93d4735d70cfatagen2026-10-26 01:32 UTC751
        revision,
93d4735d70cfatagen2026-10-26 01:32 UTC752
        Ok((
93d4735d70cfatagen2026-10-26 01:32 UTC753
            crate::EventContext {
93d4735d70cfatagen2026-10-26 01:32 UTC754
                events_before: Vec::new(),
93d4735d70cfatagen2026-10-26 01:32 UTC755
                event: target,
93d4735d70cfatagen2026-10-26 01:32 UTC756
                events_after: vec![neighbour],
93d4735d70cfatagen2026-10-26 01:32 UTC757
                start_token: None,
93d4735d70cfatagen2026-10-26 01:32 UTC758
                end_token: None,
93d4735d70cfatagen2026-10-26 01:32 UTC759
            },
93d4735d70cfatagen2026-10-26 01:32 UTC760
            Vec::new(),
93d4735d70cfatagen2026-10-26 01:32 UTC761
            Vec::new(),
93d4735d70cfatagen2026-10-26 01:32 UTC762
        )),
93d4735d70cfatagen2026-10-26 01:32 UTC763
    );
93d4735d70cfatagen2026-10-26 01:32 UTC764
    let capture = data.entries[&ping].capture.clone();
93d4735d70cfatagen2026-10-26 01:32 UTC765
93d4735d70cfatagen2026-10-26 01:32 UTC766
    service.observe(&mut data, &room_id, following).await;
93d4735d70cfatagen2026-10-26 01:32 UTC767
    service.observe(&mut data, &room_id, mention).await;
93d4735d70cfatagen2026-10-26 01:32 UTC768
93d4735d70cfatagen2026-10-26 01:32 UTC769
    let name = |item: &TimelineItem| match item {
93d4735d70cfatagen2026-10-26 01:32 UTC770
        TimelineItem::Event(event) => event.sender.display_name.clone(),
93d4735d70cfatagen2026-10-26 01:32 UTC771
        _ => None,
93d4735d70cfatagen2026-10-26 01:32 UTC772
    };
93d4735d70cfatagen2026-10-26 01:32 UTC773
    assert_eq!(name(&data.entries[&ping].event).as_deref(), Some("Bob"));
93d4735d70cfatagen2026-10-26 01:32 UTC774
    assert_eq!(name(&data.events[&ping].item).as_deref(), Some("Bob"));
038cc6faeb14atagen2026-10-26 02:05 UTC775
    assert_eq!(
038cc6faeb14atagen2026-10-26 02:05 UTC776
        name(&data.events[&following_id].item).as_deref(),
038cc6faeb14atagen2026-10-26 02:05 UTC777
        Some("Bob")
038cc6faeb14atagen2026-10-26 02:05 UTC778
    );
93d4735d70cfatagen2026-10-26 01:32 UTC779
    assert_eq!(data.entries[&ping].capture, capture);
93d4735d70cfatagen2026-10-26 01:32 UTC780
}