1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::profile::{
8 BalancedRecallSnapshot, BalancedRecallState, ProfileBinding, ProfileLifecycle, ProfileRecord,
9};
10use crate::section_state::{SectionPosteriorSnapshot, SectionPosteriorState};
11
12pub fn sort_fallback_candidates(candidates: &mut [&ProfileRecord]) {
19 candidates.sort_by(|a, b| a.created_at.cmp(&b.created_at).then(a.id.cmp(&b.id)));
20}
21
22pub struct BrainState {
24 pub profiles: HashMap<String, ProfileRecord>,
25 pub balanced_recall: BalancedRecallState,
26 pub profile_states: HashMap<String, BalancedRecallState>,
27 pub bindings: Vec<ProfileBinding>,
28 pub section_states: HashMap<String, SectionPosteriorState>,
29 pub router_state: HashMap<String, RouterStateBlob>,
30 pub adapter_set: HashMap<String, Vec<AdapterRecord>>,
31}
32
33impl BrainState {
34 pub fn new(entity_capacity: usize) -> Self {
36 let mut profiles = HashMap::new();
37 let record = ProfileRecord::new_balanced_recall(entity_capacity);
38 let profile_id = record.id.clone();
39 profiles.insert(profile_id, record);
40
41 Self {
42 profiles,
43 balanced_recall: BalancedRecallState::new(entity_capacity),
44 profile_states: HashMap::new(),
45 bindings: Vec::new(),
46 section_states: HashMap::new(),
47 router_state: HashMap::new(),
48 adapter_set: HashMap::new(),
49 }
50 }
51
52 pub fn to_snapshot(&self) -> BrainStateSnapshot {
54 let extra: HashMap<String, BalancedRecallSnapshot> = self
55 .profile_states
56 .iter()
57 .map(|(id, s)| (id.clone(), s.to_snapshot()))
58 .collect();
59 let section_states: HashMap<String, SectionPosteriorSnapshot> = self
60 .section_states
61 .iter()
62 .map(|(id, s)| (id.clone(), s.to_snapshot()))
63 .collect();
64 BrainStateSnapshot {
65 profiles: self.profiles.clone(),
66 balanced_recall: self.balanced_recall.to_snapshot(),
67 profile_states: extra,
68 bindings: self.bindings.clone(),
69 section_states,
70 router_state: self.router_state.clone(),
71 adapter_set: self.adapter_set.clone(),
72 }
73 }
74
75 pub fn from_snapshot(snapshot: BrainStateSnapshot, entity_capacity: usize) -> Self {
77 let extra: HashMap<String, BalancedRecallState> = snapshot
78 .profile_states
79 .into_iter()
80 .map(|(id, s)| (id, BalancedRecallState::from_snapshot(s, entity_capacity)))
81 .collect();
82 let section_states: HashMap<String, SectionPosteriorState> = snapshot
83 .section_states
84 .into_iter()
85 .map(|(id, s)| (id, SectionPosteriorState::from_snapshot(s)))
86 .collect();
87 Self {
88 profiles: snapshot.profiles,
89 balanced_recall: BalancedRecallState::from_snapshot(
90 snapshot.balanced_recall,
91 entity_capacity,
92 ),
93 profile_states: extra,
94 bindings: snapshot.bindings,
95 section_states,
96 router_state: snapshot.router_state,
97 adapter_set: snapshot.adapter_set,
98 }
99 }
100
101 pub fn reset_posteriors(&mut self) {
103 self.balanced_recall.reset_posteriors();
104 if let Some(record) = self.profiles.get_mut("balanced-recall-v1") {
105 record.exploration_epoch = self.balanced_recall.exploration_epoch;
106 record.state_snapshot = serde_json::to_value(self.balanced_recall.to_snapshot()).ok();
107 }
108 if let Some(ss) = self.section_states.get_mut("balanced-recall-v1") {
109 ss.reset_posteriors();
110 }
111 }
112
113 pub fn reset_profile_posteriors(&mut self, profile_id: &str) {
115 if let Some(ps) = self.profile_states.get_mut(profile_id) {
116 ps.reset_posteriors();
117 let snap = serde_json::to_value(ps.to_snapshot()).ok();
118 let epoch = ps.exploration_epoch;
119 if let Some(record) = self.profiles.get_mut(profile_id) {
120 record.exploration_epoch = epoch;
121 record.state_snapshot = snap;
122 }
123 }
124 if let Some(ss) = self.section_states.get_mut(profile_id) {
125 ss.reset_posteriors();
126 }
127 }
128
129 pub fn resolve(
131 &self,
132 actor: Option<&str>,
133 namespace: Option<&str>,
134 consumer_kind: &str,
135 ) -> Option<&ProfileRecord> {
136 self.resolve_with_match(actor, namespace, consumer_kind)
137 .map(|(record, _, _)| record)
138 }
139
140 pub fn resolve_with_match(
149 &self,
150 actor: Option<&str>,
151 namespace: Option<&str>,
152 consumer_kind: &str,
153 ) -> Option<(&ProfileRecord, String, bool)> {
154 let actor_val = actor.unwrap_or("*");
155 let namespace_val = namespace.unwrap_or("*");
156
157 let best = self
158 .bindings
159 .iter()
160 .filter(|b| {
161 (b.actor == "*" || b.actor == actor_val)
162 && (b.namespace == "*" || b.namespace == namespace_val)
163 && (b.consumer_kind == "*" || b.consumer_kind == consumer_kind)
164 && self
165 .profiles
166 .get(&b.profile_id)
167 .is_some_and(|p| p.lifecycle != ProfileLifecycle::Archived)
168 })
169 .max_by_key(|b| {
170 let actor_score = if b.actor != "*" { 4 } else { 0 };
171 let ns_score = if b.namespace != "*" { 2 } else { 0 };
172 let kind_score = if b.consumer_kind != "*" { 1 } else { 0 };
173 (
174 actor_score + ns_score + kind_score,
175 b.priority,
176 -(b.created_at.timestamp()),
177 )
178 });
179
180 if let Some(binding) = best {
181 if let Some(record) = self.profiles.get(&binding.profile_id) {
182 return Some((record, binding.consumer_kind.clone(), true));
184 }
185 }
186
187 if let Some(default) = self.profiles.get("balanced-recall-v1") {
188 if default.lifecycle == ProfileLifecycle::Active
189 && (default.consumer_kind == consumer_kind
190 || consumer_kind == "*"
191 || default.consumer_kind == "*")
192 {
193 return Some((default, default.consumer_kind.clone(), false));
195 }
196 }
197
198 let mut candidates: Vec<&ProfileRecord> = self
201 .profiles
202 .values()
203 .filter(|p| p.consumer_kind == consumer_kind && p.lifecycle == ProfileLifecycle::Active)
204 .collect();
205 sort_fallback_candidates(&mut candidates);
206 candidates
207 .into_iter()
208 .next()
209 .map(|p| (p, p.consumer_kind.clone(), false))
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
220pub struct RouterStateBlob {
221 pub schema_version: u32,
222 pub gate_bytes: Vec<u8>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
228pub struct AdapterRecord {
229 pub adapter_id: String,
230 pub slot: u32,
231 pub content_hash: String,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct BrainStateSnapshot {
237 pub profiles: HashMap<String, ProfileRecord>,
238 pub balanced_recall: BalancedRecallSnapshot,
239 #[serde(default)]
240 pub profile_states: HashMap<String, BalancedRecallSnapshot>,
241 pub bindings: Vec<ProfileBinding>,
242 #[serde(default)]
243 pub section_states: HashMap<String, SectionPosteriorSnapshot>,
244 #[serde(default)]
245 pub router_state: HashMap<String, RouterStateBlob>,
246 #[serde(default)]
247 pub adapter_set: HashMap<String, Vec<AdapterRecord>>,
248}
249
250pub fn validate_brain_state_snapshot(snapshot: &BrainStateSnapshot) -> Result<(), String> {
252 let br = &snapshot.balanced_recall;
253 br.relevance
254 .validate()
255 .map_err(|e| format!("balanced_recall.relevance: {e}"))?;
256 br.salience
257 .validate()
258 .map_err(|e| format!("balanced_recall.salience: {e}"))?;
259 br.temporal
260 .validate()
261 .map_err(|e| format!("balanced_recall.temporal: {e}"))?;
262 for (id, p) in &br.entity_posteriors {
263 p.validate()
264 .map_err(|e| format!("balanced_recall.entity_posteriors[{id}]: {e}"))?;
265 }
266
267 for (pid, ps) in &snapshot.profile_states {
268 ps.relevance
269 .validate()
270 .map_err(|e| format!("profile_states[{pid}].relevance: {e}"))?;
271 ps.salience
272 .validate()
273 .map_err(|e| format!("profile_states[{pid}].salience: {e}"))?;
274 ps.temporal
275 .validate()
276 .map_err(|e| format!("profile_states[{pid}].temporal: {e}"))?;
277 for (id, p) in &ps.entity_posteriors {
278 p.validate()
279 .map_err(|e| format!("profile_states[{pid}].entity_posteriors[{id}]: {e}"))?;
280 }
281 }
282
283 for (pid, ss) in &snapshot.section_states {
284 for (st, p) in &ss.posteriors {
285 p.validate()
286 .map_err(|e| format!("section_states[{pid}].posteriors[{st:?}]: {e}"))?;
287 }
288 for (st, p) in &ss.priors {
289 p.validate()
290 .map_err(|e| format!("section_states[{pid}].priors[{st:?}]: {e}"))?;
291 }
292 }
293
294 Ok(())
295}
296
297pub fn validate_brain_state_snapshot_with_capacity(
317 snapshot: &BrainStateSnapshot,
318 entity_capacity: usize,
319) -> Result<(), String> {
320 validate_brain_state_snapshot(snapshot)?;
321
322 fn check_recall(
323 label: &str,
324 br: &BalancedRecallSnapshot,
325 entity_capacity: usize,
326 ) -> Result<(), String> {
327 if br.entity_posteriors.len() > entity_capacity {
328 return Err(format!(
329 "{label}.entity_posteriors: {} entries exceeds capacity {entity_capacity}",
330 br.entity_posteriors.len()
331 ));
332 }
333
334 if br.entity_posteriors_version > 1 {
335 return Err(format!(
336 "{label}.entity_posteriors_version: unknown version {}",
337 br.entity_posteriors_version
338 ));
339 }
340
341 if !br.entity_posterior_order.is_empty() {
342 let mut seen =
343 std::collections::HashSet::with_capacity(br.entity_posterior_order.len());
344 for id in &br.entity_posterior_order {
345 if !seen.insert(*id) {
346 return Err(format!("{label}.entity_posterior_order: duplicate id {id}"));
347 }
348 if !br.entity_posteriors.contains_key(id) {
349 return Err(format!(
350 "{label}.entity_posterior_order: id {id} not present in entity_posteriors"
351 ));
352 }
353 }
354 }
355
356 if br.entity_posteriors_version == 0 && !br.entity_posterior_order.is_empty() {
359 return Err(format!(
360 "{label}.entity_posterior_order: non-empty order requires entity_posteriors_version >= 1 (got version 0, the legacy empty-order compatibility version)",
361 ));
362 }
363
364 if br.entity_posteriors_version >= 1
366 && br.entity_posterior_order.len() != br.entity_posteriors.len()
367 {
368 return Err(format!(
369 "{label}.entity_posterior_order: length {} does not cover all {} entity_posteriors entries (version {} requires full order coverage)",
370 br.entity_posterior_order.len(),
371 br.entity_posteriors.len(),
372 br.entity_posteriors_version,
373 ));
374 }
375
376 Ok(())
377 }
378
379 check_recall(
380 "balanced_recall",
381 &snapshot.balanced_recall,
382 entity_capacity,
383 )?;
384 for (pid, ps) in &snapshot.profile_states {
385 check_recall(&format!("profile_states[{pid}]"), ps, entity_capacity)?;
386 }
387
388 Ok(())
389}
390
391#[cfg(test)]
392mod tests {
393 use chrono::{TimeZone, Utc};
394
395 use super::*;
396 use crate::profile::ProfileLifecycle;
397
398 fn make_profile(id: &str, consumer_kind: &str, ts_secs: i64) -> ProfileRecord {
399 ProfileRecord {
400 id: id.to_owned(),
401 description: String::new(),
402 consumer_kind: consumer_kind.to_owned(),
403 state_class: "Bayesian".into(),
404 lifecycle: ProfileLifecycle::Active,
405 created_at: Utc.timestamp_opt(ts_secs, 0).unwrap(),
406 state_snapshot: None,
407 total_events: 0,
408 exploration_epoch: 0,
409 }
410 }
411
412 #[test]
415 fn sort_fallback_candidates_produces_created_at_then_id_order() {
416 let p1 = make_profile("aardvark", "recall", 1_000); let p2 = make_profile("mango", "recall", 2_000);
420 let p3 = make_profile("zebra", "recall", 3_000); let mut candidates = vec![&p3, &p2, &p1];
424 sort_fallback_candidates(&mut candidates);
425
426 assert_eq!(
428 candidates[0].id, "aardvark",
429 "earliest profile must come first"
430 );
431 assert_eq!(candidates[1].id, "mango");
432 assert_eq!(candidates[2].id, "zebra", "latest profile must come last");
433 }
434
435 #[test]
438 fn sort_fallback_candidates_breaks_ties_by_id() {
439 let ts = 5_000i64;
440 let p_z = make_profile("z-profile", "recall", ts);
441 let p_a = make_profile("a-profile", "recall", ts);
442 let p_m = make_profile("m-profile", "recall", ts);
443
444 let mut candidates = vec![&p_z, &p_m, &p_a];
446 sort_fallback_candidates(&mut candidates);
447
448 assert_eq!(
449 candidates[0].id, "a-profile",
450 "lowest id must come first on equal timestamps"
451 );
452 assert_eq!(candidates[1].id, "m-profile");
453 assert_eq!(candidates[2].id, "z-profile");
454 }
455
456 #[test]
459 fn resolve_fallback_is_deterministic() {
460 let p_early = make_profile("alpha", "recall", 1_000);
461 let p_later = make_profile("zeta", "recall", 2_000);
462
463 let mut state_a = BrainState {
464 profiles: HashMap::new(),
465 balanced_recall: BalancedRecallState::new(8),
466 profile_states: HashMap::new(),
467 bindings: Vec::new(),
468 section_states: HashMap::new(),
469 router_state: HashMap::new(),
470 adapter_set: HashMap::new(),
471 };
472 state_a.profiles.insert(p_early.id.clone(), p_early.clone());
473 state_a.profiles.insert(p_later.id.clone(), p_later.clone());
474
475 let mut state_b = BrainState {
476 profiles: HashMap::new(),
477 balanced_recall: BalancedRecallState::new(8),
478 profile_states: HashMap::new(),
479 bindings: Vec::new(),
480 section_states: HashMap::new(),
481 router_state: HashMap::new(),
482 adapter_set: HashMap::new(),
483 };
484 state_b.profiles.insert(p_later.id.clone(), p_later.clone());
486 state_b.profiles.insert(p_early.id.clone(), p_early.clone());
487
488 let result_a = state_a.resolve(None, None, "recall");
489 let result_b = state_b.resolve(None, None, "recall");
490
491 let id_a = result_a.map(|p| p.id.clone());
492 let id_b = result_b.map(|p| p.id.clone());
493 assert_eq!(id_a, Some("alpha".to_owned()));
494 assert_eq!(id_a, id_b, "fallback resolution must be deterministic");
495 }
496
497 #[test]
499 fn router_state_and_adapter_set_round_trip() {
500 let mut state = BrainState::new(8);
501
502 let blob = RouterStateBlob {
503 schema_version: 1,
504 gate_bytes: vec![1, 2, 3],
505 };
506 state
507 .router_state
508 .insert("profile-x".to_owned(), blob.clone());
509
510 let record = AdapterRecord {
511 adapter_id: "lora-42".to_owned(),
512 slot: 0,
513 content_hash: "abc123".to_owned(),
514 };
515 state
516 .adapter_set
517 .insert("profile-x".to_owned(), vec![record.clone()]);
518
519 let snapshot = state.to_snapshot();
520 let restored = BrainState::from_snapshot(snapshot, 8);
521
522 assert_eq!(
523 restored.router_state.get("profile-x"),
524 Some(&blob),
525 "router_state must survive round-trip"
526 );
527 assert_eq!(
528 restored.adapter_set.get("profile-x"),
529 Some(&vec![record]),
530 "adapter_set must survive round-trip"
531 );
532 }
533
534 #[test]
537 fn snapshot_missing_router_and_adapter_fields_defaults_to_empty() {
538 let state = BrainState::new(8);
539 let snapshot = state.to_snapshot();
540
541 let mut value: serde_json::Value =
542 serde_json::to_value(&snapshot).expect("serialize snapshot");
543
544 let obj = value.as_object_mut().expect("snapshot is a JSON object");
545 obj.remove("router_state");
546 obj.remove("adapter_set");
547
548 let restored: BrainStateSnapshot =
549 serde_json::from_value(value).expect("deserialize old-format snapshot");
550
551 assert!(
552 restored.router_state.is_empty(),
553 "router_state must default to empty map when absent from JSON"
554 );
555 assert!(
556 restored.adapter_set.is_empty(),
557 "adapter_set must default to empty map when absent from JSON"
558 );
559 }
560
561 #[test]
564 fn validate_brain_state_snapshot_with_capacity_rejects_entity_posteriors_over_capacity() {
565 use uuid::Uuid;
566
567 let capacity = 2;
568 let mut state = BrainState::new(capacity);
569 for _ in 0..(capacity + 1) {
570 state
571 .balanced_recall
572 .entity_posteriors
573 .get_or_insert(Uuid::new_v4(), crate::posterior::BetaPosterior::default);
574 }
575 let mut snapshot = state.to_snapshot();
578 snapshot
579 .balanced_recall
580 .entity_posteriors
581 .insert(Uuid::new_v4(), crate::posterior::BetaPosterior::default());
582
583 let result = validate_brain_state_snapshot_with_capacity(&snapshot, capacity);
584 assert!(
585 result.is_err(),
586 "snapshot with entity_posteriors.len() > capacity must be rejected"
587 );
588 }
589
590 #[test]
593 fn validate_brain_state_snapshot_with_capacity_rejects_profile_state_over_capacity() {
594 use uuid::Uuid;
595
596 let capacity = 1;
597 let mut state = BrainState::new(capacity);
598 let mut extra = BalancedRecallState::new(capacity);
599 extra
600 .entity_posteriors
601 .get_or_insert(Uuid::new_v4(), crate::posterior::BetaPosterior::default);
602 state
603 .profile_states
604 .insert("extra-profile".to_owned(), extra);
605
606 let mut snapshot = state.to_snapshot();
607 snapshot
608 .profile_states
609 .get_mut("extra-profile")
610 .unwrap()
611 .entity_posteriors
612 .insert(Uuid::new_v4(), crate::posterior::BetaPosterior::default());
613
614 let result = validate_brain_state_snapshot_with_capacity(&snapshot, capacity);
615 assert!(
616 result.is_err(),
617 "profile_states entry with entity_posteriors.len() > capacity must be rejected"
618 );
619 }
620
621 fn make_versioned_recall_snapshot(capacity: usize, n: usize) -> BalancedRecallSnapshot {
626 let mut state = BalancedRecallState::new(capacity);
627 for _ in 0..n {
628 state.entity_posteriors.get_or_insert(
629 uuid::Uuid::new_v4(),
630 crate::posterior::BetaPosterior::default,
631 );
632 }
633 state.to_snapshot()
634 }
635
636 #[test]
637 fn validate_brain_state_snapshot_with_capacity_rejects_missing_order_ids() {
638 let capacity = 4;
639 let mut snapshot = make_versioned_recall_snapshot(capacity, 2);
640 assert_eq!(snapshot.entity_posteriors_version, 1);
641 assert_eq!(snapshot.entity_posterior_order.len(), 2);
642
643 snapshot.entity_posterior_order.truncate(1);
646
647 let mut full_snapshot = BrainState::new(capacity).to_snapshot();
648 full_snapshot.balanced_recall = snapshot;
649
650 let result = validate_brain_state_snapshot_with_capacity(&full_snapshot, capacity);
651 assert!(
652 result.is_err(),
653 "version-1 snapshot with order shorter than entity_posteriors must be rejected"
654 );
655 assert!(
656 result.unwrap_err().contains("does not cover all"),
657 "error must name the missing-coverage reason"
658 );
659 }
660
661 #[test]
662 fn validate_brain_state_snapshot_with_capacity_rejects_duplicate_order_ids() {
663 let capacity = 4;
664 let mut snapshot = make_versioned_recall_snapshot(capacity, 2);
665 let dup = snapshot.entity_posterior_order[0];
666 snapshot.entity_posterior_order[1] = dup;
667
668 let mut full_snapshot = BrainState::new(capacity).to_snapshot();
669 full_snapshot.balanced_recall = snapshot;
670
671 let result = validate_brain_state_snapshot_with_capacity(&full_snapshot, capacity);
672 assert!(
673 result.is_err(),
674 "duplicate entity_posterior_order ids must be rejected"
675 );
676 assert!(result.unwrap_err().contains("duplicate id"));
677 }
678
679 #[test]
680 fn validate_brain_state_snapshot_with_capacity_rejects_unknown_order_ids() {
681 let capacity = 4;
682 let mut snapshot = make_versioned_recall_snapshot(capacity, 2);
683 snapshot.entity_posterior_order.push(uuid::Uuid::new_v4());
684
685 let mut full_snapshot = BrainState::new(capacity).to_snapshot();
686 full_snapshot.balanced_recall = snapshot;
687
688 let result = validate_brain_state_snapshot_with_capacity(&full_snapshot, capacity);
689 assert!(
690 result.is_err(),
691 "entity_posterior_order id absent from entity_posteriors must be rejected"
692 );
693 assert!(result
694 .unwrap_err()
695 .contains("not present in entity_posteriors"));
696 }
697
698 #[test]
699 fn validate_brain_state_snapshot_with_capacity_rejects_unknown_version() {
700 let capacity = 4;
701 let mut snapshot = make_versioned_recall_snapshot(capacity, 1);
702 snapshot.entity_posteriors_version = 2;
703
704 let mut full_snapshot = BrainState::new(capacity).to_snapshot();
705 full_snapshot.balanced_recall = snapshot;
706
707 let result = validate_brain_state_snapshot_with_capacity(&full_snapshot, capacity);
708 assert!(
709 result.is_err(),
710 "unknown entity_posteriors_version must be rejected"
711 );
712 assert!(result.unwrap_err().contains("unknown version"));
713 }
714
715 #[test]
716 fn validate_brain_state_snapshot_with_capacity_accepts_legacy_empty_order() {
717 let capacity = 4;
718 let mut snapshot = make_versioned_recall_snapshot(capacity, 2);
719 snapshot.entity_posteriors_version = 0;
722 snapshot.entity_posterior_order.clear();
723
724 let mut full_snapshot = BrainState::new(capacity).to_snapshot();
725 full_snapshot.balanced_recall = snapshot;
726
727 let result = validate_brain_state_snapshot_with_capacity(&full_snapshot, capacity);
728 assert!(
729 result.is_ok(),
730 "legacy version-0 snapshot with empty order must be accepted: {result:?}"
731 );
732 }
733
734 #[test]
735 fn validate_brain_state_snapshot_with_capacity_rejects_version_zero_with_partial_order() {
736 let capacity = 4;
739 let mut snapshot = make_versioned_recall_snapshot(capacity, 2);
740 snapshot.entity_posteriors_version = 0;
741 snapshot.entity_posterior_order.truncate(1);
742
743 let mut full_snapshot = BrainState::new(capacity).to_snapshot();
744 full_snapshot.balanced_recall = snapshot;
745
746 let result = validate_brain_state_snapshot_with_capacity(&full_snapshot, capacity);
747 assert!(
748 result.is_err(),
749 "version-0 snapshot with non-empty partial order must be rejected"
750 );
751 assert!(
752 result
753 .unwrap_err()
754 .contains("requires entity_posteriors_version >= 1"),
755 "error must name the version-0-with-non-empty-order reason"
756 );
757 }
758
759 #[test]
760 fn validate_brain_state_snapshot_with_capacity_accepts_full_order_current_version() {
761 let capacity = 4;
762 let mut full_snapshot = BrainState::new(capacity).to_snapshot();
763 full_snapshot.balanced_recall = make_versioned_recall_snapshot(capacity, 3);
764
765 let result = validate_brain_state_snapshot_with_capacity(&full_snapshot, capacity);
766 assert!(
767 result.is_ok(),
768 "version-1 snapshot with a fully-covering order must be accepted: {result:?}"
769 );
770 }
771
772 #[test]
775 fn restore_to_snapshot_restore_idempotency() {
776 let capacity = 4;
777 let mut state = BrainState::new(capacity);
778 for _ in 0..3 {
779 state.balanced_recall.entity_posteriors.get_or_insert(
780 uuid::Uuid::new_v4(),
781 crate::posterior::BetaPosterior::default,
782 );
783 }
784
785 let snapshot_1 = state.to_snapshot();
786 validate_brain_state_snapshot_with_capacity(&snapshot_1, capacity)
787 .expect("first snapshot must validate");
788
789 let restored_1 = BrainState::from_snapshot(snapshot_1, capacity);
790 let snapshot_2 = restored_1.to_snapshot();
791 validate_brain_state_snapshot_with_capacity(&snapshot_2, capacity)
792 .expect("round-tripped snapshot must still validate");
793
794 let restored_2 = BrainState::from_snapshot(snapshot_2.clone(), capacity);
795 let snapshot_3 = restored_2.to_snapshot();
796
797 assert_eq!(
798 snapshot_2.balanced_recall.entity_posteriors,
799 snapshot_3.balanced_recall.entity_posteriors,
800 "entity_posteriors must be stable across a second restore/snapshot cycle"
801 );
802 assert_eq!(
803 snapshot_2.balanced_recall.entity_posterior_order,
804 snapshot_3.balanced_recall.entity_posterior_order,
805 "entity_posterior_order must be stable across a second restore/snapshot cycle"
806 );
807 }
808}