Skip to main content

ipfrs_tensorlogic/
rule_versioning.rs

1//! Rule set versioning and conflict resolution for distributed knowledge federation.
2//!
3//! When multiple IPFRS nodes merge rule sets, conflicts must be detected and resolved
4//! deterministically. This module provides the infrastructure for versioning rule sets,
5//! computing diffs, and resolving conflicts according to configurable strategies.
6
7use std::collections::HashSet;
8use std::fmt;
9
10// ---------------------------------------------------------------------------
11// FNV-1a hash (pure Rust, no external crate needed)
12// ---------------------------------------------------------------------------
13
14/// Compute a 64-bit FNV-1a hash over an arbitrary byte slice.
15fn fnv1a_64(data: &[u8]) -> u64 {
16    const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
17    const FNV_PRIME: u64 = 1_099_511_628_211;
18
19    let mut hash = FNV_OFFSET_BASIS;
20    for &byte in data {
21        hash ^= u64::from(byte);
22        hash = hash.wrapping_mul(FNV_PRIME);
23    }
24    hash
25}
26
27/// Compute the deterministic fingerprint of a rule-set.
28///
29/// Rules are sorted lexicographically before hashing so that
30/// two rule-sets with the same contents (regardless of insertion order)
31/// always produce the same fingerprint.
32fn fingerprint_rules(rules: &[String]) -> u64 {
33    let mut sorted: Vec<&str> = rules.iter().map(String::as_str).collect();
34    sorted.sort_unstable();
35
36    // Hash each rule separated by a NUL byte so that "ab" + "c" ≠ "a" + "bc".
37    let mut combined: Vec<u8> = Vec::new();
38    for rule in sorted {
39        combined.extend_from_slice(rule.as_bytes());
40        combined.push(0u8);
41    }
42    fnv1a_64(&combined)
43}
44
45/// Return current Unix time in milliseconds.
46///
47/// Falls back to 0 if the system clock is unavailable (e.g. in no-std / WASM).
48fn unix_millis_now() -> u64 {
49    use std::time::{SystemTime, UNIX_EPOCH};
50    SystemTime::now()
51        .duration_since(UNIX_EPOCH)
52        .map(|d| d.as_millis() as u64)
53        .unwrap_or(0)
54}
55
56// ---------------------------------------------------------------------------
57// RuleSetVersion
58// ---------------------------------------------------------------------------
59
60/// Versioning metadata for a rule set.
61///
62/// The `fingerprint` is an FNV-1a hash of the sorted rule bodies, providing
63/// a fast, deterministic content identity check independent of ordering.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct RuleSetVersion {
66    pub major: u32,
67    pub minor: u32,
68    pub patch: u32,
69    /// FNV-1a hash over all sorted rule bodies.
70    pub fingerprint: u64,
71    /// Unix timestamp in milliseconds when this version was created.
72    pub created_at: u64,
73    /// Peer ID of the node that authored this version.
74    pub author_peer_id: String,
75}
76
77impl RuleSetVersion {
78    /// Create a new `RuleSetVersion`.
79    ///
80    /// `created_at` is set to the current wall-clock time.
81    /// `fingerprint` is computed from `rules` (sorted for determinism).
82    pub fn new(major: u32, minor: u32, patch: u32, rules: &[String], author: &str) -> Self {
83        Self {
84            major,
85            minor,
86            patch,
87            fingerprint: fingerprint_rules(rules),
88            created_at: unix_millis_now(),
89            author_peer_id: author.to_string(),
90        }
91    }
92
93    /// Returns `true` when `self` and `other` share the same major version number,
94    /// indicating that they are expected to be compatible.
95    pub fn is_compatible_with(&self, other: &Self) -> bool {
96        self.major == other.major
97    }
98}
99
100impl fmt::Display for RuleSetVersion {
101    /// Formats as `"major.minor.patch+<fingerprint_hex>"`.
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(
104            f,
105            "{}.{}.{}+{:016x}",
106            self.major, self.minor, self.patch, self.fingerprint
107        )
108    }
109}
110
111// ---------------------------------------------------------------------------
112// RuleSetDiff
113// ---------------------------------------------------------------------------
114
115/// Diff between two rule sets.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct RuleSetDiff {
118    /// Rules present in the new set but absent in the old set.
119    pub added: Vec<String>,
120    /// Rules present in the old set but absent in the new set.
121    pub removed: Vec<String>,
122    /// Number of rules that appear in both sets.
123    pub unchanged: usize,
124}
125
126impl RuleSetDiff {
127    /// Compute the diff between `old_rules` and `new_rules`.
128    ///
129    /// The `added` and `removed` vectors are sorted for determinism.
130    pub fn diff(old_rules: &[String], new_rules: &[String]) -> Self {
131        let old_set: HashSet<&str> = old_rules.iter().map(String::as_str).collect();
132        let new_set: HashSet<&str> = new_rules.iter().map(String::as_str).collect();
133
134        let mut added: Vec<String> = new_set
135            .difference(&old_set)
136            .map(|s| (*s).to_string())
137            .collect();
138        let mut removed: Vec<String> = old_set
139            .difference(&new_set)
140            .map(|s| (*s).to_string())
141            .collect();
142
143        added.sort_unstable();
144        removed.sort_unstable();
145
146        let unchanged = old_set.intersection(&new_set).count();
147
148        Self {
149            added,
150            removed,
151            unchanged,
152        }
153    }
154
155    /// Returns `true` when there are no additions and no removals.
156    pub fn is_empty(&self) -> bool {
157        self.added.is_empty() && self.removed.is_empty()
158    }
159
160    /// Returns a compact summary string in the form `"+A/-R/=U"`.
161    pub fn summary(&self) -> String {
162        format!(
163            "+{}/-{}/={}",
164            self.added.len(),
165            self.removed.len(),
166            self.unchanged
167        )
168    }
169}
170
171impl fmt::Display for RuleSetDiff {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(f, "{}", self.summary())
174    }
175}
176
177// ---------------------------------------------------------------------------
178// ConflictStrategy
179// ---------------------------------------------------------------------------
180
181/// Strategy to use when resolving a conflict between two `VersionedRuleSet`s.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum ConflictStrategy {
184    /// Choose the rule set whose `created_at` timestamp is larger (more recent).
185    LastWriteWins,
186    /// Choose the rule set with the higher semantic version (major → minor → patch).
187    HigherVersionWins,
188    /// Merge all rules from both sets, deduplicated and sorted.
189    Union,
190    /// Keep only rules that appear in both sets.
191    Intersection,
192    /// Named custom strategy — falls back to `Union` semantics at runtime.
193    Custom(String),
194}
195
196impl fmt::Display for ConflictStrategy {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        match self {
199            Self::LastWriteWins => write!(f, "LastWriteWins"),
200            Self::HigherVersionWins => write!(f, "HigherVersionWins"),
201            Self::Union => write!(f, "Union"),
202            Self::Intersection => write!(f, "Intersection"),
203            Self::Custom(name) => write!(f, "Custom({})", name),
204        }
205    }
206}
207
208// ---------------------------------------------------------------------------
209// VersionedRuleSet
210// ---------------------------------------------------------------------------
211
212/// A rule set with associated version metadata.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct VersionedRuleSet {
215    pub version: RuleSetVersion,
216    pub rules: Vec<String>,
217}
218
219impl VersionedRuleSet {
220    /// Create a new `VersionedRuleSet`.
221    pub fn new(version: RuleSetVersion, rules: Vec<String>) -> Self {
222        Self { version, rules }
223    }
224
225    /// Returns the number of rules in this set.
226    pub fn rule_count(&self) -> usize {
227        self.rules.len()
228    }
229}
230
231// ---------------------------------------------------------------------------
232// ResolvedRuleSet
233// ---------------------------------------------------------------------------
234
235/// The result of conflict resolution between two `VersionedRuleSet`s.
236#[derive(Debug, Clone)]
237pub struct ResolvedRuleSet {
238    /// The merged/chosen rules after resolution.
239    pub rules: Vec<String>,
240    /// Human-readable name of the strategy that was applied.
241    pub strategy_used: String,
242    /// `true` when the two source versions had different fingerprints,
243    /// indicating that a genuine conflict existed.
244    pub conflict_detected: bool,
245    /// The two source versions that were resolved.
246    pub source_versions: (RuleSetVersion, RuleSetVersion),
247}
248
249// ---------------------------------------------------------------------------
250// ConflictResolver
251// ---------------------------------------------------------------------------
252
253/// Resolves conflicts between two `VersionedRuleSet`s according to a chosen
254/// [`ConflictStrategy`].
255pub struct ConflictResolver {
256    pub strategy: ConflictStrategy,
257}
258
259impl ConflictResolver {
260    /// Create a new `ConflictResolver` with the given strategy.
261    pub fn new(strategy: ConflictStrategy) -> Self {
262        Self { strategy }
263    }
264
265    /// Resolve a conflict between `local` and `remote`, returning a
266    /// [`ResolvedRuleSet`] that describes the outcome.
267    pub fn resolve(&self, local: &VersionedRuleSet, remote: &VersionedRuleSet) -> ResolvedRuleSet {
268        let conflict_detected = local.version.fingerprint != remote.version.fingerprint;
269        let strategy_used = self.strategy.to_string();
270
271        let rules = match &self.strategy {
272            ConflictStrategy::LastWriteWins => {
273                if remote.version.created_at > local.version.created_at {
274                    remote.rules.clone()
275                } else {
276                    local.rules.clone()
277                }
278            }
279
280            ConflictStrategy::HigherVersionWins => {
281                let local_ver = (
282                    local.version.major,
283                    local.version.minor,
284                    local.version.patch,
285                );
286                let remote_ver = (
287                    remote.version.major,
288                    remote.version.minor,
289                    remote.version.patch,
290                );
291                if remote_ver > local_ver {
292                    remote.rules.clone()
293                } else {
294                    local.rules.clone()
295                }
296            }
297
298            ConflictStrategy::Union | ConflictStrategy::Custom(_) => {
299                // Union semantics: merge all rules, deduplicate, sort.
300                let mut merged: Vec<String> = local
301                    .rules
302                    .iter()
303                    .chain(remote.rules.iter())
304                    .cloned()
305                    .collect::<HashSet<String>>()
306                    .into_iter()
307                    .collect();
308                merged.sort_unstable();
309                merged
310            }
311
312            ConflictStrategy::Intersection => {
313                let local_set: HashSet<&str> = local.rules.iter().map(String::as_str).collect();
314                let remote_set: HashSet<&str> = remote.rules.iter().map(String::as_str).collect();
315                let mut intersection: Vec<String> = local_set
316                    .intersection(&remote_set)
317                    .map(|s| (*s).to_string())
318                    .collect();
319                intersection.sort_unstable();
320                intersection
321            }
322        };
323
324        ResolvedRuleSet {
325            rules,
326            strategy_used,
327            conflict_detected,
328            source_versions: (local.version.clone(), remote.version.clone()),
329        }
330    }
331}
332
333// ---------------------------------------------------------------------------
334// Tests
335// ---------------------------------------------------------------------------
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    // Helper: build a VersionedRuleSet quickly.
342    fn make_vrs(
343        major: u32,
344        minor: u32,
345        patch: u32,
346        rules: &[&str],
347        author: &str,
348        created_at_override: Option<u64>,
349    ) -> VersionedRuleSet {
350        let rule_strings: Vec<String> = rules.iter().map(|s| s.to_string()).collect();
351        let mut ver = RuleSetVersion::new(major, minor, patch, &rule_strings, author);
352        if let Some(ts) = created_at_override {
353            ver.created_at = ts;
354        }
355        VersionedRuleSet::new(ver, rule_strings)
356    }
357
358    // -----------------------------------------------------------------------
359    // RuleSetVersion Display
360    // -----------------------------------------------------------------------
361
362    #[test]
363    fn test_version_display_format() {
364        let rules = vec!["rule_a".to_string(), "rule_b".to_string()];
365        let v = RuleSetVersion::new(1, 2, 3, &rules, "peer-1");
366        let s = v.to_string();
367        // Must start with "1.2.3+"
368        assert!(s.starts_with("1.2.3+"), "got: {s}");
369        // Must contain exactly one '+' followed by 16 hex chars
370        let parts: Vec<&str> = s.splitn(2, '+').collect();
371        assert_eq!(parts.len(), 2);
372        assert_eq!(
373            parts[1].len(),
374            16,
375            "fingerprint hex should be 16 chars: {s}"
376        );
377        assert!(
378            parts[1].chars().all(|c| c.is_ascii_hexdigit()),
379            "fingerprint not hex: {s}"
380        );
381    }
382
383    #[test]
384    fn test_version_display_zero_patch() {
385        let rules: Vec<String> = vec![];
386        let v = RuleSetVersion::new(0, 0, 0, &rules, "peer");
387        assert!(v.to_string().starts_with("0.0.0+"));
388    }
389
390    // -----------------------------------------------------------------------
391    // is_compatible_with
392    // -----------------------------------------------------------------------
393
394    #[test]
395    fn test_compatible_same_major() {
396        let rules: Vec<String> = vec!["r1".to_string()];
397        let v1 = RuleSetVersion::new(2, 0, 0, &rules, "a");
398        let v2 = RuleSetVersion::new(2, 5, 3, &rules, "b");
399        assert!(v1.is_compatible_with(&v2));
400        assert!(v2.is_compatible_with(&v1));
401    }
402
403    #[test]
404    fn test_incompatible_different_major() {
405        let rules: Vec<String> = vec!["r1".to_string()];
406        let v1 = RuleSetVersion::new(1, 9, 9, &rules, "a");
407        let v2 = RuleSetVersion::new(2, 0, 0, &rules, "b");
408        assert!(!v1.is_compatible_with(&v2));
409        assert!(!v2.is_compatible_with(&v1));
410    }
411
412    #[test]
413    fn test_compatible_same_version() {
414        let rules: Vec<String> = vec![];
415        let v = RuleSetVersion::new(3, 1, 4, &rules, "peer");
416        assert!(v.is_compatible_with(&v));
417    }
418
419    // -----------------------------------------------------------------------
420    // Fingerprint determinism
421    // -----------------------------------------------------------------------
422
423    #[test]
424    fn test_fingerprint_order_independent() {
425        let rules_ab = vec!["rule_a".to_string(), "rule_b".to_string()];
426        let rules_ba = vec!["rule_b".to_string(), "rule_a".to_string()];
427        let v1 = RuleSetVersion::new(1, 0, 0, &rules_ab, "peer");
428        let v2 = RuleSetVersion::new(1, 0, 0, &rules_ba, "peer");
429        assert_eq!(v1.fingerprint, v2.fingerprint);
430    }
431
432    // -----------------------------------------------------------------------
433    // RuleSetDiff
434    // -----------------------------------------------------------------------
435
436    #[test]
437    fn test_diff_added_removed_unchanged() {
438        let old = vec!["a".to_string(), "b".to_string(), "c".to_string()];
439        let new = vec!["b".to_string(), "c".to_string(), "d".to_string()];
440        let diff = RuleSetDiff::diff(&old, &new);
441        assert_eq!(diff.added, vec!["d".to_string()]);
442        assert_eq!(diff.removed, vec!["a".to_string()]);
443        assert_eq!(diff.unchanged, 2);
444    }
445
446    #[test]
447    fn test_diff_no_change() {
448        let rules = vec!["x".to_string(), "y".to_string()];
449        let diff = RuleSetDiff::diff(&rules, &rules);
450        assert!(diff.is_empty());
451        assert_eq!(diff.unchanged, 2);
452    }
453
454    #[test]
455    fn test_diff_all_added() {
456        let old: Vec<String> = vec![];
457        let new = vec!["r1".to_string(), "r2".to_string()];
458        let diff = RuleSetDiff::diff(&old, &new);
459        assert_eq!(diff.added.len(), 2);
460        assert!(diff.removed.is_empty());
461        assert_eq!(diff.unchanged, 0);
462    }
463
464    #[test]
465    fn test_diff_all_removed() {
466        let old = vec!["r1".to_string(), "r2".to_string()];
467        let new: Vec<String> = vec![];
468        let diff = RuleSetDiff::diff(&old, &new);
469        assert!(diff.added.is_empty());
470        assert_eq!(diff.removed.len(), 2);
471        assert_eq!(diff.unchanged, 0);
472    }
473
474    #[test]
475    fn test_diff_is_empty_false() {
476        let old = vec!["a".to_string()];
477        let new = vec!["b".to_string()];
478        let diff = RuleSetDiff::diff(&old, &new);
479        assert!(!diff.is_empty());
480    }
481
482    #[test]
483    fn test_diff_summary_format() {
484        let old = vec!["a".to_string(), "b".to_string(), "c".to_string()];
485        let new = vec![
486            "b".to_string(),
487            "c".to_string(),
488            "d".to_string(),
489            "e".to_string(),
490        ];
491        let diff = RuleSetDiff::diff(&old, &new);
492        // +2 added (d, e), -1 removed (a), =2 unchanged (b, c)
493        assert_eq!(diff.summary(), "+2/-1/=2");
494    }
495
496    #[test]
497    fn test_diff_summary_no_change() {
498        let rules = vec!["x".to_string()];
499        let diff = RuleSetDiff::diff(&rules, &rules);
500        assert_eq!(diff.summary(), "+0/-0/=1");
501    }
502
503    // -----------------------------------------------------------------------
504    // ConflictResolver — LastWriteWins
505    // -----------------------------------------------------------------------
506
507    #[test]
508    fn test_last_write_wins_picks_higher_timestamp() {
509        let local = make_vrs(1, 0, 0, &["rule_local"], "local", Some(100));
510        let remote = make_vrs(1, 0, 0, &["rule_remote"], "remote", Some(200));
511        let resolver = ConflictResolver::new(ConflictStrategy::LastWriteWins);
512        let result = resolver.resolve(&local, &remote);
513        assert_eq!(result.rules, vec!["rule_remote".to_string()]);
514        assert_eq!(result.strategy_used, "LastWriteWins");
515    }
516
517    #[test]
518    fn test_last_write_wins_picks_local_when_newer() {
519        let local = make_vrs(1, 0, 0, &["rule_local"], "local", Some(999));
520        let remote = make_vrs(1, 0, 0, &["rule_remote"], "remote", Some(1));
521        let resolver = ConflictResolver::new(ConflictStrategy::LastWriteWins);
522        let result = resolver.resolve(&local, &remote);
523        assert_eq!(result.rules, vec!["rule_local".to_string()]);
524    }
525
526    // -----------------------------------------------------------------------
527    // ConflictResolver — HigherVersionWins
528    // -----------------------------------------------------------------------
529
530    #[test]
531    fn test_higher_version_wins_remote_major() {
532        let local = make_vrs(1, 9, 9, &["local_rule"], "local", None);
533        let remote = make_vrs(2, 0, 0, &["remote_rule"], "remote", None);
534        let resolver = ConflictResolver::new(ConflictStrategy::HigherVersionWins);
535        let result = resolver.resolve(&local, &remote);
536        assert_eq!(result.rules, vec!["remote_rule".to_string()]);
537    }
538
539    #[test]
540    fn test_higher_version_wins_local_minor() {
541        let local = make_vrs(1, 5, 0, &["local_rule"], "local", None);
542        let remote = make_vrs(1, 3, 0, &["remote_rule"], "remote", None);
543        let resolver = ConflictResolver::new(ConflictStrategy::HigherVersionWins);
544        let result = resolver.resolve(&local, &remote);
545        assert_eq!(result.rules, vec!["local_rule".to_string()]);
546    }
547
548    #[test]
549    fn test_higher_version_wins_patch_tiebreak() {
550        let local = make_vrs(2, 1, 3, &["local_rule"], "local", None);
551        let remote = make_vrs(2, 1, 7, &["remote_rule"], "remote", None);
552        let resolver = ConflictResolver::new(ConflictStrategy::HigherVersionWins);
553        let result = resolver.resolve(&local, &remote);
554        assert_eq!(result.rules, vec!["remote_rule".to_string()]);
555    }
556
557    // -----------------------------------------------------------------------
558    // ConflictResolver — Union
559    // -----------------------------------------------------------------------
560
561    #[test]
562    fn test_union_merges_and_deduplicates() {
563        let local = make_vrs(1, 0, 0, &["a", "b", "c"], "local", None);
564        let remote = make_vrs(1, 0, 0, &["b", "c", "d"], "remote", None);
565        let resolver = ConflictResolver::new(ConflictStrategy::Union);
566        let result = resolver.resolve(&local, &remote);
567        let expected = vec!["a", "b", "c", "d"]
568            .into_iter()
569            .map(String::from)
570            .collect::<Vec<_>>();
571        assert_eq!(result.rules, expected);
572    }
573
574    #[test]
575    fn test_union_identical_sets() {
576        let local = make_vrs(1, 0, 0, &["x", "y"], "local", None);
577        let remote = make_vrs(1, 0, 0, &["x", "y"], "remote", None);
578        let resolver = ConflictResolver::new(ConflictStrategy::Union);
579        let result = resolver.resolve(&local, &remote);
580        assert_eq!(result.rules, vec!["x".to_string(), "y".to_string()]);
581    }
582
583    // -----------------------------------------------------------------------
584    // ConflictResolver — Intersection
585    // -----------------------------------------------------------------------
586
587    #[test]
588    fn test_intersection_returns_common_rules() {
589        let local = make_vrs(1, 0, 0, &["a", "b", "c"], "local", None);
590        let remote = make_vrs(1, 0, 0, &["b", "c", "d"], "remote", None);
591        let resolver = ConflictResolver::new(ConflictStrategy::Intersection);
592        let result = resolver.resolve(&local, &remote);
593        let expected = vec!["b".to_string(), "c".to_string()];
594        assert_eq!(result.rules, expected);
595    }
596
597    #[test]
598    fn test_intersection_empty_when_disjoint() {
599        let local = make_vrs(1, 0, 0, &["a", "b"], "local", None);
600        let remote = make_vrs(1, 0, 0, &["c", "d"], "remote", None);
601        let resolver = ConflictResolver::new(ConflictStrategy::Intersection);
602        let result = resolver.resolve(&local, &remote);
603        assert!(result.rules.is_empty());
604    }
605
606    // -----------------------------------------------------------------------
607    // ConflictResolver — Custom (Union fallback)
608    // -----------------------------------------------------------------------
609
610    #[test]
611    fn test_custom_strategy_union_fallback() {
612        let local = make_vrs(1, 0, 0, &["p", "q"], "local", None);
613        let remote = make_vrs(1, 0, 0, &["q", "r"], "remote", None);
614        let resolver = ConflictResolver::new(ConflictStrategy::Custom("my-policy".to_string()));
615        let result = resolver.resolve(&local, &remote);
616        let expected = vec!["p".to_string(), "q".to_string(), "r".to_string()];
617        assert_eq!(result.rules, expected);
618        assert_eq!(result.strategy_used, "Custom(my-policy)");
619    }
620
621    // -----------------------------------------------------------------------
622    // conflict_detected flag
623    // -----------------------------------------------------------------------
624
625    #[test]
626    fn test_conflict_detected_different_fingerprints() {
627        let local = make_vrs(1, 0, 0, &["rule_a"], "local", None);
628        let remote = make_vrs(1, 0, 0, &["rule_b"], "remote", None);
629        // Different rule bodies → different fingerprints.
630        assert_ne!(local.version.fingerprint, remote.version.fingerprint);
631        let resolver = ConflictResolver::new(ConflictStrategy::Union);
632        let result = resolver.resolve(&local, &remote);
633        assert!(result.conflict_detected);
634    }
635
636    #[test]
637    fn test_conflict_not_detected_identical_fingerprints() {
638        let rules = vec!["rule_a".to_string(), "rule_b".to_string()];
639        // Build two versioned rule sets with the same rule bodies.
640        let ver_local = RuleSetVersion::new(1, 0, 0, &rules, "local");
641        let ver_remote = RuleSetVersion::new(1, 0, 1, &rules, "remote");
642        // Same rules ⇒ same fingerprint, no conflict.
643        assert_eq!(ver_local.fingerprint, ver_remote.fingerprint);
644        let local = VersionedRuleSet::new(ver_local, rules.clone());
645        let remote = VersionedRuleSet::new(ver_remote, rules);
646        let resolver = ConflictResolver::new(ConflictStrategy::Union);
647        let result = resolver.resolve(&local, &remote);
648        assert!(!result.conflict_detected);
649    }
650
651    // -----------------------------------------------------------------------
652    // VersionedRuleSet
653    // -----------------------------------------------------------------------
654
655    #[test]
656    fn test_versioned_rule_set_rule_count() {
657        let vrs = make_vrs(1, 0, 0, &["a", "b", "c", "d", "e"], "peer", None);
658        assert_eq!(vrs.rule_count(), 5);
659    }
660
661    // -----------------------------------------------------------------------
662    // Source versions are preserved in ResolvedRuleSet
663    // -----------------------------------------------------------------------
664
665    #[test]
666    fn test_source_versions_preserved() {
667        let local = make_vrs(1, 0, 0, &["l"], "local-peer", None);
668        let remote = make_vrs(2, 0, 0, &["r"], "remote-peer", None);
669        let resolver = ConflictResolver::new(ConflictStrategy::HigherVersionWins);
670        let result = resolver.resolve(&local, &remote);
671        assert_eq!(result.source_versions.0.author_peer_id, "local-peer");
672        assert_eq!(result.source_versions.1.author_peer_id, "remote-peer");
673    }
674}