diff --git a/.gitignore b/.gitignore index 318b2e8..d6037bc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ result/ result target/ *.avanterules +.claude/* +.claude diff --git a/offline-web-model/src/lib.rs b/offline-web-model/src/lib.rs index 2fbda39..0178b2f 100644 --- a/offline-web-model/src/lib.rs +++ b/offline-web-model/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{cmp::Ordering, collections::HashMap, sync::Arc}; use blake2::{Blake2b512, Digest}; use rand::Rng; @@ -60,9 +60,17 @@ impl Reference { pub fn add_dep(&self, dep: Arc) -> Self { let mut cloned = self.clone(); cloned.dependents.push(dep); + // We ensure that our dependents are always sorted lexicographically by name. + cloned.dependents.sort_by(|left, right| if left.name == right.name { + Ordering::Equal + } else if left.name < right.name { + Ordering::Less + } else { + Ordering::Greater + }); // Recalculate the ID based on dependents, content_address, and path let mut hasher = Self::initial_hash(&self.content_address, &self.name); - for dependent in &self.dependents { + for dependent in &cloned.dependents { hasher.update(&dependent.id); } cloned.id = format!("{:x}", hasher.finalize()); diff --git a/offline-web-model/src/test.rs b/offline-web-model/src/test.rs index a7e81aa..792251c 100644 --- a/offline-web-model/src/test.rs +++ b/offline-web-model/src/test.rs @@ -2,15 +2,20 @@ use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; -use rand::random_range; use crate::{Graph, Reference, random_object}; -fn get_random_candidate(graph: &Graph) -> Arc { - // Pick a random leaf node to update - let refs: Vec> =graph.refs.values().filter(|r| r.name != graph.root.name).map(|r| r.clone()).collect(); - let random_index = random_range(0..refs.len()); - refs[random_index].clone() +fn get_deterministic_candidate(graph: &Graph) -> Arc { + // Pick a deterministic leaf node to update (first lexicographically) + let mut refs: Vec> = graph.refs.values() + .filter(|r| r.name != graph.root.name && r.is_leaf()) + .map(|r| r.clone()) + .collect(); + + // Sort by name to ensure deterministic ordering + refs.sort_by(|a, b| a.name.cmp(&b.name)); + + refs[0].clone() } /// Tests that all dependencies are kept updated when new nodes are added @@ -22,13 +27,14 @@ fn test_dependencies_updated_when_nodes_added() { // Get the initial content address of the root let initial_root_id = graph.root.id.clone(); - let candidate = get_random_candidate(&graph); + let candidate = get_deterministic_candidate(&graph); - // Update the leaf node - graph.update_object_reference(&candidate.name, random_object().1).unwrap(); + // Update the leaf node with deterministic content + let new_content = b"deterministic_test_content".to_vec(); + graph.update_object_reference(&candidate.name, new_content).unwrap(); // Verify that the leaf node's ID has changed - let updated_leaf = dbg!(graph.get_reference(&candidate.name).unwrap()); + let updated_leaf = graph.get_reference(&candidate.name).unwrap(); assert_ne!(updated_leaf.id, candidate.id, "Leaf node ID should change when content is updated"); @@ -200,3 +206,90 @@ fn test_id_calculation() { "ID calculation should be consistent for the same reference properties"); } +/// Tests that the root ID always changes when any reference is updated +#[test] +fn test_root_id_changes_for_any_reference_update() { + let mut graph = create_test_graph(); + + // Get all non-root references sorted by name for deterministic iteration + let mut all_refs: Vec<(String, String)> = graph.refs.as_ref() + .iter() + .filter(|(name, _)| **name != graph.root.name) + .map(|(name, ref_arc)| (name.clone(), ref_arc.id.clone())) + .collect(); + + all_refs.sort_by(|a, b| a.0.cmp(&b.0)); + + // Test each reference update + for (ref_name, original_ref_id) in all_refs { + // Record the current root ID + let initial_root_id = graph.root.id.clone(); + + // Update the reference with new content + let new_content = format!("updated_content_for_{}", ref_name).into_bytes(); + graph.update_object_reference(&ref_name, new_content).unwrap(); + + // Verify the reference itself changed + let updated_ref = graph.get_reference(&ref_name).unwrap(); + assert_ne!(updated_ref.id, original_ref_id, + "Reference {} should have changed ID after update", ref_name); + + // Verify the root ID changed + assert_ne!(graph.root.id, initial_root_id, + "Root ID should change when reference {} is updated", ref_name); + } +} + +/// Tests that dependencies of a Reference are always lexicographically ordered by name +#[test] +fn test_dependencies_lexicographically_ordered() { + let graph = create_test_graph(); + + // Check all references to ensure their dependents are lexicographically ordered + for (_, reference) in graph.refs.as_ref() { + if reference.dependents.len() > 1 { + // Check that dependents are ordered by name + for i in 0..reference.dependents.len() - 1 { + let current = &reference.dependents[i]; + let next = &reference.dependents[i + 1]; + + assert!(current.name < next.name, + "Dependencies should be lexicographically ordered by name. Found '{}' before '{}'", + current.name, next.name); + } + } + } + + // Also verify that when we add dependencies, they maintain the correct order + let mut test_ref = Reference::new( + Some(String::from("test_content")), + String::from("/test_ordering"), + ); + + // Add dependencies in non-lexicographical order + let dep_c = Reference::new( + Some(String::from("c_content")), + String::from("/c"), + ).to_arc(); + + let dep_a = Reference::new( + Some(String::from("a_content")), + String::from("/a"), + ).to_arc(); + + let dep_b = Reference::new( + Some(String::from("b_content")), + String::from("/b"), + ).to_arc(); + + // Add in non-lexicographical order + test_ref = test_ref.add_dep(dep_c.clone()); + test_ref = test_ref.add_dep(dep_a.clone()); + test_ref = test_ref.add_dep(dep_b.clone()); + + // Verify they are stored in lexicographical order + assert_eq!(test_ref.dependents[0].name, "/a", "First dependent should be '/a'"); + assert_eq!(test_ref.dependents[1].name, "/b", "Second dependent should be '/b'"); + assert_eq!(test_ref.dependents[2].name, "/c", "Third dependent should be '/c'"); +} +