Skip to main content

ruma_lean/
lib.rs

1// Copyright 2026 Shane Jaroch
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![no_std]
16
17extern crate alloc;
18
19pub mod auth;
20
21use alloc::collections::BTreeSet;
22use alloc::collections::{BTreeMap, BinaryHeap};
23
24use alloc::string::String;
25use alloc::vec::Vec;
26use core::cmp::Ordering;
27use serde::{Deserialize, Serialize};
28
29use serde_json::Value;
30
31#[cfg(feature = "std")]
32extern crate std;
33
34#[cfg(feature = "std")]
35pub use std::collections::HashMap;
36
37#[cfg(not(feature = "std"))]
38pub use hashbrown::HashMap;
39
40/// The version of the Matrix State Resolution algorithm to use.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
43pub enum StateResVersion {
44    V1,
45    V2,
46    V2_1,
47}
48
49/// Result of Kahn's topological sort with diagnostic information.
50#[derive(Debug, Clone)]
51pub enum KahnSortResult {
52    /// All events were successfully sorted.
53    Ok(Vec<String>),
54    /// A cycle was detected. `sorted` contains the partial ordering of events
55    /// that could be processed, `stuck` contains events that could not reach
56    /// in-degree 0 (involved in cycles).
57    CycleDetected {
58        sorted: Vec<String>,
59        stuck: Vec<String>,
60    },
61}
62
63impl KahnSortResult {
64    /// Returns the sorted event IDs, or an empty vec if a cycle was detected.
65    /// This preserves backward compatibility with the old API.
66    pub fn into_sorted(self) -> Vec<String> {
67        match self {
68            KahnSortResult::Ok(v) => v,
69            KahnSortResult::CycleDetected { .. } => Vec::new(),
70        }
71    }
72
73    /// Returns true if sorting completed without cycles.
74    pub fn is_ok(&self) -> bool {
75        matches!(self, KahnSortResult::Ok(_))
76    }
77}
78
79/// Synapse-compatible power level deserialization.
80/// Handles integer (100), string ("100"), and float (100.0) representations.
81fn deserialize_power_level<'de, D>(deserializer: D) -> Result<i64, D::Error>
82where
83    D: serde::Deserializer<'de>,
84{
85    use serde::de;
86
87    struct PowerLevelVisitor;
88
89    impl<'de> de::Visitor<'de> for PowerLevelVisitor {
90        type Value = i64;
91
92        fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
93            formatter.write_str("an integer, float, or string representation of a power level")
94        }
95
96        fn visit_i64<E: de::Error>(self, v: i64) -> Result<i64, E> {
97            Ok(v)
98        }
99
100        fn visit_u64<E: de::Error>(self, v: u64) -> Result<i64, E> {
101            Ok(v as i64)
102        }
103
104        fn visit_f64<E: de::Error>(self, v: f64) -> Result<i64, E> {
105            Ok(v as i64)
106        }
107
108        fn visit_str<E: de::Error>(self, v: &str) -> Result<i64, E> {
109            Ok(v.parse::<i64>()
110                .or_else(|_| v.parse::<f64>().map(|f| f as i64))
111                .unwrap_or(0))
112        }
113    }
114
115    deserializer.deserialize_any(PowerLevelVisitor)
116}
117
118/// A lightweight Matrix Event representation for Lean-equivalent resolution.
119#[derive(Debug, Clone, Serialize, Deserialize, Default)]
120pub struct LeanEvent {
121    pub event_id: String,
122    #[serde(rename = "type")]
123    pub event_type: String,
124    #[serde(default)]
125    pub state_key: String,
126    #[serde(default, deserialize_with = "deserialize_power_level")]
127    pub power_level: i64,
128    pub origin_server_ts: u64,
129    #[serde(default)]
130    pub sender: String,
131    #[serde(default)]
132    pub content: Value,
133    #[serde(default)]
134    pub prev_events: Vec<String>,
135    #[serde(default)]
136    pub auth_events: Vec<String>,
137    #[serde(default)]
138    pub depth: u64, // Required for V1
139}
140
141impl PartialEq for LeanEvent {
142    fn eq(&self, other: &Self) -> bool {
143        self.event_id == other.event_id
144    }
145}
146
147impl Eq for LeanEvent {}
148
149impl Ord for LeanEvent {
150    fn cmp(&self, other: &Self) -> Ordering {
151        self.event_id.cmp(&other.event_id)
152    }
153}
154
155impl PartialOrd for LeanEvent {
156    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
157        Some(self.cmp(other))
158    }
159}
160
161/// A wrapper to ensure BinaryHeap pops the "smallest" (best) event first.
162#[derive(Debug, Clone, Copy)]
163struct SortPriority<'a> {
164    event: &'a LeanEvent,
165    version: StateResVersion,
166}
167
168impl<'a> PartialEq for SortPriority<'a> {
169    fn eq(&self, other: &Self) -> bool {
170        self.cmp(other) == Ordering::Equal
171    }
172}
173
174impl<'a> Eq for SortPriority<'a> {}
175
176impl<'a> Ord for SortPriority<'a> {
177    fn cmp(&self, other: &Self) -> Ordering {
178        match self.version {
179            StateResVersion::V1 => {
180                // V1 tie-breaking: depth (asc) -> event_id (asc)
181                // Inverted for Max-Heap
182                match other.event.depth.cmp(&self.event.depth) {
183                    Ordering::Equal => other.event.event_id.cmp(&self.event.event_id),
184                    ord => ord,
185                }
186            }
187            StateResVersion::V2 | StateResVersion::V2_1 => {
188                // V2 tie-breaking: power_level (desc) -> origin_server_ts (asc) -> event_id (asc)
189                // To have "best" events come LAST in the sorted list, we must pop "worst" events FIRST.
190                // In Rust's Max-Heap BinaryHeap, "greater" elements are popped first.
191                // So "worst" must be "greater" than "best".
192
193                // Higher power level is BETTER (should win = come last = be smallest = pop last).
194                // So lower power_level pops first (is "greater" in max-heap).
195                match other.event.power_level.cmp(&self.event.power_level) {
196                    Ordering::Equal => {
197                        // Later timestamp is BETTER (should win = come last = be smallest).
198                        // So earlier timestamp pops first (is "greater" in max-heap).
199                        match other
200                            .event
201                            .origin_server_ts
202                            .cmp(&self.event.origin_server_ts)
203                        {
204                            Ordering::Equal => {
205                                // Lexicographically SMALLER ID is BETTER (pops last).
206                                // Larger ID pops first (is "greater" in max-heap).
207                                self.event.event_id.cmp(&other.event.event_id)
208                            }
209                            ord => ord,
210                        }
211                    }
212                    ord => ord,
213                }
214            }
215        }
216    }
217}
218
219impl<'a> PartialOrd for SortPriority<'a> {
220    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
221        Some(self.cmp(other))
222    }
223}
224
225/// Kahn's Topological Sort with full diagnostic output.
226/// Returns a `KahnSortResult` that distinguishes between successful sorts
227/// and cycle detection, providing the stuck set for debugging.
228pub fn lean_kahn_sort_detailed(
229    events: &HashMap<String, LeanEvent>,
230    version: StateResVersion,
231) -> KahnSortResult {
232    let mut in_degree: HashMap<String, usize> = HashMap::new();
233    let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
234
235    for (id, event) in events {
236        in_degree.entry(id.clone()).or_insert(0);
237        for auth in &event.auth_events {
238            if events.contains_key(auth) {
239                adjacency.entry(auth.clone()).or_default().push(id.clone());
240                *in_degree.entry(id.clone()).or_insert(0) += 1;
241            }
242        }
243    }
244
245    let mut queue: BinaryHeap<SortPriority> = BinaryHeap::new();
246    for (id, &degree) in &in_degree {
247        if degree == 0 {
248            if let Some(event) = events.get(id) {
249                queue.push(SortPriority { event, version });
250            }
251        }
252    }
253
254    let mut result = Vec::new();
255    while let Some(priority) = queue.pop() {
256        let event = priority.event;
257        result.push(event.event_id.clone());
258        if let Some(neighbors) = adjacency.get(&event.event_id) {
259            for next_id in neighbors {
260                let degree = in_degree.get_mut(next_id).unwrap();
261                *degree -= 1;
262                if *degree == 0 {
263                    queue.push(SortPriority {
264                        event: events.get(next_id).unwrap(),
265                        version,
266                    });
267                }
268            }
269        }
270    }
271
272    // Detect cycles: events that never reached in-degree 0.
273    if result.len() != events.len() {
274        let sorted_set: alloc::collections::BTreeSet<&String> = result.iter().collect();
275        let stuck: Vec<String> = events
276            .keys()
277            .filter(|id| !sorted_set.contains(id))
278            .cloned()
279            .collect();
280        return KahnSortResult::CycleDetected {
281            sorted: result,
282            stuck,
283        };
284    }
285
286    KahnSortResult::Ok(result)
287}
288
289/// A simplified implementation of Kahn's Topological Sort.
290/// Backward-compatible wrapper that returns an empty Vec on cycles.
291pub fn lean_kahn_sort(
292    events: &HashMap<String, LeanEvent>,
293    version: StateResVersion,
294) -> Vec<String> {
295    lean_kahn_sort_detailed(events, version).into_sorted()
296}
297
298pub fn resolve_lean(
299    unconflicted_state: BTreeMap<(String, String), String>,
300    conflicted_events: HashMap<String, LeanEvent>,
301    version: StateResVersion,
302) -> BTreeMap<(String, String), String> {
303    // MSC4297 (v2.1): The algorithm starts from an empty set of state.
304    let (mut resolved, sort_set) = match version {
305        StateResVersion::V2_1 => (BTreeMap::new(), conflicted_events.clone()),
306        _ => (unconflicted_state, conflicted_events),
307    };
308
309    // Route all events through Kahn sort (reverse topological power ordering).
310    // The spec classifies only certain m.room.member events as "power events,"
311    // but empirical testing against production homeservers shows that ALL member
312    // events go through Kahn sort, not mainline sort. Mainline sort is only used
313    // for non-member state events (topic, name, etc.) where PL chain proximity
314    // determines winner.
315    let mut power_events = HashMap::new();
316    let mut non_power_events = HashMap::new();
317
318    for (id, ev) in &sort_set {
319        if ev.event_type == "m.room.member"
320            || ev.event_type == "m.room.create"
321            || ev.event_type == "m.room.power_levels"
322            || ev.event_type == "m.room.join_rules"
323        {
324            power_events.insert(id.clone(), ev.clone());
325        } else {
326            non_power_events.insert(id.clone(), ev.clone());
327        }
328    }
329
330    // Step 1: Sort power events by reverse topological power ordering (Kahn sort)
331    // Step 2: Apply iterative auth checks (per spec & Ruma implementation)
332    let sorted_power_ids = lean_kahn_sort(&power_events, version);
333    for id in &sorted_power_ids {
334        if let Some(event) = sort_set.get(id) {
335            if iterative_auth_ok(event, &resolved, &sort_set) {
336                resolved.insert(
337                    (event.event_type.clone(), event.state_key.clone()),
338                    event.event_id.clone(),
339                );
340            }
341        }
342    }
343
344    // Step 3: Build the power-level mainline for mainline sort
345    let mainline = build_mainline(&resolved, &sort_set);
346
347    // Step 4: Sort non-power events by mainline ordering + iterative auth check
348    let mut non_power_list: Vec<&LeanEvent> = non_power_events.values().collect();
349    mainline_sort(&mut non_power_list, &mainline, &sort_set);
350
351    for ev in non_power_list {
352        if iterative_auth_ok(ev, &resolved, &sort_set) {
353            resolved.insert(
354                (ev.event_type.clone(), ev.state_key.clone()),
355                ev.event_id.clone(),
356            );
357        }
358    }
359
360    resolved
361}
362
363/// Targeted iterative auth check: reject m.room.member events when the
364/// resolved state already has a ban/kick for that user from a different sender
365/// (i.e., a moderator action). This prevents stale join forks from overwriting
366/// moderation actions resolved in earlier iterations.
367fn iterative_auth_ok(
368    event: &LeanEvent,
369    resolved: &BTreeMap<(String, String), String>,
370    all_events: &HashMap<String, LeanEvent>,
371) -> bool {
372    // Only check m.room.member events where membership is join or invite
373    if event.event_type == "m.room.member" {
374        let new_membership = event
375            .content
376            .get("membership")
377            .and_then(|v| v.as_str())
378            .unwrap_or("");
379
380        if new_membership == "join" || new_membership == "invite" {
381            let target_key = (
382                alloc::string::String::from("m.room.member"),
383                event.state_key.clone(),
384            );
385            if let Some(resolved_eid) = resolved.get(&target_key) {
386                if let Some(resolved_ev) = all_events.get(resolved_eid) {
387                    let resolved_membership = resolved_ev
388                        .content
389                        .get("membership")
390                        .and_then(|v| v.as_str())
391                        .unwrap_or("");
392                    // Only bans permanently prevent joins. Kicks (leave) allow rejoin.
393                    if resolved_membership == "ban" && resolved_ev.sender != resolved_ev.state_key {
394                        return false;
395                    }
396                }
397            }
398        }
399    }
400
401    true
402}
403
404/// Build the power-level mainline: the chain of m.room.power_levels events
405/// from the resolved PL event backwards through auth_events.
406fn build_mainline(
407    resolved: &BTreeMap<(String, String), String>,
408    all_events: &HashMap<String, LeanEvent>,
409) -> Vec<String> {
410    let mut mainline = Vec::new();
411    let pl_key = (
412        alloc::string::String::from("m.room.power_levels"),
413        alloc::string::String::new(),
414    );
415    let mut current = resolved.get(&pl_key).cloned();
416
417    while let Some(eid) = current {
418        mainline.push(eid.clone());
419        current = None;
420        if let Some(ev) = all_events.get(&eid) {
421            for auth_id in &ev.auth_events {
422                if let Some(auth_ev) = all_events.get(auth_id) {
423                    if auth_ev.event_type == "m.room.power_levels" {
424                        current = Some(auth_id.clone());
425                        break;
426                    }
427                }
428            }
429        }
430    }
431
432    mainline
433}
434
435/// Find the closest mainline event for a given event by walking its auth chain.
436/// Returns the index in the mainline (0 = most recent PL event = best position).
437fn closest_mainline_position(
438    event: &LeanEvent,
439    mainline: &[String],
440    all_events: &HashMap<String, LeanEvent>,
441) -> usize {
442    // Check if this event itself is on the mainline
443    if let Some(pos) = mainline.iter().position(|id| id == &event.event_id) {
444        return pos;
445    }
446
447    // Walk auth_events to find the closest mainline event
448    let mut visited = alloc::collections::BTreeSet::new();
449    let mut stack: Vec<String> = event.auth_events.clone();
450
451    while let Some(auth_id) = stack.pop() {
452        if !visited.insert(auth_id.clone()) {
453            continue;
454        }
455        if let Some(pos) = mainline.iter().position(|id| id == &auth_id) {
456            return pos;
457        }
458        if let Some(auth_ev) = all_events.get(&auth_id) {
459            for parent_auth in &auth_ev.auth_events {
460                stack.push(parent_auth.clone());
461            }
462        }
463    }
464
465    // Not connected to mainline at all — worst position
466    mainline.len()
467}
468
469/// Sort events by mainline ordering per the Matrix spec:
470/// 1. Closest mainline position (smaller index = closer to current PL = better = wins = comes last)
471/// 2. origin_server_ts ascending (earlier first, later wins via last-write)
472/// 3. event_id ascending (smaller first)
473fn mainline_sort(
474    events: &mut Vec<&LeanEvent>,
475    mainline: &[String],
476    all_events: &HashMap<String, LeanEvent>,
477) {
478    // Pre-compute mainline positions
479    let positions: HashMap<String, usize> = events
480        .iter()
481        .map(|ev| {
482            (
483                ev.event_id.clone(),
484                closest_mainline_position(ev, mainline, all_events),
485            )
486        })
487        .collect();
488
489    events.sort_by(|a, b| {
490        let pos_a = positions.get(&a.event_id).copied().unwrap_or(usize::MAX);
491        let pos_b = positions.get(&b.event_id).copied().unwrap_or(usize::MAX);
492
493        // Larger mainline position = farther from current PL = worse = comes first
494        // (so it gets overwritten by closer events via last-write-wins)
495        match pos_b.cmp(&pos_a) {
496            Ordering::Equal => {
497                // Earlier timestamp comes first (later wins via last-write)
498                match a.origin_server_ts.cmp(&b.origin_server_ts) {
499                    Ordering::Equal => a.event_id.cmp(&b.event_id),
500                    ord => ord,
501                }
502            }
503            ord => ord,
504        }
505    });
506}
507
508/// Result of conflicted subgraph computation with diagnostic info.
509#[derive(Debug, Clone)]
510pub struct SubgraphResult {
511    /// The computed conflicted subgraph.
512    pub subgraph: HashMap<String, LeanEvent>,
513    /// Auth events referenced but not found in the graph (permanently lost to federation).
514    pub missing_auth_events: Vec<String>,
515}
516
517pub fn compute_v2_1_conflicted_subgraph(
518    auth_graph: &HashMap<String, LeanEvent>,
519    conflicted_set: &[String],
520) -> HashMap<String, LeanEvent> {
521    compute_v2_1_conflicted_subgraph_bounded(auth_graph, conflicted_set, None).subgraph
522}
523
524/// Bounded version of conflicted subgraph computation.
525/// `max_auth_depth`: If set, limits backwards traversal depth to prevent
526/// history-flooding DoS attacks where a rogue admin generates millions of
527/// spoofed events on a dead-end fork.
528pub fn compute_v2_1_conflicted_subgraph_bounded(
529    auth_graph: &HashMap<String, LeanEvent>,
530    conflicted_set: &[String],
531    max_auth_depth: Option<usize>,
532) -> SubgraphResult {
533    let mut backwards_reachable = BTreeSet::new();
534    let mut forwards_reachable = BTreeSet::new();
535    let mut missing_auth_events = BTreeSet::new();
536
537    // 1. Calculate Backwards Reachable (Ancestors up the auth chain)
538    // Each entry is (event_id, depth_from_conflicted_set)
539    let mut b_stack: Vec<(String, usize)> = conflicted_set.iter().map(|s| (s.clone(), 0)).collect();
540    while let Some((node, depth)) = b_stack.pop() {
541        // Anti-DoS: stop expanding beyond max depth
542        if let Some(max_depth) = max_auth_depth {
543            if depth > max_depth {
544                continue;
545            }
546        }
547        if backwards_reachable.insert(node.clone()) {
548            if let Some(event) = auth_graph.get(&node) {
549                for auth_id in &event.auth_events {
550                    if !auth_graph.contains_key(auth_id) {
551                        missing_auth_events.insert(auth_id.clone());
552                    }
553                    b_stack.push((auth_id.clone(), depth + 1));
554                }
555            }
556        }
557    }
558
559    // 2. Build Reverse Adjacency for Forwards Search
560    let mut children_map: HashMap<String, Vec<String>> = HashMap::new();
561    for (id, event) in auth_graph {
562        for prev in &event.auth_events {
563            children_map
564                .entry(prev.clone())
565                .or_default()
566                .push(id.clone());
567        }
568    }
569
570    // 3. Calculate Forwards Reachable (Descendants down the auth chain)
571    let mut f_stack: Vec<String> = conflicted_set.to_vec();
572    while let Some(node) = f_stack.pop() {
573        if forwards_reachable.insert(node.clone()) {
574            if let Some(children) = children_map.get(&node) {
575                f_stack.extend(children.clone());
576            }
577        }
578    }
579
580    // 4. Intersect and build the final Conflicted Subgraph
581    let mut subgraph = HashMap::new();
582    let backwards_ids: BTreeSet<String> = backwards_reachable.iter().cloned().collect();
583    let forwards_ids: BTreeSet<String> = forwards_reachable.iter().cloned().collect();
584
585    for id in backwards_ids.intersection(&forwards_ids) {
586        if let Some(event) = auth_graph.get(id) {
587            subgraph.insert(id.clone(), event.clone());
588        }
589    }
590
591    SubgraphResult {
592        subgraph,
593        missing_auth_events: missing_auth_events.into_iter().collect(),
594    }
595}
596
597#[cfg(feature = "zkvm")]
598pub fn verify_signature(_public_key: &[u8; 32], _message: &[u8], _signature: &[u8; 64]) {
599    // Verifiable signature check for ZKVM environment
600}
601
602#[cfg(all(feature = "std", not(feature = "zkvm")))]
603pub fn verify_signature(public_key: &[u8; 32], message: &[u8], signature: &[u8; 64]) {
604    use ed25519_consensus::{Signature, VerificationKey};
605    let vk = VerificationKey::try_from(*public_key).expect("Invalid public key");
606    let sig = Signature::from(*signature);
607    vk.verify(&sig, message)
608        .expect("Signature verification failed");
609}
610
611#[cfg(all(not(feature = "std"), not(feature = "zkvm")))]
612pub fn verify_signature(_public_key: &[u8; 32], _message: &[u8], _signature: &[u8; 64]) {
613    // No-op for other configurations
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619    use alloc::string::ToString;
620    use alloc::vec;
621
622    #[cfg(not(feature = "std"))]
623    use hashbrown::HashMap;
624    #[cfg(feature = "std")]
625    use std::collections::HashMap;
626
627    #[test]
628    fn test_leanevent_deserialization_defaults() {
629        let json = r#"{
630            "event_id": "$test",
631            "type": "m.room.message",
632            "origin_server_ts": 12345
633        }"#;
634        let ev: LeanEvent = serde_json::from_str(json).unwrap();
635        assert_eq!(ev.event_id, "$test");
636        assert_eq!(ev.event_type, "m.room.message");
637        assert_eq!(ev.origin_server_ts, 12345);
638        assert_eq!(ev.state_key, "");
639        assert_eq!(ev.power_level, 0);
640        assert_eq!(ev.sender, "");
641        assert_eq!(ev.prev_events.len(), 0);
642        assert_eq!(ev.auth_events.len(), 0);
643        assert_eq!(ev.depth, 0);
644    }
645
646    #[test]
647    fn test_sort_priority_v2_tie_break() {
648        let e_base = LeanEvent {
649            event_id: "$1".into(),
650            power_level: 100,
651            origin_server_ts: 10,
652            ..Default::default()
653        };
654        let e_worst_pl = LeanEvent {
655            event_id: "$2".into(),
656            power_level: 50,
657            origin_server_ts: 10,
658            ..Default::default()
659        };
660        let p_base = SortPriority {
661            event: &e_base,
662            version: StateResVersion::V2,
663        };
664        let p_worst_pl = SortPriority {
665            event: &e_worst_pl,
666            version: StateResVersion::V2,
667        };
668
669        // Worse events (lower PL) should be GREATER so they pop FIRST from Max-Heap.
670        assert_eq!(p_base.cmp(&p_worst_pl), Ordering::Less); // p_worst_pl has power 50, p_base 100. Lower pl pops first = Greater.
671
672        let e_later_ts = LeanEvent {
673            event_id: "$3".into(),
674            power_level: 100,
675            origin_server_ts: 20,
676            ..Default::default()
677        };
678        let p_later_ts = SortPriority {
679            event: &e_later_ts,
680            version: StateResVersion::V2,
681        };
682        // p_later_ts has ts 20 (better — wins), p_base has ts 10 (worse — pops first = Greater).
683        assert_eq!(p_base.cmp(&p_later_ts), Ordering::Greater);
684
685        let e_larger_id = LeanEvent {
686            event_id: "$2".into(),
687            power_level: 100,
688            origin_server_ts: 10,
689            ..Default::default()
690        };
691        let p_larger_id = SortPriority {
692            event: &e_larger_id,
693            version: StateResVersion::V2,
694        };
695        // p_larger_id has id "$2", p_base has id "$1". Larger ID pops first = Greater.
696        assert_eq!(p_base.cmp(&p_larger_id), Ordering::Less);
697    }
698
699    #[test]
700    fn test_v1_resolution_happy_path() {
701        let mut events = HashMap::new();
702        events.insert(
703            "A".into(),
704            LeanEvent {
705                event_id: "A".into(),
706                event_type: "m.room.member".into(),
707                state_key: "@alice:example.com".into(),
708                power_level: 0,
709                origin_server_ts: 100,
710                prev_events: vec![],
711                auth_events: vec![],
712                depth: 1,
713                ..Default::default()
714            },
715        );
716        events.insert(
717            "B".into(),
718            LeanEvent {
719                event_id: "B".into(),
720                event_type: "m.room.member".into(),
721                state_key: "@alice:example.com".into(),
722                power_level: 0,
723                origin_server_ts: 50,
724                prev_events: vec![],
725                auth_events: vec!["A".into()],
726                depth: 2,
727                ..Default::default()
728            },
729        );
730        let sorted = lean_kahn_sort(&events, StateResVersion::V1);
731        assert_eq!(sorted, vec!["A", "B"]);
732    }
733
734    #[test]
735    fn test_v2_1_strict_resolution() {
736        let mut unconflicted = BTreeMap::new();
737        unconflicted.insert(
738            ("m.room.member".into(), "@alice:example.com".into()),
739            "A".into(),
740        );
741
742        let mut conflicted = HashMap::new();
743        conflicted.insert(
744            "A".into(),
745            LeanEvent {
746                event_id: "A".into(),
747                event_type: "m.room.member".into(),
748                state_key: "@alice:example.com".into(),
749                power_level: 50,
750                origin_server_ts: 100,
751                prev_events: vec![],
752                auth_events: vec![],
753                depth: 1,
754                ..Default::default()
755            },
756        );
757        conflicted.insert(
758            "B".into(),
759            LeanEvent {
760                event_id: "B".into(),
761                event_type: "m.room.member".into(),
762                state_key: "@alice:example.com".into(),
763                power_level: 100,
764                origin_server_ts: 50,
765                prev_events: vec![],
766                auth_events: vec![],
767                depth: 1,
768                ..Default::default()
769            },
770        );
771
772        // In V2, A would win because it's unconflicted.
773        // In V2.1, B should win because it has a higher power level (100 > 50) and it's sorted together with A.
774        let resolved = resolve_lean(unconflicted, conflicted, StateResVersion::V2_1);
775        assert_eq!(
776            resolved.get(&("m.room.member".into(), "@alice:example.com".into())),
777            Some(&"B".into())
778        );
779    }
780
781    #[test]
782    fn test_v1_tie_break_by_id() {
783        let mut events = HashMap::new();
784        events.insert(
785            "B".into(),
786            LeanEvent {
787                event_id: "B".into(),
788                event_type: "m.room.member".into(),
789                state_key: "@alice:example.com".into(),
790                power_level: 0,
791                origin_server_ts: 100,
792                prev_events: vec![],
793                auth_events: vec![],
794                depth: 1,
795                ..Default::default()
796            },
797        );
798        events.insert(
799            "A".into(),
800            LeanEvent {
801                event_id: "A".into(),
802                event_type: "m.room.member".into(),
803                state_key: "@alice:example.com".into(),
804                power_level: 0,
805                origin_server_ts: 100,
806                prev_events: vec![],
807                auth_events: vec![],
808                depth: 1,
809                ..Default::default()
810            },
811        );
812        let sorted = lean_kahn_sort(&events, StateResVersion::V1);
813        assert_eq!(sorted, vec!["A", "B"]);
814    }
815
816    #[test]
817    fn test_v2_resolution_happy_path() {
818        let mut events = HashMap::new();
819        events.insert(
820            "A".into(),
821            LeanEvent {
822                event_id: "A".into(),
823                event_type: "m.room.member".into(),
824                state_key: "@alice:example.com".into(),
825                power_level: 100,
826                origin_server_ts: 100,
827                prev_events: vec![],
828                auth_events: vec![],
829                depth: 10,
830                ..Default::default()
831            },
832        );
833        events.insert(
834            "B".into(),
835            LeanEvent {
836                event_id: "B".into(),
837                event_type: "m.room.member".into(),
838                state_key: "@alice:example.com".into(),
839                power_level: 50,
840                origin_server_ts: 10,
841                prev_events: vec![],
842                auth_events: vec![],
843                depth: 1,
844                ..Default::default()
845            },
846        );
847        let sorted = lean_kahn_sort(&events, StateResVersion::V2);
848        // Best (A) comes LAST.
849        assert_eq!(sorted, vec!["B", "A"]);
850    }
851
852    #[test]
853    fn test_v2_deep_tie_break() {
854        let mut events = HashMap::new();
855        events.insert(
856            "B".into(),
857            LeanEvent {
858                event_id: "B".into(),
859                event_type: "m.room.member".into(),
860                state_key: "@alice:example.com".into(),
861                power_level: 100,
862                origin_server_ts: 10,
863                prev_events: vec![],
864                auth_events: vec![],
865                depth: 1,
866                ..Default::default()
867            },
868        );
869        events.insert(
870            "A".into(),
871            LeanEvent {
872                event_id: "A".into(),
873                event_type: "m.room.member".into(),
874                state_key: "@alice:example.com".into(),
875                power_level: 100,
876                origin_server_ts: 10,
877                prev_events: vec![],
878                auth_events: vec![],
879                depth: 1,
880                ..Default::default()
881            },
882        );
883        let sorted = lean_kahn_sort(&events, StateResVersion::V2);
884        // Best (A, smaller ID) comes LAST.
885        assert_eq!(sorted, vec!["B", "A"]);
886    }
887
888    #[test]
889    fn test_v1_v2_v2_1_comparison_determinism() {
890        let mut events = HashMap::new();
891        events.insert(
892            "A".into(),
893            LeanEvent {
894                event_id: "A".into(),
895                event_type: "m.room.member".into(),
896                state_key: "@alice:example.com".into(),
897                power_level: 10,
898                origin_server_ts: 10,
899                prev_events: vec![],
900                auth_events: vec![],
901                depth: 1,
902                ..Default::default()
903            },
904        );
905        events.insert(
906            "B".into(),
907            LeanEvent {
908                event_id: "B".into(),
909                event_type: "m.room.member".into(),
910                state_key: "@alice:example.com".into(),
911                power_level: 100,
912                origin_server_ts: 100,
913                prev_events: vec![],
914                auth_events: vec![],
915                depth: 10,
916                ..Default::default()
917            },
918        );
919        let sorted_v1 = lean_kahn_sort(&events, StateResVersion::V1);
920        let sorted_v2 = lean_kahn_sort(&events, StateResVersion::V2);
921        let sorted_v2_1 = lean_kahn_sort(&events, StateResVersion::V2_1);
922        assert_eq!(sorted_v1, vec!["A", "B"]);
923        // B is better (higher power level), so it comes LAST in V2 and V2.1
924        assert_eq!(sorted_v2, vec!["A", "B"]);
925        assert_eq!(sorted_v2_1, vec!["A", "B"]);
926    }
927
928    #[test]
929    fn test_unhappy_path_cycle_detection() {
930        let mut events = HashMap::new();
931        events.insert(
932            "A".into(),
933            LeanEvent {
934                event_id: "A".into(),
935                event_type: "m.room.member".into(),
936                state_key: "@alice:example.com".into(),
937                power_level: 100,
938                origin_server_ts: 100,
939                prev_events: vec!["B".into()],
940                auth_events: vec!["B".into()],
941                depth: 1,
942                ..Default::default()
943            },
944        );
945        events.insert(
946            "B".into(),
947            LeanEvent {
948                event_id: "B".into(),
949                event_type: "m.room.member".into(),
950                state_key: "@alice:example.com".into(),
951                power_level: 100,
952                origin_server_ts: 100,
953                prev_events: vec!["A".into()],
954                auth_events: vec!["A".into()],
955                depth: 1,
956                ..Default::default()
957            },
958        );
959        let sorted = lean_kahn_sort(&events, StateResVersion::V2);
960        assert!(sorted.is_empty());
961    }
962
963    #[test]
964    #[cfg(all(feature = "std", not(feature = "zkvm")))]
965    #[should_panic(expected = "Signature verification failed")]
966    fn test_signature_verification_failure() {
967        let pk = [
968            215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114,
969            243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26,
970        ];
971        let sig = [0u8; 64];
972        let msg = b"test";
973        verify_signature(&pk, msg, &sig);
974    }
975
976    #[test]
977    fn test_serialization_roundtrip() {
978        let event = LeanEvent {
979            event_id: "$abc".into(),
980            event_type: "m.room.member".into(),
981            state_key: "@alice:example.com".into(),
982            power_level: 100,
983            origin_server_ts: 12345,
984            prev_events: vec![],
985            auth_events: vec![],
986            depth: 5,
987            ..Default::default()
988        };
989        let serialized = serde_json::to_string(&event).unwrap();
990        let deserialized: LeanEvent = serde_json::from_str(&serialized).unwrap();
991        assert_eq!(event, deserialized);
992    }
993
994    #[test]
995    fn test_partial_ord_implementations() {
996        let e1 = LeanEvent {
997            event_id: "a".into(),
998            event_type: "m.room.member".into(),
999            state_key: "@alice:example.com".into(),
1000            power_level: 100,
1001            origin_server_ts: 10,
1002            prev_events: vec![],
1003            auth_events: vec![],
1004            depth: 1,
1005            ..Default::default()
1006        };
1007        let e2 = LeanEvent {
1008            event_id: "b".into(),
1009            event_type: "m.room.member".into(),
1010            state_key: "@alice:example.com".into(),
1011            power_level: 100,
1012            origin_server_ts: 10,
1013            prev_events: vec![],
1014            auth_events: vec![],
1015            depth: 1,
1016            ..Default::default()
1017        };
1018        assert!(e1.partial_cmp(&e2).is_some());
1019
1020        let p1 = SortPriority {
1021            event: &e1,
1022            version: StateResVersion::V2,
1023        };
1024        let p2 = SortPriority {
1025            event: &e2,
1026            version: StateResVersion::V2,
1027        };
1028        assert!(p1.partial_cmp(&p2).is_some());
1029    }
1030
1031    #[test]
1032    fn test_trait_coverage() {
1033        let v = StateResVersion::V2;
1034        assert_eq!(v, StateResVersion::V2);
1035        let _ = alloc::format!("{:?}", v);
1036
1037        let e = LeanEvent {
1038            event_id: "a".into(),
1039            event_type: "m.room.member".into(),
1040            state_key: "@alice:example.com".into(),
1041            power_level: 100,
1042            origin_server_ts: 10,
1043            prev_events: vec![],
1044            auth_events: vec![],
1045            depth: 1,
1046            ..Default::default()
1047        };
1048        let _ = e.clone();
1049        let _ = alloc::format!("{:?}", e);
1050    }
1051
1052    #[test]
1053    fn test_complex_dag_sort() {
1054        let mut events = HashMap::new();
1055        events.insert(
1056            "1".into(),
1057            LeanEvent {
1058                event_id: "1".into(),
1059                event_type: "m.room.member".into(),
1060                state_key: "@alice:example.com".into(),
1061                power_level: 100,
1062                origin_server_ts: 10,
1063                prev_events: vec![],
1064                auth_events: vec![],
1065                depth: 1,
1066                ..Default::default()
1067            },
1068        );
1069        events.insert(
1070            "2".into(),
1071            LeanEvent {
1072                event_id: "2".into(),
1073                event_type: "m.room.member".into(),
1074                state_key: "@alice:example.com".into(),
1075                power_level: 50,
1076                origin_server_ts: 20,
1077                prev_events: vec!["1".into()],
1078                auth_events: vec!["1".into()],
1079                depth: 2,
1080                ..Default::default()
1081            },
1082        );
1083        events.insert(
1084            "3".into(),
1085            LeanEvent {
1086                event_id: "3".into(),
1087                event_type: "m.room.member".into(),
1088                state_key: "@alice:example.com".into(),
1089                power_level: 50,
1090                origin_server_ts: 15,
1091                prev_events: vec!["1".into()],
1092                auth_events: vec!["1".into()],
1093                depth: 2,
1094                ..Default::default()
1095            },
1096        );
1097        events.insert(
1098            "4".into(),
1099            LeanEvent {
1100                event_id: "4".into(),
1101                event_type: "m.room.member".into(),
1102                state_key: "@alice:example.com".into(),
1103                power_level: 10,
1104                origin_server_ts: 30,
1105                prev_events: vec!["2".into(), "3".into()],
1106                auth_events: vec!["2".into(), "3".into()],
1107                depth: 3,
1108                ..Default::default()
1109            },
1110        );
1111        let sorted = lean_kahn_sort(&events, StateResVersion::V2);
1112        // 1 pops first (only one with in-degree 0).
1113        // Then 2 and 3 are in queue. 3 has earlier TS (15, worse) so it pops first.
1114        // Then 2 (TS 20, better) pops.
1115        // Then 4 pops.
1116        assert_eq!(sorted, vec!["1", "3", "2", "4"]);
1117    }
1118
1119    #[test]
1120    fn test_kahn_missing_parents() {
1121        let mut events = HashMap::new();
1122        events.insert(
1123            "A".into(),
1124            LeanEvent {
1125                event_id: "A".into(),
1126                event_type: "m.room.member".into(),
1127                state_key: "@alice:example.com".into(),
1128                power_level: 100,
1129                origin_server_ts: 10,
1130                prev_events: vec!["MISSING".into()],
1131                auth_events: vec!["MISSING".into()],
1132                depth: 1,
1133                ..Default::default()
1134            },
1135        );
1136        let sorted = lean_kahn_sort(&events, StateResVersion::V2);
1137        assert_eq!(sorted, vec!["A"]);
1138    }
1139
1140    #[test]
1141    fn test_resolve_lean_functionality() {
1142        let mut unconflicted = BTreeMap::new();
1143        unconflicted.insert(("type".into(), "key".into()), "id".into());
1144        let conflicted = HashMap::new();
1145        let resolved = resolve_lean(unconflicted.clone(), conflicted, StateResVersion::V2);
1146        assert_eq!(resolved, unconflicted);
1147    }
1148
1149    #[test]
1150    fn test_resolve_lean_v2_1_overlay() {
1151        use serde_json::json;
1152
1153        let mut unconflicted = BTreeMap::new();
1154        unconflicted.insert(
1155            ("m.room.member".into(), "@alice:example.com".into()),
1156            "id1".into(),
1157        );
1158        unconflicted.insert(
1159            ("m.room.member".into(), "@bob:example.com".into()),
1160            "id2".into(),
1161        );
1162
1163        let mut conflicted = HashMap::new();
1164        // m.room.create to seed auth state
1165        conflicted.insert(
1166            "create".into(),
1167            LeanEvent {
1168                event_id: "create".into(),
1169                event_type: "m.room.create".into(),
1170                state_key: String::new(),
1171                sender: "@alice:example.com".into(),
1172                power_level: 100,
1173                origin_server_ts: 1,
1174                content: json!({}),
1175                ..Default::default()
1176            },
1177        );
1178        // Provide objects for all events to be sorted in V2.1
1179        conflicted.insert(
1180            "id1".into(),
1181            LeanEvent {
1182                event_id: "id1".into(),
1183                event_type: "m.room.member".into(),
1184                state_key: "@alice:example.com".into(),
1185                sender: "@alice:example.com".into(),
1186                power_level: 50,
1187                origin_server_ts: 500,
1188                content: json!({"membership": "join"}),
1189                auth_events: vec!["create".into()],
1190                ..Default::default()
1191            },
1192        );
1193        conflicted.insert(
1194            "id2".into(),
1195            LeanEvent {
1196                event_id: "id2".into(),
1197                event_type: "m.room.member".into(),
1198                state_key: "@bob:example.com".into(),
1199                sender: "@bob:example.com".into(),
1200                power_level: 50,
1201                origin_server_ts: 500,
1202                content: json!({"membership": "join"}),
1203                auth_events: vec!["create".into()],
1204                ..Default::default()
1205            },
1206        );
1207        conflicted.insert(
1208            "id2_new".into(),
1209            LeanEvent {
1210                event_id: "id2_new".into(),
1211                event_type: "m.room.member".into(),
1212                state_key: "@bob:example.com".into(),
1213                sender: "@bob:example.com".into(),
1214                power_level: 100,
1215                origin_server_ts: 1000,
1216                content: json!({"membership": "join"}),
1217                auth_events: vec!["create".into()],
1218                ..Default::default()
1219            },
1220        );
1221
1222        let resolved = resolve_lean(unconflicted.clone(), conflicted, StateResVersion::V2_1);
1223
1224        assert_eq!(
1225            resolved.get(&("m.room.member".into(), "@alice:example.com".into())),
1226            Some(&"id1".into())
1227        );
1228        assert_eq!(
1229            resolved.get(&("m.room.member".into(), "@bob:example.com".into())),
1230            Some(&"id2_new".into())
1231        );
1232    }
1233
1234    fn run_batch_test(
1235        version: StateResVersion,
1236        rows: &[(&str, i64, u64, u64, &[&str])],
1237        expected: &[&str],
1238    ) {
1239        let mut events = HashMap::new();
1240        for r in rows {
1241            events.insert(
1242                r.0.to_string(),
1243                LeanEvent {
1244                    event_id: r.0.to_string(),
1245                    event_type: "m.room.member".into(),
1246                    state_key: "@alice:example.com".into(),
1247                    power_level: r.1,
1248                    origin_server_ts: r.2,
1249                    depth: r.3,
1250                    prev_events: r.4.iter().map(|s| s.to_string()).collect(),
1251                    auth_events: r.4.iter().map(|s| s.to_string()).collect(),
1252                    ..Default::default()
1253                },
1254            );
1255        }
1256        let result = lean_kahn_sort(&events, version);
1257        assert_eq!(
1258            result,
1259            expected.iter().map(|s| s.to_string()).collect::<Vec<_>>()
1260        );
1261    }
1262
1263    #[test]
1264    fn test_resolution_batch() {
1265        run_batch_test(
1266            StateResVersion::V2,
1267            &[("Alice", 100, 500, 1, &[]), ("Bob", 50, 100, 1, &[])],
1268            &["Bob", "Alice"], // Bob is worse (PL 50), pops first.
1269        );
1270        run_batch_test(
1271            StateResVersion::V1,
1272            &[("Deep", 100, 100, 10, &[]), ("Shallow", 10, 100, 1, &[])],
1273            &["Shallow", "Deep"],
1274        );
1275    }
1276
1277    #[test]
1278    fn test_native_resolution_bootstrap_parity() {
1279        let mut events = HashMap::new();
1280        events.insert(
1281            "1".into(),
1282            LeanEvent {
1283                event_id: "1".into(),
1284                event_type: "m.room.member".into(),
1285                state_key: "@user:example.com".into(),
1286                power_level: 100,
1287                origin_server_ts: 10,
1288                prev_events: vec![],
1289                auth_events: vec![],
1290                depth: 1,
1291                ..Default::default()
1292            },
1293        );
1294        events.insert(
1295            "2".into(),
1296            LeanEvent {
1297                event_id: "2".into(),
1298                event_type: "m.room.member".into(),
1299                state_key: "@user:example.com".into(),
1300                power_level: 0,
1301                origin_server_ts: 20,
1302                prev_events: vec!["1".into()],
1303                auth_events: vec!["1".into()],
1304                depth: 2,
1305                ..Default::default()
1306            },
1307        );
1308        let sorted = lean_kahn_sort(&events, StateResVersion::V2);
1309        let mut resolved_state = BTreeMap::new();
1310        for id in sorted {
1311            let ev = events.get(&id).unwrap();
1312            let key = (ev.event_type.clone(), ev.state_key.clone());
1313            resolved_state.insert(key, ev.event_id.clone());
1314        }
1315        assert_eq!(
1316            resolved_state.get(&("m.room.member".to_string(), "@user:example.com".to_string())),
1317            Some(&"2".to_string())
1318        );
1319    }
1320
1321    #[test]
1322    fn test_enum_coverage() {
1323        let v = StateResVersion::V2;
1324        let v2 = v;
1325        assert_eq!(v, v2);
1326        let debug_str = alloc::format!("{:?}", v);
1327        assert!(debug_str.contains("V2"));
1328    }
1329
1330    #[test]
1331    fn test_event_traits_coverage() {
1332        let e = LeanEvent {
1333            event_id: "a".into(),
1334            event_type: "m.room.member".into(),
1335            state_key: "@alice:example.com".into(),
1336            power_level: 100,
1337            origin_server_ts: 10,
1338            prev_events: vec![],
1339            auth_events: vec![],
1340            depth: 1,
1341            ..Default::default()
1342        };
1343        let e2 = e.clone();
1344        assert_eq!(e, e2);
1345        let debug_str = alloc::format!("{:?}", e);
1346        assert!(debug_str.contains("event_id"));
1347    }
1348
1349    #[test]
1350    fn test_sort_priority_traits() {
1351        let e = LeanEvent {
1352            event_id: "a".into(),
1353            event_type: "m.room.member".into(),
1354            state_key: "@alice:example.com".into(),
1355            power_level: 100,
1356            origin_server_ts: 10,
1357            prev_events: vec![],
1358            auth_events: vec![],
1359            depth: 1,
1360            ..Default::default()
1361        };
1362        let p = SortPriority {
1363            event: &e,
1364            version: StateResVersion::V2,
1365        };
1366        let p2 = p;
1367        assert_eq!(p, p2);
1368        let debug_str = alloc::format!("{:?}", p);
1369        assert!(debug_str.contains("version"));
1370    }
1371
1372    #[test]
1373    fn test_v1_equal_depth_tie_break() {
1374        let mut events = HashMap::new();
1375        events.insert(
1376            "B".into(),
1377            LeanEvent {
1378                event_id: "B".into(),
1379                event_type: "m.room.member".into(),
1380                state_key: "@alice:example.com".into(),
1381                power_level: 0,
1382                origin_server_ts: 10,
1383                prev_events: vec![],
1384                auth_events: vec![],
1385                depth: 1,
1386                ..Default::default()
1387            },
1388        );
1389        events.insert(
1390            "A".into(),
1391            LeanEvent {
1392                event_id: "A".into(),
1393                event_type: "m.room.member".into(),
1394                state_key: "@alice:example.com".into(),
1395                power_level: 0,
1396                origin_server_ts: 10,
1397                prev_events: vec![],
1398                auth_events: vec![],
1399                depth: 1,
1400                ..Default::default()
1401            },
1402        );
1403        let sorted = lean_kahn_sort(&events, StateResVersion::V1);
1404        assert_eq!(sorted, vec!["A", "B"]);
1405    }
1406
1407    #[test]
1408    fn test_kahn_no_neighbors() {
1409        let mut events = HashMap::new();
1410        events.insert(
1411            "1".into(),
1412            LeanEvent {
1413                event_id: "1".into(),
1414                event_type: "m.room.member".into(),
1415                state_key: "@alice:example.com".into(),
1416                power_level: 100,
1417                origin_server_ts: 10,
1418                prev_events: vec![],
1419                auth_events: vec![],
1420                depth: 1,
1421                ..Default::default()
1422            },
1423        );
1424        let sorted = lean_kahn_sort(&events, StateResVersion::V2);
1425        assert_eq!(sorted, vec!["1"]);
1426    }
1427
1428    #[test]
1429    fn test_v2_1_full_coverage() {
1430        let mut events = HashMap::new();
1431        events.insert(
1432            "A".into(),
1433            LeanEvent {
1434                event_id: "A".into(),
1435                event_type: "m.room.member".into(),
1436                state_key: "@alice:example.com".into(),
1437                power_level: 100,
1438                origin_server_ts: 10,
1439                prev_events: vec![],
1440                auth_events: vec![],
1441                depth: 1,
1442                ..Default::default()
1443            },
1444        );
1445        let sorted = lean_kahn_sort(&events, StateResVersion::V2_1);
1446        assert_eq!(sorted, vec!["A"]);
1447    }
1448
1449    #[test]
1450    fn test_total_order_properties() {
1451        let e1 = LeanEvent {
1452            event_id: "a".into(),
1453            event_type: "m.room.member".into(),
1454            state_key: "@alice:example.com".into(),
1455            power_level: 100,
1456            origin_server_ts: 10,
1457            prev_events: vec![],
1458            auth_events: vec![],
1459            depth: 1,
1460            ..Default::default()
1461        };
1462        let e2 = LeanEvent {
1463            event_id: "b".into(),
1464            event_type: "m.room.member".into(),
1465            state_key: "@alice:example.com".into(),
1466            power_level: 100,
1467            origin_server_ts: 10,
1468            prev_events: vec![],
1469            auth_events: vec![],
1470            depth: 1,
1471            ..Default::default()
1472        };
1473        let e3 = LeanEvent {
1474            event_id: "c".into(),
1475            event_type: "m.room.member".into(),
1476            state_key: "@alice:example.com".into(),
1477            power_level: 50,
1478            origin_server_ts: 10,
1479            prev_events: vec![],
1480            auth_events: vec![],
1481            depth: 1,
1482            ..Default::default()
1483        };
1484        assert_eq!(e1.cmp(&e1), Ordering::Equal);
1485        assert!(e1 <= e1);
1486        assert!(e1 <= e2 || e2 <= e1);
1487        if e1 <= e2 && e2 <= e3 {
1488            assert!(e1 <= e3);
1489        }
1490        let e1_copy = e1.clone();
1491        if e1 <= e1_copy && e1_copy <= e1 {
1492            assert_eq!(e1, e1_copy);
1493        }
1494    }
1495
1496    #[test]
1497    fn test_coverage_booster_all_branches() {
1498        let e_base = LeanEvent {
1499            event_id: "m".into(),
1500            event_type: "m.room.member".into(),
1501            state_key: "@alice:example.com".into(),
1502            power_level: 50,
1503            origin_server_ts: 50,
1504            prev_events: vec![],
1505            auth_events: vec![],
1506            depth: 50,
1507            ..Default::default()
1508        };
1509        let p_base = SortPriority {
1510            event: &e_base,
1511            version: StateResVersion::V2,
1512        };
1513        let e_high_power = LeanEvent {
1514            power_level: 100,
1515            ..e_base.clone()
1516        };
1517        let p_high_power = SortPriority {
1518            event: &e_high_power,
1519            version: StateResVersion::V2,
1520        };
1521        // p_base is WORSE (PL 50 < 100), so it should be GREATER.
1522        assert_eq!(p_base.cmp(&p_high_power), Ordering::Greater);
1523        let e_early_ts = LeanEvent {
1524            origin_server_ts: 10,
1525            ..e_base.clone()
1526        };
1527        let p_early_ts = SortPriority {
1528            event: &e_early_ts,
1529            version: StateResVersion::V2,
1530        };
1531        // p_early_ts has TS 10 (worse, pops first = Greater), p_base has TS 50 (better, pops last = Less).
1532        assert_eq!(p_base.cmp(&p_early_ts), Ordering::Less);
1533        let e_early_id = LeanEvent {
1534            event_id: "a".into(),
1535            ..e_base.clone()
1536        };
1537        let p_early_id = SortPriority {
1538            event: &e_early_id,
1539            version: StateResVersion::V2,
1540        };
1541        // p_early_id has ID "a", p_base has ID "m". Larger ID pops first, so p_base is GREATER.
1542        assert_eq!(p_base.cmp(&p_early_id), Ordering::Greater);
1543        let p_v1_base = SortPriority {
1544            event: &e_base,
1545            version: StateResVersion::V1,
1546        };
1547        let e_shallow = LeanEvent {
1548            depth: 1,
1549            ..e_base.clone()
1550        };
1551        let p_shallow = SortPriority {
1552            event: &e_shallow,
1553            version: StateResVersion::V1,
1554        };
1555        assert_eq!(p_v1_base.cmp(&p_shallow), Ordering::Less);
1556        let p_v1_early_id = SortPriority {
1557            event: &e_early_id,
1558            version: StateResVersion::V1,
1559        };
1560        assert_eq!(p_v1_base.cmp(&p_v1_early_id), Ordering::Less);
1561        assert_eq!(p_v1_base.cmp(&p_v1_base), Ordering::Equal);
1562    }
1563
1564    // ========================================================================
1565    // Phase 2: Battle-Hardening Tests
1566    // ========================================================================
1567
1568    #[test]
1569    fn test_cycle_detection_detailed() {
1570        let mut events = HashMap::new();
1571        events.insert(
1572            "A".into(),
1573            LeanEvent {
1574                event_id: "A".into(),
1575                event_type: "m.room.member".into(),
1576                state_key: "@alice:example.com".into(),
1577                auth_events: vec!["B".into()],
1578                ..Default::default()
1579            },
1580        );
1581        events.insert(
1582            "B".into(),
1583            LeanEvent {
1584                event_id: "B".into(),
1585                event_type: "m.room.member".into(),
1586                state_key: "@alice:example.com".into(),
1587                auth_events: vec!["A".into()],
1588                ..Default::default()
1589            },
1590        );
1591        let result = lean_kahn_sort_detailed(&events, StateResVersion::V2);
1592        match result {
1593            KahnSortResult::CycleDetected { sorted, stuck } => {
1594                assert!(sorted.is_empty());
1595                assert_eq!(stuck.len(), 2);
1596                let mut stuck_sorted = stuck.clone();
1597                stuck_sorted.sort();
1598                assert_eq!(stuck_sorted, vec!["A", "B"]);
1599            }
1600            KahnSortResult::Ok(_) => panic!("Expected cycle detection"),
1601        }
1602    }
1603
1604    #[test]
1605    fn test_cycle_detection_partial_sort() {
1606        // C -> A -> B -> A (cycle), but C is reachable
1607        let mut events = HashMap::new();
1608        events.insert(
1609            "C".into(),
1610            LeanEvent {
1611                event_id: "C".into(),
1612                event_type: "m.room.member".into(),
1613                state_key: "@alice:example.com".into(),
1614                auth_events: vec![],
1615                ..Default::default()
1616            },
1617        );
1618        events.insert(
1619            "A".into(),
1620            LeanEvent {
1621                event_id: "A".into(),
1622                event_type: "m.room.member".into(),
1623                state_key: "@alice:example.com".into(),
1624                auth_events: vec!["B".into(), "C".into()],
1625                ..Default::default()
1626            },
1627        );
1628        events.insert(
1629            "B".into(),
1630            LeanEvent {
1631                event_id: "B".into(),
1632                event_type: "m.room.member".into(),
1633                state_key: "@alice:example.com".into(),
1634                auth_events: vec!["A".into()],
1635                ..Default::default()
1636            },
1637        );
1638        let result = lean_kahn_sort_detailed(&events, StateResVersion::V2);
1639        match result {
1640            KahnSortResult::CycleDetected { sorted, stuck } => {
1641                assert_eq!(sorted, vec!["C"]);
1642                assert_eq!(stuck.len(), 2);
1643            }
1644            KahnSortResult::Ok(_) => panic!("Expected cycle detection"),
1645        }
1646    }
1647
1648    #[test]
1649    fn test_kahn_sort_result_api() {
1650        let ok = KahnSortResult::Ok(vec!["A".into()]);
1651        assert!(ok.is_ok());
1652        assert_eq!(ok.into_sorted(), vec!["A"]);
1653
1654        let cycle = KahnSortResult::CycleDetected {
1655            sorted: vec!["C".into()],
1656            stuck: vec!["A".into(), "B".into()],
1657        };
1658        assert!(!cycle.is_ok());
1659        assert!(cycle.into_sorted().is_empty());
1660    }
1661
1662    #[test]
1663    fn test_power_level_coercion_integer() {
1664        let json = r#"{"event_id": "$1", "type": "m.room.member", "origin_server_ts": 1, "power_level": 100}"#;
1665        let ev: LeanEvent = serde_json::from_str(json).unwrap();
1666        assert_eq!(ev.power_level, 100);
1667    }
1668
1669    #[test]
1670    fn test_power_level_coercion_string() {
1671        let json = r#"{"event_id": "$1", "type": "m.room.member", "origin_server_ts": 1, "power_level": "100"}"#;
1672        let ev: LeanEvent = serde_json::from_str(json).unwrap();
1673        assert_eq!(ev.power_level, 100);
1674    }
1675
1676    #[test]
1677    fn test_power_level_coercion_float() {
1678        let json = r#"{"event_id": "$1", "type": "m.room.member", "origin_server_ts": 1, "power_level": 100.0}"#;
1679        let ev: LeanEvent = serde_json::from_str(json).unwrap();
1680        assert_eq!(ev.power_level, 100);
1681    }
1682
1683    #[test]
1684    fn test_power_level_coercion_invalid_string() {
1685        let json = r#"{"event_id": "$1", "type": "m.room.member", "origin_server_ts": 1, "power_level": "abc"}"#;
1686        let ev: LeanEvent = serde_json::from_str(json).unwrap();
1687        assert_eq!(ev.power_level, 0);
1688    }
1689
1690    #[test]
1691    fn test_deep_chain_stack_safety() {
1692        // 1000-event deep chain: ev_0 <- ev_1 <- ev_2 <- ... <- ev_999
1693        let mut events = HashMap::new();
1694        for i in 0..1000u32 {
1695            let id = alloc::format!("ev_{}", i);
1696            let auth = if i > 0 {
1697                vec![alloc::format!("ev_{}", i - 1)]
1698            } else {
1699                vec![]
1700            };
1701            events.insert(
1702                id.clone(),
1703                LeanEvent {
1704                    event_id: id,
1705                    event_type: "m.room.member".into(),
1706                    state_key: "@alice:example.com".into(),
1707                    power_level: 100,
1708                    origin_server_ts: i as u64,
1709                    auth_events: auth,
1710                    depth: i as u64,
1711                    ..Default::default()
1712                },
1713            );
1714        }
1715        let sorted = lean_kahn_sort(&events, StateResVersion::V2);
1716        assert_eq!(sorted.len(), 1000);
1717        // First element must be ev_0 (in-degree 0)
1718        assert_eq!(sorted[0], "ev_0");
1719        // Last element must be ev_999 (deepest)
1720        assert_eq!(sorted[999], "ev_999");
1721    }
1722
1723    #[test]
1724    fn test_subgraph_bounded_depth() {
1725        // Chain: A <- B <- C <- D (all in conflicted set for proper subgraph)
1726        let mut graph = HashMap::new();
1727        for (id, auths) in [
1728            ("A", vec![]),
1729            ("B", vec!["A"]),
1730            ("C", vec!["B"]),
1731            ("D", vec!["C"]),
1732        ] {
1733            graph.insert(
1734                id.to_string(),
1735                LeanEvent {
1736                    event_id: id.into(),
1737                    event_type: "m.room.member".into(),
1738                    state_key: "@alice:example.com".into(),
1739                    auth_events: auths.iter().map(|s| s.to_string()).collect(),
1740                    ..Default::default()
1741                },
1742            );
1743        }
1744        // Unbounded with A and D as conflicted: full intersection includes all
1745        let full = compute_v2_1_conflicted_subgraph_bounded(
1746            &graph,
1747            &["A".to_string(), "D".to_string()],
1748            None,
1749        );
1750        assert!(full.subgraph.contains_key("A"));
1751        assert!(full.subgraph.contains_key("D"));
1752
1753        // Bounded to depth 1: backwards from D only reaches C (depth 1),
1754        // so the backwards set is {A, D, C} (A + D from seeds, C from D's auth).
1755        // But A is not reachable forward from any of these at depth 1 only.
1756        let bounded = compute_v2_1_conflicted_subgraph_bounded(
1757            &graph,
1758            &["A".to_string(), "D".to_string()],
1759            Some(1),
1760        );
1761        // D at depth 0, C at depth 1 from D's backwards walk
1762        assert!(bounded.subgraph.contains_key("D"));
1763        assert!(bounded.subgraph.contains_key("A"));
1764        // B is NOT reachable within depth 1 from D (it's at depth 2)
1765        assert!(!bounded.subgraph.contains_key("B"));
1766    }
1767
1768    #[test]
1769    fn test_subgraph_missing_auth_detection() {
1770        let mut graph = HashMap::new();
1771        graph.insert(
1772            "X".to_string(),
1773            LeanEvent {
1774                event_id: "X".into(),
1775                event_type: "m.room.member".into(),
1776                state_key: "@alice:example.com".into(),
1777                auth_events: vec!["MISSING_1".into(), "MISSING_2".into()],
1778                ..Default::default()
1779            },
1780        );
1781        let result = compute_v2_1_conflicted_subgraph_bounded(&graph, &["X".to_string()], None);
1782        let mut missing = result.missing_auth_events.clone();
1783        missing.sort();
1784        assert_eq!(missing, vec!["MISSING_1", "MISSING_2"]);
1785    }
1786}