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