Skip to main content

hypen_engine/lifecycle/
module.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4/// Metadata describing a stateful module.
5///
6/// A module is the unit of state and behavior in Hypen. It declares which
7/// actions it handles, which state keys it exposes, and optional persistence
8/// or versioning settings.
9///
10/// Modules are instantiated as [`ModuleInstance`] which holds the live state.
11///
12/// # Example
13///
14/// ```rust
15/// use hypen_engine::Module;
16///
17/// let module = Module::new("Counter")
18///     .with_actions(vec!["increment".into(), "decrement".into()])
19///     .with_state_keys(vec!["count".into()]);
20/// ```
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Module {
23    /// Module name (e.g. `"Counter"`, `"ProfilePage"`)
24    pub name: String,
25
26    /// Action names this module handles (e.g. `["increment", "decrement"]`)
27    pub actions: Vec<String>,
28
29    /// Top-level state keys this module exposes (e.g. `["count", "user"]`)
30    pub state_keys: Vec<String>,
31
32    /// Whether state should be persisted across sessions
33    pub persist: bool,
34
35    /// Optional schema version for state migration
36    pub version: Option<u32>,
37}
38
39impl Module {
40    pub fn new(name: impl Into<String>) -> Self {
41        Self {
42            name: name.into(),
43            actions: Vec::new(),
44            state_keys: Vec::new(),
45            persist: false,
46            version: None,
47        }
48    }
49
50    pub fn with_actions(mut self, actions: Vec<String>) -> Self {
51        self.actions = actions;
52        self
53    }
54
55    pub fn with_state_keys(mut self, state_keys: Vec<String>) -> Self {
56        self.state_keys = state_keys;
57        self
58    }
59
60    pub fn with_persist(mut self, persist: bool) -> Self {
61        self.persist = persist;
62        self
63    }
64
65    pub fn with_version(mut self, version: u32) -> Self {
66        self.version = Some(version);
67        self
68    }
69}
70
71/// Lifecycle hooks for a module.
72///
73/// Implement this trait to receive notifications when a module is created,
74/// destroyed, or when its state changes. This is primarily used by SDK
75/// implementations to bridge engine lifecycle events to host language callbacks.
76pub trait ModuleLifecycle {
77    /// Called when the module is first mounted.
78    fn on_created(&mut self);
79
80    /// Called when the module is unmounted and about to be dropped.
81    fn on_destroyed(&mut self);
82
83    /// Called when state is updated from the host.
84    fn on_state_changed(&mut self, state: serde_json::Value);
85}
86
87/// Callback type for lifecycle events
88pub type LifecycleCallback = Box<dyn Fn() + Send + Sync>;
89
90/// A live module instance holding the current state and lifecycle callbacks.
91///
92/// State is wrapped in `Arc` for O(1) clone performance — critical because
93/// state is accessed on every render cycle. With `Arc`, "cloning" is just a
94/// reference count increment instead of a deep copy.
95///
96/// # Lifecycle
97///
98/// ```text
99/// new() → mount() → [update_state()...] → unmount()
100///          ↑                                    |
101///          └────────────────────────────────────┘
102///                   (can re-mount)
103/// ```
104///
105/// Mount/unmount are idempotent — calling `mount()` twice only fires `on_created` once.
106pub struct ModuleInstance {
107    /// Module metadata (name, action names, state keys)
108    pub module: Module,
109
110    /// Current state snapshot (Arc-wrapped for O(1) clone)
111    state: Arc<serde_json::Value>,
112
113    /// Whether the module is currently mounted
114    pub mounted: bool,
115
116    /// Callback for when module is created
117    on_created: Option<LifecycleCallback>,
118
119    /// Callback for when module is destroyed
120    on_destroyed: Option<LifecycleCallback>,
121}
122
123impl ModuleInstance {
124    pub fn new(module: Module, initial_state: serde_json::Value) -> Self {
125        Self {
126            module,
127            state: Arc::new(initial_state),
128            mounted: false,
129            on_created: None,
130            on_destroyed: None,
131        }
132    }
133
134    /// Set the on_created lifecycle callback
135    pub fn set_on_created<F>(&mut self, callback: F)
136    where
137        F: Fn() + Send + Sync + 'static,
138    {
139        self.on_created = Some(Box::new(callback));
140    }
141
142    /// Set the on_destroyed lifecycle callback
143    pub fn set_on_destroyed<F>(&mut self, callback: F)
144    where
145        F: Fn() + Send + Sync + 'static,
146    {
147        self.on_destroyed = Some(Box::new(callback));
148    }
149
150    /// Mount the module (call on_created)
151    pub fn mount(&mut self) {
152        if !self.mounted {
153            self.mounted = true;
154            // Call on_created lifecycle hook if registered
155            if let Some(ref callback) = self.on_created {
156                callback();
157            }
158        }
159    }
160
161    /// Unmount the module (call on_destroyed)
162    pub fn unmount(&mut self) {
163        if self.mounted {
164            // Call on_destroyed lifecycle hook if registered
165            if let Some(ref callback) = self.on_destroyed {
166                callback();
167            }
168            self.mounted = false;
169        }
170    }
171
172    /// Update state from a patch
173    ///
174    /// Uses Arc::make_mut for copy-on-write semantics: if this is the only
175    /// reference, mutates in place; otherwise clones first.
176    pub fn update_state(&mut self, patch: serde_json::Value) {
177        // Arc::make_mut provides copy-on-write: clones only if shared
178        let state = Arc::make_mut(&mut self.state);
179        merge_json(state, patch);
180    }
181
182    /// Update state from sparse path-value pairs
183    /// This is more efficient than sending the full state when only a few paths changed
184    ///
185    /// Uses Arc::make_mut for copy-on-write semantics.
186    pub fn update_state_sparse(&mut self, paths: &[String], values: &serde_json::Value) {
187        // values is expected to be an object mapping paths to their new values
188        if let serde_json::Value::Object(map) = values {
189            // Only get mutable access if we have paths to update
190            if paths.iter().any(|p| map.contains_key(p)) {
191                let state = Arc::make_mut(&mut self.state);
192                for path in paths {
193                    if let Some(new_value) = map.get(path) {
194                        set_value_at_path(state, path, new_value.clone());
195                    }
196                }
197            }
198        }
199    }
200
201    /// Get a reference to the current state
202    ///
203    /// For read-only access without cloning. Prefer this over `get_state_shared()`
204    /// when you don't need to store the state beyond the current scope.
205    pub fn get_state(&self) -> &serde_json::Value {
206        &self.state
207    }
208
209    /// Get a shared reference to the current state (O(1) clone)
210    ///
211    /// Use this for the render hot path where state needs to be passed around
212    /// without deep copying. Cloning the Arc is just a reference count increment.
213    pub fn get_state_shared(&self) -> Arc<serde_json::Value> {
214        Arc::clone(&self.state)
215    }
216}
217
218/// Deep merge two JSON values
219fn merge_json(target: &mut serde_json::Value, source: serde_json::Value) {
220    use serde_json::Value;
221
222    match (target, source) {
223        (Value::Object(target_map), Value::Object(source_map)) => {
224            for (key, value) in source_map {
225                if let Some(target_value) = target_map.get_mut(&key) {
226                    merge_json(target_value, value);
227                } else {
228                    target_map.insert(key, value);
229                }
230            }
231        }
232        (target, source) => {
233            *target = source;
234        }
235    }
236}
237
238/// Set a value at a dot-separated path (e.g., "user.profile.name")
239/// Creates intermediate objects if they don't exist
240fn set_value_at_path(target: &mut serde_json::Value, path: &str, value: serde_json::Value) {
241    use serde_json::Value;
242
243    let parts: Vec<&str> = path.split('.').collect();
244    if parts.is_empty() {
245        return;
246    }
247
248    let mut current = target;
249
250    // Navigate to the parent of the final key
251    for part in &parts[..parts.len() - 1] {
252        // Try to parse as array index first
253        if let Ok(index) = part.parse::<usize>() {
254            if let Value::Array(arr) = current {
255                // Extend array if needed
256                while arr.len() <= index {
257                    arr.push(Value::Null);
258                }
259                current = &mut arr[index];
260                continue;
261            }
262        }
263
264        // Otherwise treat as object key
265        if !current.is_object() {
266            *current = Value::Object(serde_json::Map::new());
267        }
268
269        if let Value::Object(map) = current {
270            if !map.contains_key(*part) {
271                map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
272            }
273            current = map.get_mut(*part).unwrap();
274        }
275    }
276
277    // Set the final value
278    let final_key = parts[parts.len() - 1];
279
280    // Try to parse as array index
281    if let Ok(index) = final_key.parse::<usize>() {
282        if let Value::Array(arr) = current {
283            while arr.len() <= index {
284                arr.push(Value::Null);
285            }
286            arr[index] = value;
287            return;
288        }
289    }
290
291    // Set as object property
292    if !current.is_object() {
293        *current = Value::Object(serde_json::Map::new());
294    }
295
296    if let Value::Object(map) = current {
297        map.insert(final_key.to_string(), value);
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use serde_json::json;
305
306    #[test]
307    fn test_merge_json() {
308        let mut target = json!({
309            "user": {
310                "name": "Alice",
311                "age": 30
312            }
313        });
314
315        let patch = json!({
316            "user": {
317                "age": 31
318            }
319        });
320
321        merge_json(&mut target, patch);
322
323        assert_eq!(target["user"]["name"], "Alice");
324        assert_eq!(target["user"]["age"], 31);
325    }
326
327    #[test]
328    fn test_lifecycle_on_created_callback() {
329        use std::sync::{Arc, Mutex};
330
331        let module = Module::new("TestModule");
332        let initial_state = json!({});
333        let mut instance = ModuleInstance::new(module, initial_state);
334
335        // Track if callback was called
336        let called = Arc::new(Mutex::new(false));
337        let called_clone = called.clone();
338
339        instance.set_on_created(move || {
340            *called_clone.lock().unwrap() = true;
341        });
342
343        // Mount should call on_created
344        assert!(!*called.lock().unwrap());
345        instance.mount();
346        assert!(*called.lock().unwrap());
347        assert!(instance.mounted);
348    }
349
350    #[test]
351    fn test_lifecycle_on_destroyed_callback() {
352        use std::sync::{Arc, Mutex};
353
354        let module = Module::new("TestModule");
355        let initial_state = json!({});
356        let mut instance = ModuleInstance::new(module, initial_state);
357
358        let called = Arc::new(Mutex::new(false));
359        let called_clone = called.clone();
360
361        instance.set_on_destroyed(move || {
362            *called_clone.lock().unwrap() = true;
363        });
364
365        // First mount the module
366        instance.mount();
367        assert!(instance.mounted);
368
369        // Unmount should call on_destroyed
370        assert!(!*called.lock().unwrap());
371        instance.unmount();
372        assert!(*called.lock().unwrap());
373        assert!(!instance.mounted);
374    }
375
376    #[test]
377    fn test_lifecycle_callbacks_not_called_when_not_set() {
378        let module = Module::new("TestModule");
379        let initial_state = json!({});
380        let mut instance = ModuleInstance::new(module, initial_state);
381
382        // Should not panic when callbacks are not set
383        instance.mount();
384        assert!(instance.mounted);
385
386        instance.unmount();
387        assert!(!instance.mounted);
388    }
389
390    #[test]
391    fn test_lifecycle_mount_idempotent() {
392        use std::sync::{Arc, Mutex};
393
394        let module = Module::new("TestModule");
395        let initial_state = json!({});
396        let mut instance = ModuleInstance::new(module, initial_state);
397
398        let call_count = Arc::new(Mutex::new(0));
399        let call_count_clone = call_count.clone();
400
401        instance.set_on_created(move || {
402            *call_count_clone.lock().unwrap() += 1;
403        });
404
405        // First mount
406        instance.mount();
407        assert_eq!(*call_count.lock().unwrap(), 1);
408
409        // Second mount should not call callback again
410        instance.mount();
411        assert_eq!(*call_count.lock().unwrap(), 1);
412    }
413
414    #[test]
415    fn test_lifecycle_unmount_idempotent() {
416        use std::sync::{Arc, Mutex};
417
418        let module = Module::new("TestModule");
419        let initial_state = json!({});
420        let mut instance = ModuleInstance::new(module, initial_state);
421
422        let call_count = Arc::new(Mutex::new(0));
423        let call_count_clone = call_count.clone();
424
425        instance.set_on_destroyed(move || {
426            *call_count_clone.lock().unwrap() += 1;
427        });
428
429        // Mount first
430        instance.mount();
431
432        // First unmount
433        instance.unmount();
434        assert_eq!(*call_count.lock().unwrap(), 1);
435
436        // Second unmount should not call callback again
437        instance.unmount();
438        assert_eq!(*call_count.lock().unwrap(), 1);
439    }
440
441    #[test]
442    fn test_lifecycle_full_cycle() {
443        use std::sync::{Arc, Mutex};
444
445        let module = Module::new("TestModule");
446        let initial_state = json!({});
447        let mut instance = ModuleInstance::new(module, initial_state);
448
449        let events = Arc::new(Mutex::new(Vec::new()));
450        let events_created = events.clone();
451        let events_destroyed = events.clone();
452
453        instance.set_on_created(move || {
454            events_created.lock().unwrap().push("created");
455        });
456
457        instance.set_on_destroyed(move || {
458            events_destroyed.lock().unwrap().push("destroyed");
459        });
460
461        // Full lifecycle: mount -> unmount -> mount -> unmount
462        instance.mount();
463        instance.unmount();
464        instance.mount();
465        instance.unmount();
466
467        let events = events.lock().unwrap();
468        assert_eq!(events.len(), 4);
469        assert_eq!(events[0], "created");
470        assert_eq!(events[1], "destroyed");
471        assert_eq!(events[2], "created");
472        assert_eq!(events[3], "destroyed");
473    }
474
475    #[test]
476    fn test_set_value_at_path_simple() {
477        let mut state = json!({
478            "count": 0
479        });
480
481        set_value_at_path(&mut state, "count", json!(42));
482        assert_eq!(state["count"], 42);
483    }
484
485    #[test]
486    fn test_set_value_at_path_nested() {
487        let mut state = json!({
488            "user": {
489                "name": "Alice",
490                "profile": {
491                    "bio": "Developer"
492                }
493            }
494        });
495
496        set_value_at_path(&mut state, "user.profile.bio", json!("Engineer"));
497        assert_eq!(state["user"]["profile"]["bio"], "Engineer");
498        // Other values should be unchanged
499        assert_eq!(state["user"]["name"], "Alice");
500    }
501
502    #[test]
503    fn test_set_value_at_path_creates_intermediate() {
504        let mut state = json!({});
505
506        set_value_at_path(&mut state, "user.profile.name", json!("Bob"));
507        assert_eq!(state["user"]["profile"]["name"], "Bob");
508    }
509
510    #[test]
511    fn test_set_value_at_path_array_index() {
512        let mut state = json!({
513            "items": ["a", "b", "c"]
514        });
515
516        set_value_at_path(&mut state, "items.1", json!("modified"));
517        assert_eq!(state["items"][1], "modified");
518        assert_eq!(state["items"][0], "a");
519        assert_eq!(state["items"][2], "c");
520    }
521
522    #[test]
523    fn test_update_state_sparse_single_path() {
524        let module = Module::new("TestModule");
525        let initial_state = json!({
526            "count": 0,
527            "name": "Alice"
528        });
529        let mut instance = ModuleInstance::new(module, initial_state);
530
531        let paths = vec!["count".to_string()];
532        let values = json!({
533            "count": 42
534        });
535
536        instance.update_state_sparse(&paths, &values);
537
538        assert_eq!(instance.get_state()["count"], 42);
539        assert_eq!(instance.get_state()["name"], "Alice"); // Unchanged
540    }
541
542    #[test]
543    fn test_update_state_sparse_nested_path() {
544        let module = Module::new("TestModule");
545        let initial_state = json!({
546            "user": {
547                "name": "Alice",
548                "age": 30
549            },
550            "settings": {
551                "theme": "dark"
552            }
553        });
554        let mut instance = ModuleInstance::new(module, initial_state);
555
556        let paths = vec!["user.age".to_string()];
557        let values = json!({
558            "user.age": 31
559        });
560
561        instance.update_state_sparse(&paths, &values);
562
563        assert_eq!(instance.get_state()["user"]["age"], 31);
564        assert_eq!(instance.get_state()["user"]["name"], "Alice"); // Unchanged
565        assert_eq!(instance.get_state()["settings"]["theme"], "dark"); // Unchanged
566    }
567
568    #[test]
569    fn test_update_state_sparse_multiple_paths() {
570        let module = Module::new("TestModule");
571        let initial_state = json!({
572            "count": 0,
573            "user": {
574                "name": "Alice"
575            }
576        });
577        let mut instance = ModuleInstance::new(module, initial_state);
578
579        let paths = vec!["count".to_string(), "user.name".to_string()];
580        let values = json!({
581            "count": 100,
582            "user.name": "Bob"
583        });
584
585        instance.update_state_sparse(&paths, &values);
586
587        assert_eq!(instance.get_state()["count"], 100);
588        assert_eq!(instance.get_state()["user"]["name"], "Bob");
589    }
590
591    // ============ Edge Case Tests ============
592
593    #[test]
594    fn test_set_value_at_path_empty_path() {
595        let mut state = json!({"count": 0});
596        // Empty path should do nothing
597        set_value_at_path(&mut state, "", json!(42));
598        assert_eq!(state["count"], 0);
599    }
600
601    #[test]
602    fn test_set_value_at_path_null_value() {
603        let mut state = json!({
604            "user": {
605                "name": "Alice",
606                "email": "alice@example.com"
607            }
608        });
609
610        set_value_at_path(&mut state, "user.email", json!(null));
611        assert_eq!(state["user"]["email"], serde_json::Value::Null);
612        assert_eq!(state["user"]["name"], "Alice"); // Unchanged
613    }
614
615    #[test]
616    fn test_set_value_at_path_type_change() {
617        let mut state = json!({
618            "data": "string value"
619        });
620
621        // Change string to object
622        set_value_at_path(&mut state, "data", json!({"nested": true}));
623        assert_eq!(state["data"]["nested"], true);
624
625        // Change object to array
626        set_value_at_path(&mut state, "data", json!([1, 2, 3]));
627        assert_eq!(state["data"][0], 1);
628
629        // Change array to number
630        set_value_at_path(&mut state, "data", json!(42));
631        assert_eq!(state["data"], 42);
632    }
633
634    #[test]
635    fn test_set_value_at_path_deeply_nested() {
636        let mut state = json!({});
637
638        // Create deeply nested path (6 levels deep)
639        set_value_at_path(&mut state, "a.b.c.d.e.f", json!("deep value"));
640        assert_eq!(state["a"]["b"]["c"]["d"]["e"]["f"], "deep value");
641    }
642
643    #[test]
644    fn test_set_value_at_path_nested_array_object() {
645        let mut state = json!({
646            "users": [
647                {"name": "Alice", "tags": ["admin"]},
648                {"name": "Bob", "tags": ["user"]}
649            ]
650        });
651
652        // Update nested object within array
653        set_value_at_path(&mut state, "users.1.name", json!("Robert"));
654        assert_eq!(state["users"][1]["name"], "Robert");
655        assert_eq!(state["users"][0]["name"], "Alice"); // Unchanged
656
657        // Update nested array within array element
658        set_value_at_path(&mut state, "users.0.tags.0", json!("superadmin"));
659        assert_eq!(state["users"][0]["tags"][0], "superadmin");
660    }
661
662    #[test]
663    fn test_set_value_at_path_extend_array() {
664        let mut state = json!({
665            "items": ["a", "b"]
666        });
667
668        // Setting index 5 should extend array with nulls
669        set_value_at_path(&mut state, "items.5", json!("extended"));
670        assert_eq!(state["items"].as_array().unwrap().len(), 6);
671        assert_eq!(state["items"][5], "extended");
672        assert_eq!(state["items"][2], serde_json::Value::Null);
673        assert_eq!(state["items"][3], serde_json::Value::Null);
674        assert_eq!(state["items"][4], serde_json::Value::Null);
675    }
676
677    #[test]
678    fn test_set_value_at_path_overwrite_primitive_with_nested() {
679        let mut state = json!({
680            "config": 42
681        });
682
683        // Trying to set a nested path where parent is a primitive
684        // Should convert primitive to object
685        set_value_at_path(&mut state, "config.nested.value", json!("test"));
686        assert_eq!(state["config"]["nested"]["value"], "test");
687    }
688
689    #[test]
690    fn test_set_value_at_path_boolean_values() {
691        let mut state = json!({
692            "flags": {
693                "enabled": true,
694                "visible": false
695            }
696        });
697
698        set_value_at_path(&mut state, "flags.enabled", json!(false));
699        set_value_at_path(&mut state, "flags.visible", json!(true));
700        assert_eq!(state["flags"]["enabled"], false);
701        assert_eq!(state["flags"]["visible"], true);
702    }
703
704    #[test]
705    fn test_set_value_at_path_float_values() {
706        let mut state = json!({
707            "coordinates": {
708                "lat": 0.0,
709                "lng": 0.0
710            }
711        });
712
713        set_value_at_path(&mut state, "coordinates.lat", json!(37.7749));
714        set_value_at_path(&mut state, "coordinates.lng", json!(-122.4194));
715
716        // Use approximate comparison for floats
717        let lat = state["coordinates"]["lat"].as_f64().unwrap();
718        let lng = state["coordinates"]["lng"].as_f64().unwrap();
719        assert!((lat - 37.7749).abs() < 0.0001);
720        assert!((lng - (-122.4194)).abs() < 0.0001);
721    }
722
723    #[test]
724    fn test_update_state_sparse_empty_paths() {
725        let module = Module::new("TestModule");
726        let initial_state = json!({
727            "count": 0
728        });
729        let mut instance = ModuleInstance::new(module, initial_state);
730
731        let paths: Vec<String> = vec![];
732        let values = json!({});
733
734        // Should not panic or modify state
735        instance.update_state_sparse(&paths, &values);
736        assert_eq!(instance.get_state()["count"], 0);
737    }
738
739    #[test]
740    fn test_update_state_sparse_path_not_in_values() {
741        let module = Module::new("TestModule");
742        let initial_state = json!({
743            "count": 0,
744            "name": "Alice"
745        });
746        let mut instance = ModuleInstance::new(module, initial_state);
747
748        // Path is specified but not in values - should be skipped
749        let paths = vec!["count".to_string(), "missing".to_string()];
750        let values = json!({
751            "count": 42
752            // "missing" not provided
753        });
754
755        instance.update_state_sparse(&paths, &values);
756        assert_eq!(instance.get_state()["count"], 42);
757        assert_eq!(instance.get_state()["name"], "Alice");
758    }
759
760    #[test]
761    fn test_update_state_sparse_invalid_values_type() {
762        let module = Module::new("TestModule");
763        let initial_state = json!({
764            "count": 0
765        });
766        let mut instance = ModuleInstance::new(module, initial_state);
767
768        // values is not an object - should not crash, just skip
769        let paths = vec!["count".to_string()];
770        let values = json!("not an object");
771
772        instance.update_state_sparse(&paths, &values);
773        // State should remain unchanged
774        assert_eq!(instance.get_state()["count"], 0);
775    }
776
777    #[test]
778    fn test_update_state_sparse_array_values() {
779        let module = Module::new("TestModule");
780        let initial_state = json!({
781            "items": []
782        });
783        let mut instance = ModuleInstance::new(module, initial_state);
784
785        let paths = vec!["items".to_string()];
786        let values = json!({
787            "items": ["a", "b", "c"]
788        });
789
790        instance.update_state_sparse(&paths, &values);
791        assert_eq!(instance.get_state()["items"], json!(["a", "b", "c"]));
792    }
793
794    #[test]
795    fn test_update_state_sparse_complex_nested_update() {
796        let module = Module::new("TestModule");
797        let initial_state = json!({
798            "app": {
799                "ui": {
800                    "theme": "light",
801                    "sidebar": {
802                        "collapsed": false,
803                        "width": 250
804                    }
805                },
806                "data": {
807                    "users": [],
808                    "cache": {}
809                }
810            }
811        });
812        let mut instance = ModuleInstance::new(module, initial_state);
813
814        let paths = vec![
815            "app.ui.theme".to_string(),
816            "app.ui.sidebar.collapsed".to_string(),
817            "app.data.users".to_string(),
818        ];
819        let values = json!({
820            "app.ui.theme": "dark",
821            "app.ui.sidebar.collapsed": true,
822            "app.data.users": [{"id": 1, "name": "Alice"}]
823        });
824
825        instance.update_state_sparse(&paths, &values);
826
827        assert_eq!(instance.get_state()["app"]["ui"]["theme"], "dark");
828        assert_eq!(
829            instance.get_state()["app"]["ui"]["sidebar"]["collapsed"],
830            true
831        );
832        assert_eq!(instance.get_state()["app"]["ui"]["sidebar"]["width"], 250); // Unchanged
833        assert_eq!(
834            instance.get_state()["app"]["data"]["users"][0]["name"],
835            "Alice"
836        );
837        assert!(instance.get_state()["app"]["data"]["cache"].is_object()); // Unchanged
838    }
839
840    #[test]
841    fn test_update_state_sparse_preserves_sibling_keys() {
842        let module = Module::new("TestModule");
843        let initial_state = json!({
844            "user": {
845                "name": "Alice",
846                "email": "alice@example.com",
847                "profile": {
848                    "bio": "Developer",
849                    "avatar": "alice.png",
850                    "social": {
851                        "twitter": "@alice",
852                        "github": "alice"
853                    }
854                }
855            }
856        });
857        let mut instance = ModuleInstance::new(module, initial_state);
858
859        // Only update one deeply nested field
860        let paths = vec!["user.profile.social.twitter".to_string()];
861        let values = json!({
862            "user.profile.social.twitter": "@alice_new"
863        });
864
865        instance.update_state_sparse(&paths, &values);
866
867        // Verify updated field
868        assert_eq!(
869            instance.get_state()["user"]["profile"]["social"]["twitter"],
870            "@alice_new"
871        );
872
873        // Verify all sibling fields are unchanged
874        assert_eq!(instance.get_state()["user"]["name"], "Alice");
875        assert_eq!(instance.get_state()["user"]["email"], "alice@example.com");
876        assert_eq!(instance.get_state()["user"]["profile"]["bio"], "Developer");
877        assert_eq!(
878            instance.get_state()["user"]["profile"]["avatar"],
879            "alice.png"
880        );
881        assert_eq!(
882            instance.get_state()["user"]["profile"]["social"]["github"],
883            "alice"
884        );
885    }
886}