1use std::collections::{HashMap, VecDeque};
7use std::marker::PhantomData;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::SystemTime;
11
12use dashmap::DashMap;
13
14use crate::clock::{SharedClock, system_clock};
15use serde_json::Value;
16
17const DEFAULT_MUTATION_JOURNAL_CAPACITY: usize = 1024;
18
19pub struct StateKey<T> {
31 key: &'static str,
32 _phantom: PhantomData<fn() -> T>,
33}
34
35impl<T> StateKey<T> {
36 pub const fn new(key: &'static str) -> Self {
38 Self {
39 key,
40 _phantom: PhantomData,
41 }
42 }
43
44 pub const fn key(&self) -> &'static str {
46 self.key
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum StateMutationOrigin {
54 Set,
56 SetCommitted,
58 Remove,
60 ClearPrefix,
62 Commit,
64}
65
66#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
71pub struct StateMutation {
72 pub sequence: u64,
74 pub key: String,
76 pub old: Option<Value>,
78 pub new: Option<Value>,
80 pub origin: StateMutationOrigin,
82 #[serde(rename = "timestamp_ms", with = "systemtime_epoch_millis")]
85 pub timestamp: SystemTime,
86 pub delta: bool,
88}
89
90mod systemtime_epoch_millis {
92 use std::time::{Duration, SystemTime, UNIX_EPOCH};
93
94 use serde::{Deserialize, Deserializer, Serializer};
95
96 pub(super) fn serialize<S: Serializer>(t: &SystemTime, ser: S) -> Result<S::Ok, S::Error> {
97 let millis = t
98 .duration_since(UNIX_EPOCH)
99 .map(|d| d.as_millis() as u64)
100 .unwrap_or(0);
101 ser.serialize_u64(millis)
102 }
103
104 pub(super) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<SystemTime, D::Error> {
105 let millis = u64::deserialize(de)?;
106 Ok(UNIX_EPOCH + Duration::from_millis(millis))
107 }
108}
109
110pub trait JournalSink: Send + Sync {
120 fn write(&self, m: &StateMutation);
122}
123
124#[derive(Clone, Default)]
127struct JournalSinkSlot(Arc<parking_lot::RwLock<Option<Arc<dyn JournalSink>>>>);
128
129pub const REDACTED: &str = "[redacted]";
132
133#[derive(Clone, Default, Debug)]
135struct RedactionSlot(Arc<parking_lot::RwLock<Arc<std::collections::BTreeSet<String>>>>);
136
137#[derive(Clone)]
139struct ClockSlot(Arc<parking_lot::RwLock<SharedClock>>);
140
141impl Default for ClockSlot {
142 fn default() -> Self {
143 Self(Arc::new(parking_lot::RwLock::new(system_clock())))
144 }
145}
146
147impl std::fmt::Debug for ClockSlot {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_tuple("ClockSlot").field(&*self.0.read()).finish()
150 }
151}
152
153impl std::fmt::Debug for JournalSinkSlot {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 let installed = self.0.read().is_some();
156 f.debug_tuple("JournalSinkSlot").field(&installed).finish()
157 }
158}
159
160const JOURNAL_FLUSH_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
161
162fn journal_log_error(context: &'static str, e: &dyn std::fmt::Display) {
165 tracing::warn!(error = %e, "{context}");
166}
167
168struct FileJournalInner {
169 writer: std::io::BufWriter<std::fs::File>,
170 last_flush: std::time::Instant,
171}
172
173pub struct FileJournalSink {
183 inner: parking_lot::Mutex<FileJournalInner>,
184}
185
186impl FileJournalSink {
187 pub fn create(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
189 let file = std::fs::File::create(path)?;
190 Ok(Self {
191 inner: parking_lot::Mutex::new(FileJournalInner {
192 writer: std::io::BufWriter::new(file),
193 last_flush: std::time::Instant::now(),
194 }),
195 })
196 }
197
198 pub fn flush(&self) {
200 let mut inner = self.inner.lock();
201 if let Err(e) = std::io::Write::flush(&mut inner.writer) {
202 journal_log_error("FileJournalSink flush failed", &e);
203 }
204 inner.last_flush = std::time::Instant::now();
205 }
206}
207
208impl JournalSink for FileJournalSink {
209 fn write(&self, m: &StateMutation) {
210 let line = match serde_json::to_string(m) {
211 Ok(line) => line,
212 Err(e) => {
213 journal_log_error("FileJournalSink serialize failed", &e);
214 return;
215 }
216 };
217 let mut inner = self.inner.lock();
218 if let Err(e) = std::io::Write::write_all(&mut inner.writer, line.as_bytes())
219 .and_then(|()| std::io::Write::write_all(&mut inner.writer, b"\n"))
220 {
221 journal_log_error("FileJournalSink write failed", &e);
222 return;
223 }
224 if inner.last_flush.elapsed() >= JOURNAL_FLUSH_INTERVAL {
225 if let Err(e) = std::io::Write::flush(&mut inner.writer) {
226 journal_log_error("FileJournalSink flush failed", &e);
227 }
228 inner.last_flush = std::time::Instant::now();
229 }
230 }
231}
232
233pub fn equivalent_values(a: &Value, b: &Value) -> bool {
246 fn text(s: &str) -> String {
247 let chars: Vec<char> = s.chars().collect();
248 let digit = |c: Option<&char>| c.is_some_and(char::is_ascii_digit);
249 let mut out = String::new();
250 for (i, c) in chars.iter().enumerate() {
251 if c.is_alphanumeric() {
252 out.extend(c.to_lowercase());
253 } else if !c.is_whitespace() {
254 let before = chars[..i].iter().rev().find(|c| !c.is_whitespace());
255 let after = chars[i + 1..].iter().find(|c| !c.is_whitespace());
256 if digit(before) && digit(after) {
257 out.push(*c);
258 }
259 }
260 }
261 out
262 }
263 fn number(v: &Value) -> Option<f64> {
264 match v {
265 Value::Number(n) => n.as_f64(),
266 Value::String(s) => s.trim().parse().ok(),
267 _ => None,
268 }
269 }
270 match (a, b) {
271 (Value::String(x), Value::String(y)) => text(x) == text(y),
272 (Value::Number(_), _) | (_, Value::Number(_)) => {
273 matches!((number(a), number(b)), (Some(x), Some(y)) if x == y)
274 }
275 (Value::Array(x), Value::Array(y)) => {
276 x.len() == y.len() && x.iter().zip(y).all(|(p, q)| equivalent_values(p, q))
277 }
278 (Value::Object(x), Value::Object(y)) => {
279 x.len() == y.len()
280 && x.iter()
281 .all(|(k, v)| y.get(k).is_some_and(|w| equivalent_values(v, w)))
282 }
283 _ => a == b,
284 }
285}
286
287pub fn read_journal(path: impl AsRef<std::path::Path>) -> std::io::Result<Vec<StateMutation>> {
289 parse_journal(&std::fs::read_to_string(path)?)
290}
291
292pub fn parse_journal(data: &str) -> std::io::Result<Vec<StateMutation>> {
294 data.lines()
295 .enumerate()
296 .filter(|(_, line)| !line.trim().is_empty())
297 .map(|(n, line)| {
298 serde_json::from_str(line).map_err(|e| {
299 std::io::Error::new(
300 std::io::ErrorKind::InvalidData,
301 format!("journal line {}: {e}", n + 1),
302 )
303 })
304 })
305 .collect()
306}
307
308impl Drop for FileJournalSink {
309 fn drop(&mut self) {
310 if let Err(e) = std::io::Write::flush(&mut self.inner.lock().writer) {
311 journal_log_error("FileJournalSink final flush failed", &e);
312 }
313 }
314}
315
316#[derive(Default)]
318pub struct MemoryJournalSink {
319 entries: parking_lot::Mutex<Vec<StateMutation>>,
320}
321
322impl MemoryJournalSink {
323 pub fn new() -> Self {
325 Self::default()
326 }
327
328 pub fn entries(&self) -> Vec<StateMutation> {
330 self.entries.lock().clone()
331 }
332
333 pub fn len(&self) -> usize {
335 self.entries.lock().len()
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.entries.lock().is_empty()
341 }
342}
343
344impl JournalSink for MemoryJournalSink {
345 fn write(&self, m: &StateMutation) {
346 self.entries.lock().push(m.clone());
347 }
348}
349
350#[derive(Debug, thiserror::Error)]
352pub enum StateError {
353 #[error("failed to serialize state value for key '{key}': {source}")]
355 Serialize {
356 key: String,
358 source: serde_json::Error,
360 },
361 #[error("state value at key '{key}' is not the requested type: {source}")]
364 WrongType {
365 key: String,
367 source: serde_json::Error,
369 },
370}
371
372#[derive(Debug, Clone)]
378enum DeltaOp {
379 Put(Value),
381 Delete,
383}
384
385#[derive(Debug, Clone, serde::Serialize)]
392pub struct SlotEvidence {
393 pub key: String,
395 pub present: bool,
397 pub value: Option<Value>,
399 pub source: Option<String>,
402 pub confidence: Option<f64>,
404 pub last_sequence: Option<u64>,
407 pub last_origin: Option<StateMutationOrigin>,
409}
410
411#[derive(Debug, Clone)]
417pub struct State {
418 inner: Arc<DashMap<String, Value>>,
419 delta: Arc<DashMap<String, DeltaOp>>,
420 mutations: Arc<std::sync::Mutex<VecDeque<StateMutation>>>,
421 next_mutation_sequence: Arc<AtomicU64>,
422 mutation_capacity: usize,
423 journal_sink: JournalSinkSlot,
424 clock: ClockSlot,
425 redacted: RedactionSlot,
426 track_delta: bool,
427}
428
429impl Default for State {
430 fn default() -> Self {
431 Self::new()
432 }
433}
434
435impl State {
436 pub fn new() -> Self {
438 Self {
439 inner: Arc::new(DashMap::new()),
440 delta: Arc::new(DashMap::new()),
441 mutations: Arc::new(std::sync::Mutex::new(VecDeque::new())),
442 next_mutation_sequence: Arc::new(AtomicU64::new(1)),
443 mutation_capacity: DEFAULT_MUTATION_JOURNAL_CAPACITY,
444 journal_sink: JournalSinkSlot::default(),
445 clock: ClockSlot::default(),
446 redacted: RedactionSlot::default(),
447 track_delta: false,
448 }
449 }
450
451 pub fn with_delta_tracking(&self) -> State {
454 State {
455 inner: self.inner.clone(),
456 delta: Arc::new(DashMap::new()),
457 mutations: self.mutations.clone(),
458 next_mutation_sequence: self.next_mutation_sequence.clone(),
459 mutation_capacity: self.mutation_capacity,
460 journal_sink: self.journal_sink.clone(),
461 clock: self.clock.clone(),
462 redacted: self.redacted.clone(),
463 track_delta: true,
464 }
465 }
466
467 pub fn set_journal_sink(&self, sink: Arc<dyn JournalSink>) {
475 *self.journal_sink.0.write() = Some(sink);
476 }
477
478 pub fn with_journal_sink(self, sink: Arc<dyn JournalSink>) -> Self {
480 self.set_journal_sink(sink);
481 self
482 }
483
484 pub fn set_clock(&self, clock: SharedClock) {
491 *self.clock.0.write() = clock;
492 }
493
494 pub fn with_clock(self, clock: SharedClock) -> Self {
496 self.set_clock(clock);
497 self
498 }
499
500 pub fn clock(&self) -> SharedClock {
502 self.clock.0.read().clone()
503 }
504
505 pub fn redact_keys<I, S>(&self, keys: I)
516 where
517 I: IntoIterator<Item = S>,
518 S: Into<String>,
519 {
520 let mut guard = self.redacted.0.write();
521 let mut set = (**guard).clone();
522 set.extend(keys.into_iter().map(Into::into));
523 *guard = Arc::new(set);
524 }
525
526 pub fn redacted_keys(&self) -> std::collections::BTreeSet<String> {
528 (**self.redacted.0.read()).clone()
529 }
530
531 pub fn is_redacted(&self, key: &str) -> bool {
534 let set = self.redacted.0.read().clone();
535 if set.is_empty() {
536 return false;
537 }
538 set.contains(key)
539 || key
540 .rsplit_once(':')
541 .is_some_and(|(_, base)| set.contains(base))
542 }
543
544 pub fn redact_value(&self, key: &str, value: &Value) -> Value {
546 if self.is_redacted(key) {
547 Value::String(REDACTED.into())
548 } else {
549 value.clone()
550 }
551 }
552
553 pub fn redact_fields(&self, value: &Value) -> Value {
556 match value {
557 Value::Object(obj) if !self.redacted.0.read().is_empty() => Value::Object(
558 obj.iter()
559 .map(|(k, v)| (k.clone(), self.redact_value(k, v)))
560 .collect(),
561 ),
562 other => other.clone(),
563 }
564 }
565
566 pub fn to_redacted_hashmap(&self) -> HashMap<String, Value> {
569 self.to_hashmap()
570 .into_iter()
571 .map(|(k, v)| {
572 let v = self.redact_value(&k, &v);
573 (k, v)
574 })
575 .collect()
576 }
577
578 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
585 self.get_raw(key)
586 .and_then(|v| serde_json::from_value(v).ok())
587 }
588
589 pub fn try_get<T: serde::de::DeserializeOwned>(
599 &self,
600 key: &str,
601 ) -> Result<Option<T>, StateError> {
602 match self.get_raw(key) {
603 None => Ok(None),
604 Some(v) => {
605 serde_json::from_value(v)
606 .map(Some)
607 .map_err(|source| StateError::WrongType {
608 key: key.to_string(),
609 source,
610 })
611 }
612 }
613 }
614
615 pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
623 where
624 F: FnOnce(&Value) -> R,
625 {
626 if self.track_delta {
627 match self.delta.get(key).map(|r| r.value().clone()) {
628 Some(DeltaOp::Put(v)) => return Some(f(&v)),
629 Some(DeltaOp::Delete) => return None, None => {}
631 }
632 }
633 if let Some(ref_multi) = self.inner.get(key) {
634 return Some(f(ref_multi.value()));
635 }
636 if !key.contains(':') {
637 let mut derived_key = String::with_capacity(8 + key.len());
638 use std::fmt::Write;
639 let _ = write!(derived_key, "derived:{key}");
640 if self.track_delta {
641 match self.delta.get(&derived_key).map(|r| r.value().clone()) {
642 Some(DeltaOp::Put(v)) => return Some(f(&v)),
643 Some(DeltaOp::Delete) => return None,
644 None => {}
645 }
646 }
647 if let Some(ref_multi) = self.inner.get(&derived_key) {
648 return Some(f(ref_multi.value()));
649 }
650 }
651 None
652 }
653
654 pub fn get_raw(&self, key: &str) -> Option<Value> {
659 if self.track_delta {
660 match self.delta.get(key).map(|r| r.value().clone()) {
661 Some(DeltaOp::Put(v)) => return Some(v),
662 Some(DeltaOp::Delete) => return None, None => {}
664 }
665 }
666 if let Some(v) = self.inner.get(key) {
667 return Some(v.value().clone());
668 }
669 if !key.contains(':') {
671 use std::fmt::Write;
672 let mut derived_key = String::with_capacity(8 + key.len());
673 let _ = write!(derived_key, "derived:{key}");
674 if self.track_delta {
675 match self.delta.get(&derived_key).map(|r| r.value().clone()) {
676 Some(DeltaOp::Put(v)) => return Some(v),
677 Some(DeltaOp::Delete) => return None,
678 None => {}
679 }
680 }
681 return self.inner.get(&derived_key).map(|v| v.value().clone());
682 }
683 None
684 }
685
686 pub fn get_key<T: serde::de::DeserializeOwned>(&self, key: &StateKey<T>) -> Option<T> {
689 self.get(key.key())
690 }
691
692 pub fn try_get_key<T: serde::de::DeserializeOwned>(
695 &self,
696 key: &StateKey<T>,
697 ) -> Result<Option<T>, StateError> {
698 self.try_get(key.key())
699 }
700
701 pub fn set_key<T: serde::Serialize>(
705 &self,
706 key: &StateKey<T>,
707 value: T,
708 ) -> Result<(), StateError> {
709 self.set(key.key(), value)
710 }
711
712 pub fn with_key<T, F, R>(&self, key: &StateKey<T>, f: F) -> Option<R>
714 where
715 F: FnOnce(&Value) -> R,
716 {
717 self.with(key.key(), f)
718 }
719
720 pub fn set(
726 &self,
727 key: impl Into<String>,
728 value: impl serde::Serialize,
729 ) -> Result<(), StateError> {
730 let key = key.into();
731 let v = serde_json::to_value(value).map_err(|source| StateError::Serialize {
732 key: key.clone(),
733 source,
734 })?;
735 self.put_value(key, v, StateMutationOrigin::Set);
736 Ok(())
737 }
738
739 fn put_value(&self, key: String, v: Value, origin: StateMutationOrigin) {
744 let old = self.get_raw(&key);
745 if self.track_delta {
746 self.delta.insert(key.clone(), DeltaOp::Put(v.clone()));
747 } else {
748 self.inner.insert(key.clone(), v.clone());
749 }
750 self.record_mutation(key, old, Some(v), origin);
751 }
752
753 pub fn set_committed(
757 &self,
758 key: impl Into<String>,
759 value: impl serde::Serialize,
760 ) -> Result<(), StateError> {
761 let key = key.into();
762 let v = serde_json::to_value(value).map_err(|source| StateError::Serialize {
763 key: key.clone(),
764 source,
765 })?;
766 let old = self.inner.insert(key.clone(), v.clone());
767 self.record_mutation(key, old, Some(v), StateMutationOrigin::SetCommitted);
768 Ok(())
769 }
770
771 pub fn modify<T, F>(&self, key: &str, default: T, f: F) -> Result<T, StateError>
779 where
780 T: serde::Serialize + serde::de::DeserializeOwned,
781 F: FnOnce(T) -> T,
782 {
783 use dashmap::mapref::entry::Entry;
784
785 let serialize = |key: &str, val: &T| {
786 serde_json::to_value(val).map_err(|source| StateError::Serialize {
787 key: key.to_string(),
788 source,
789 })
790 };
791
792 if self.track_delta {
793 match self.delta.entry(key.to_string()) {
796 Entry::Occupied(mut o) => {
797 let current = match o.get() {
798 DeltaOp::Put(v) => serde_json::from_value(v.clone()).unwrap_or(default),
799 DeltaOp::Delete => default,
800 };
801 let old = self.inner.get(key).map(|r| r.value().clone());
802 let new_val = f(current);
803 let v = serialize(key, &new_val)?;
804 o.insert(DeltaOp::Put(v.clone()));
805 self.record_mutation(key.to_string(), old, Some(v), StateMutationOrigin::Set);
806 Ok(new_val)
807 }
808 Entry::Vacant(slot) => {
809 let base = self
810 .inner
811 .get(key)
812 .and_then(|r| serde_json::from_value(r.value().clone()).ok());
813 let old = self.inner.get(key).map(|r| r.value().clone());
814 let new_val = f(base.unwrap_or(default));
815 let v = serialize(key, &new_val)?;
816 slot.insert(DeltaOp::Put(v.clone()));
817 self.record_mutation(key.to_string(), old, Some(v), StateMutationOrigin::Set);
818 Ok(new_val)
819 }
820 }
821 } else {
822 match self.inner.entry(key.to_string()) {
823 Entry::Occupied(mut o) => {
824 let old = o.get().clone();
825 let current = serde_json::from_value(old.clone()).unwrap_or(default);
826 let new_val = f(current);
827 let v = serialize(key, &new_val)?;
828 o.insert(v.clone());
829 self.record_mutation(
830 key.to_string(),
831 Some(old),
832 Some(v),
833 StateMutationOrigin::Set,
834 );
835 Ok(new_val)
836 }
837 Entry::Vacant(slot) => {
838 let new_val = f(default);
839 let v = serialize(key, &new_val)?;
840 slot.insert(v.clone());
841 self.record_mutation(key.to_string(), None, Some(v), StateMutationOrigin::Set);
842 Ok(new_val)
843 }
844 }
845 }
846 }
847
848 pub fn contains(&self, key: &str) -> bool {
856 if self.track_delta {
857 match self.delta.get(key).map(|r| r.value().clone()) {
858 Some(DeltaOp::Put(_)) => return true,
859 Some(DeltaOp::Delete) => return false, None => {}
861 }
862 }
863 if self.inner.contains_key(key) {
864 return true;
865 }
866 if !key.contains(':') {
867 let derived_key = format!("derived:{key}");
868 if self.track_delta {
869 match self.delta.get(&derived_key).map(|r| r.value().clone()) {
870 Some(DeltaOp::Put(_)) => return true,
871 Some(DeltaOp::Delete) => return false,
872 None => {}
873 }
874 }
875 return self.inner.contains_key(&derived_key);
876 }
877 false
878 }
879
880 pub fn remove(&self, key: &str) -> Option<Value> {
886 if self.track_delta {
887 let removed = self.get_raw(key);
888 self.delta.insert(key.to_string(), DeltaOp::Delete);
891 if let Some(ref old) = removed {
892 self.record_mutation(
893 key.to_string(),
894 Some(old.clone()),
895 None,
896 StateMutationOrigin::Remove,
897 );
898 }
899 removed
900 } else {
901 let removed = self.inner.remove(key).map(|(_, v)| v);
902 if let Some(ref old) = removed {
903 self.record_mutation(
904 key.to_string(),
905 Some(old.clone()),
906 None,
907 StateMutationOrigin::Remove,
908 );
909 }
910 removed
911 }
912 }
913
914 pub fn keys(&self) -> Vec<String> {
918 if !self.track_delta || self.delta.is_empty() {
919 return self.inner.iter().map(|r| r.key().clone()).collect();
920 }
921 let mut seen =
922 std::collections::HashSet::with_capacity(self.inner.len() + self.delta.len());
923 let mut keys = Vec::with_capacity(self.inner.len() + self.delta.len());
924 for entry in self.delta.iter() {
926 let key = entry.key().clone();
927 seen.insert(key.clone());
928 if matches!(entry.value(), DeltaOp::Put(_)) {
929 keys.push(key);
930 }
931 }
932 for entry in self.inner.iter() {
933 let key = entry.key().clone();
934 if seen.insert(key.clone()) {
935 keys.push(key);
936 }
937 }
938 keys
939 }
940
941 pub fn pick(&self, keys: &[&str]) -> State {
943 let new = State::new().with_clock(self.clock());
944 new.redact_keys(self.redacted_keys());
945 for key in keys {
946 if let Some(v) = self.get_raw(key) {
947 new.put_value((*key).to_string(), v, StateMutationOrigin::Set);
948 }
949 }
950 new
951 }
952
953 pub fn merge(&self, other: &State) {
955 for entry in other.inner.iter() {
956 self.put_value(
957 entry.key().clone(),
958 entry.value().clone(),
959 StateMutationOrigin::Set,
960 );
961 }
962 }
963
964 pub fn rename(&self, from: &str, to: &str) {
966 if let Some(v) = self.remove(from) {
967 self.put_value(to.to_string(), v, StateMutationOrigin::Set);
968 }
969 }
970
971 pub fn is_tracking_delta(&self) -> bool {
975 self.track_delta
976 }
977
978 pub fn has_delta(&self) -> bool {
980 self.track_delta && !self.delta.is_empty()
981 }
982
983 pub fn delta(&self) -> HashMap<String, Value> {
985 self.delta
986 .iter()
987 .filter_map(|entry| match entry.value() {
988 DeltaOp::Put(v) => Some((entry.key().clone(), v.clone())),
989 DeltaOp::Delete => None,
990 })
991 .collect()
992 }
993
994 pub fn commit(&self) {
999 let ops: Vec<(String, DeltaOp)> = self
1001 .delta
1002 .iter()
1003 .map(|e| (e.key().clone(), e.value().clone()))
1004 .collect();
1005 for (key, op) in ops {
1006 match op {
1007 DeltaOp::Put(value) => {
1008 let old = self.inner.insert(key.clone(), value.clone());
1009 self.record_mutation_with_delta(
1010 key,
1011 old,
1012 Some(value),
1013 StateMutationOrigin::Commit,
1014 false,
1015 );
1016 }
1017 DeltaOp::Delete => {
1018 if let Some((_, old)) = self.inner.remove(&key) {
1019 self.record_mutation_with_delta(
1020 key,
1021 Some(old),
1022 None,
1023 StateMutationOrigin::Commit,
1024 false,
1025 );
1026 }
1027 }
1028 }
1029 }
1030 self.delta.clear();
1031 }
1032
1033 pub fn rollback(&self) {
1039 self.delta.clear();
1040 }
1041
1042 pub fn app(&self) -> PrefixedState<'_> {
1046 PrefixedState {
1047 state: self,
1048 prefix: "app:",
1049 }
1050 }
1051
1052 pub fn user(&self) -> PrefixedState<'_> {
1054 PrefixedState {
1055 state: self,
1056 prefix: "user:",
1057 }
1058 }
1059
1060 pub fn temp(&self) -> PrefixedState<'_> {
1062 PrefixedState {
1063 state: self,
1064 prefix: "temp:",
1065 }
1066 }
1067
1068 pub fn session(&self) -> PrefixedState<'_> {
1070 PrefixedState {
1071 state: self,
1072 prefix: "session:",
1073 }
1074 }
1075
1076 pub fn turn(&self) -> PrefixedState<'_> {
1078 PrefixedState {
1079 state: self,
1080 prefix: "turn:",
1081 }
1082 }
1083
1084 pub fn bg(&self) -> PrefixedState<'_> {
1086 PrefixedState {
1087 state: self,
1088 prefix: "bg:",
1089 }
1090 }
1091
1092 pub fn derived(&self) -> ReadOnlyPrefixedState<'_> {
1094 ReadOnlyPrefixedState {
1095 state: self,
1096 prefix: "derived:",
1097 }
1098 }
1099
1100 pub fn snapshot_values(&self, keys: &[&str]) -> HashMap<String, Value> {
1105 keys.iter()
1106 .filter_map(|&k| self.get_raw(k).map(|v| (k.to_string(), v)))
1107 .collect()
1108 }
1109
1110 pub fn diff_values(
1113 &self,
1114 prev: &HashMap<String, Value>,
1115 keys: &[&str],
1116 ) -> Vec<(String, Value, Value)> {
1117 keys.iter()
1118 .filter_map(|&k| {
1119 let old = prev.get(k);
1120 let new = self.get_raw(k);
1121 match (old, new) {
1122 (Some(o), Some(n)) if o != &n => Some((k.to_string(), o.clone(), n)),
1123 (None, Some(n)) => Some((k.to_string(), Value::Null, n)),
1124 (Some(o), None) => Some((k.to_string(), o.clone(), Value::Null)),
1125 _ => None,
1126 }
1127 })
1128 .collect()
1129 }
1130
1131 pub fn to_hashmap(&self) -> std::collections::HashMap<String, serde_json::Value> {
1133 self.inner
1134 .iter()
1135 .map(|entry| (entry.key().clone(), entry.value().clone()))
1136 .collect()
1137 }
1138
1139 pub fn from_hashmap(&self, map: std::collections::HashMap<String, serde_json::Value>) {
1141 for (key, value) in map {
1142 let old = self.inner.insert(key.clone(), value.clone());
1144 self.record_mutation(key, old, Some(value), StateMutationOrigin::SetCommitted);
1145 }
1146 }
1147
1148 pub fn clear_prefix(&self, prefix: &str) {
1154 if self.track_delta {
1155 let keys: Vec<String> = self
1156 .keys()
1157 .into_iter()
1158 .filter(|k| k.starts_with(prefix))
1159 .collect();
1160 for key in keys {
1161 let old = self.get_raw(&key);
1162 self.delta.insert(key.clone(), DeltaOp::Delete);
1163 if let Some(old) = old {
1164 self.record_mutation(key, Some(old), None, StateMutationOrigin::ClearPrefix);
1165 }
1166 }
1167 return;
1168 }
1169 let keys_to_remove: Vec<String> = self
1170 .inner
1171 .iter()
1172 .filter(|entry| entry.key().starts_with(prefix))
1173 .map(|entry| entry.key().clone())
1174 .collect();
1175 for key in keys_to_remove {
1176 if let Some((_, old)) = self.inner.remove(&key) {
1177 self.record_mutation(key, Some(old), None, StateMutationOrigin::ClearPrefix);
1178 }
1179 }
1180 }
1181
1182 pub fn recent_mutations(&self) -> Vec<StateMutation> {
1184 self.mutations
1185 .lock()
1186 .expect("state mutation journal poisoned")
1187 .iter()
1188 .cloned()
1189 .collect()
1190 }
1191
1192 pub fn mutation_cursor(&self) -> u64 {
1194 self.next_mutation_sequence.load(Ordering::Relaxed) - 1
1195 }
1196
1197 pub fn mutations_since(&self, cursor: u64) -> Vec<StateMutation> {
1199 let mutations = self
1200 .mutations
1201 .lock()
1202 .expect("state mutation journal poisoned");
1203 mutations
1204 .iter()
1205 .filter(|mutation| mutation.sequence > cursor)
1206 .cloned()
1207 .collect()
1208 }
1209
1210 pub fn try_mutations_since(&self, cursor: u64) -> Option<Vec<StateMutation>> {
1217 let mutations = self
1218 .mutations
1219 .lock()
1220 .expect("state mutation journal poisoned");
1221 let latest = self.next_mutation_sequence.load(Ordering::Relaxed) - 1;
1222 if latest > cursor {
1223 let oldest = mutations.front().map_or(u64::MAX, |m| m.sequence);
1224 if oldest > cursor + 1 {
1225 return None;
1226 }
1227 }
1228 Some(
1229 mutations
1230 .iter()
1231 .filter(|mutation| mutation.sequence > cursor)
1232 .cloned()
1233 .collect(),
1234 )
1235 }
1236
1237 pub fn drain_mutations(&self) -> Vec<StateMutation> {
1239 self.mutations
1240 .lock()
1241 .expect("state mutation journal poisoned")
1242 .drain(..)
1243 .collect()
1244 }
1245
1246 pub fn evidence(&self, key: &str) -> SlotEvidence {
1249 let value = self.get_raw(key);
1250 let meta = self.get::<Value>(&format!("state_meta:{key}"));
1251 let source = meta
1252 .as_ref()
1253 .and_then(|m| m.get("source"))
1254 .and_then(|s| s.as_str().map(String::from));
1255 let confidence = meta
1256 .as_ref()
1257 .and_then(|m| m.get("confidence"))
1258 .and_then(serde_json::Value::as_f64);
1259
1260 let mut last_sequence: Option<u64> = None;
1261 let mut last_origin: Option<StateMutationOrigin> = None;
1262 for m in self.recent_mutations() {
1263 if m.key == key && last_sequence.is_none_or(|s| m.sequence > s) {
1264 last_sequence = Some(m.sequence);
1265 last_origin = Some(m.origin);
1266 }
1267 }
1268
1269 SlotEvidence {
1270 key: key.to_string(),
1271 present: value.is_some(),
1272 value,
1273 source,
1274 confidence,
1275 last_sequence,
1276 last_origin,
1277 }
1278 }
1279
1280 fn record_mutation(
1281 &self,
1282 key: String,
1283 old: Option<Value>,
1284 new: Option<Value>,
1285 origin: StateMutationOrigin,
1286 ) {
1287 self.record_mutation_with_delta(key, old, new, origin, self.track_delta);
1288 }
1289
1290 fn record_mutation_with_delta(
1291 &self,
1292 key: String,
1293 old: Option<Value>,
1294 new: Option<Value>,
1295 origin: StateMutationOrigin,
1296 delta: bool,
1297 ) {
1298 let mut mutations = self
1299 .mutations
1300 .lock()
1301 .expect("state mutation journal poisoned");
1302 if mutations.len() >= self.mutation_capacity {
1303 mutations.pop_front();
1304 }
1305 let sequence = self.next_mutation_sequence.fetch_add(1, Ordering::Relaxed);
1306 let mutation = StateMutation {
1307 sequence,
1308 key,
1309 old,
1310 new,
1311 origin,
1312 timestamp: self.clock().system_time(),
1313 delta,
1314 };
1315 if let Some(sink) = self.journal_sink.0.read().as_ref() {
1318 if self.is_redacted(&mutation.key) {
1319 let masked = |v: &Option<Value>| v.as_ref().map(|_| Value::String(REDACTED.into()));
1320 sink.write(&StateMutation {
1321 old: masked(&mutation.old),
1322 new: masked(&mutation.new),
1323 ..mutation.clone()
1324 });
1325 } else {
1326 sink.write(&mutation);
1327 }
1328 }
1329 mutations.push_back(mutation);
1330 }
1331}
1332
1333pub struct PrefixedState<'a> {
1335 state: &'a State,
1336 prefix: &'static str,
1337}
1338
1339impl<'a> PrefixedState<'a> {
1340 fn prefixed_key(&self, key: &str) -> String {
1341 format!("{}{}", self.prefix, key)
1342 }
1343
1344 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
1346 self.state.get(&self.prefixed_key(key))
1347 }
1348
1349 pub fn get_raw(&self, key: &str) -> Option<Value> {
1351 self.state.get_raw(&self.prefixed_key(key))
1352 }
1353
1354 pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
1356 where
1357 F: FnOnce(&Value) -> R,
1358 {
1359 self.state.with(&self.prefixed_key(key), f)
1360 }
1361
1362 pub fn set(
1366 &self,
1367 key: impl AsRef<str>,
1368 value: impl serde::Serialize,
1369 ) -> Result<(), StateError> {
1370 self.state.set(self.prefixed_key(key.as_ref()), value)
1371 }
1372
1373 pub fn contains(&self, key: &str) -> bool {
1375 self.state.contains(&self.prefixed_key(key))
1376 }
1377
1378 pub fn remove(&self, key: &str) -> Option<Value> {
1380 self.state.remove(&self.prefixed_key(key))
1381 }
1382
1383 pub fn keys(&self) -> Vec<String> {
1385 self.state
1386 .keys()
1387 .into_iter()
1388 .filter_map(|k| {
1389 k.strip_prefix(self.prefix)
1390 .map(std::string::ToString::to_string)
1391 })
1392 .collect()
1393 }
1394}
1395
1396pub struct ReadOnlyPrefixedState<'a> {
1401 state: &'a State,
1402 prefix: &'static str,
1403}
1404
1405impl<'a> ReadOnlyPrefixedState<'a> {
1406 fn prefixed_key(&self, key: &str) -> String {
1407 format!("{}{}", self.prefix, key)
1408 }
1409
1410 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
1412 self.state.get(&self.prefixed_key(key))
1413 }
1414
1415 pub fn get_raw(&self, key: &str) -> Option<Value> {
1417 self.state.get_raw(&self.prefixed_key(key))
1418 }
1419
1420 pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
1422 where
1423 F: FnOnce(&Value) -> R,
1424 {
1425 self.state.with(&self.prefixed_key(key), f)
1426 }
1427
1428 pub fn contains(&self, key: &str) -> bool {
1430 self.state.contains(&self.prefixed_key(key))
1431 }
1432
1433 pub fn keys(&self) -> Vec<String> {
1435 self.state
1436 .keys()
1437 .into_iter()
1438 .filter_map(|k| {
1439 k.strip_prefix(self.prefix)
1440 .map(std::string::ToString::to_string)
1441 })
1442 .collect()
1443 }
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448 use super::*;
1449
1450 #[test]
1451 fn equivalent_values_ignore_wording_noise() {
1452 use serde_json::json;
1453 assert!(equivalent_values(
1454 &json!("tomorrow at 7 pm"),
1455 &json!("Tomorrow at 7 PM.")
1456 ));
1457 assert!(equivalent_values(&json!(4), &json!(4.0)));
1458 assert!(equivalent_values(&json!(4), &json!("4")));
1459 assert!(equivalent_values(
1460 &json!({ "verified": true, "name": "Rossi" }),
1461 &json!({ "name": "rossi", "verified": true })
1462 ));
1463 assert!(!equivalent_values(&json!("7 pm"), &json!("8 pm")));
1464 assert!(!equivalent_values(&json!("1.0 mg"), &json!("10 mg")));
1466 assert!(!equivalent_values(&json!("10:30"), &json!("1030")));
1467 assert!(!equivalent_values(&json!("2026-10-20"), &json!("20261020")));
1468 assert!(equivalent_values(&json!("1.0 mg"), &json!("1.0 MG.")));
1469 assert!(equivalent_values(&json!("Rossi!"), &json!("rossi")));
1470 assert!(!equivalent_values(&json!(4), &json!(5)));
1471 assert!(!equivalent_values(&json!(true), &json!(false)));
1472 assert!(!equivalent_values(&json!("4"), &json!(true)));
1473 }
1474
1475 #[test]
1476 fn journal_sink_receives_every_mutation_in_ring_order() {
1477 let state = State::new();
1478 let sink = Arc::new(MemoryJournalSink::new());
1479 state.set_journal_sink(sink.clone());
1480
1481 let _ = state.set("a", 1);
1482 let _ = state.set("b", "two");
1483 state.remove("a");
1484
1485 let entries = sink.entries();
1486 assert_eq!(entries.len(), 3);
1487 assert_eq!(entries, state.recent_mutations());
1488 assert_eq!(entries[0].key, "a");
1489 assert_eq!(entries[2].origin, StateMutationOrigin::Remove);
1490 }
1491
1492 #[test]
1493 fn journal_sink_is_shared_with_clones_and_delta_views() {
1494 let state = State::new();
1495 let sink = Arc::new(MemoryJournalSink::new());
1496 state.set_journal_sink(sink.clone());
1497
1498 let clone = state.clone();
1499 let _ = clone.set("from_clone", true);
1500
1501 let tracked = state.with_delta_tracking();
1502 let _ = tracked.set("from_delta", 1);
1503 tracked.commit();
1504
1505 let keys: Vec<_> = sink.entries().iter().map(|m| m.key.clone()).collect();
1506 assert!(keys.contains(&"from_clone".to_string()));
1507 assert!(keys.contains(&"from_delta".to_string()));
1508 assert!(
1510 sink.entries()
1511 .iter()
1512 .any(|m| m.origin == StateMutationOrigin::Commit)
1513 );
1514 }
1515
1516 #[test]
1517 fn journal_sink_outlives_ring_capacity() {
1518 let state = State::new();
1520 let sink = Arc::new(MemoryJournalSink::new());
1521 state.set_journal_sink(sink.clone());
1522
1523 for i in 0..(DEFAULT_MUTATION_JOURNAL_CAPACITY + 10) {
1524 let _ = state.set(format!("k{i}"), i);
1525 }
1526
1527 assert_eq!(
1528 state.recent_mutations().len(),
1529 DEFAULT_MUTATION_JOURNAL_CAPACITY
1530 );
1531 assert_eq!(sink.len(), DEFAULT_MUTATION_JOURNAL_CAPACITY + 10);
1532 assert_eq!(sink.entries()[0].key, "k0");
1533 }
1534
1535 #[test]
1536 fn state_mutation_serde_round_trip_uses_epoch_millis() {
1537 let m = StateMutation {
1538 sequence: 42,
1539 key: "app:last_city".into(),
1540 old: None,
1541 new: Some(serde_json::json!("London")),
1542 origin: StateMutationOrigin::Set,
1543 timestamp: std::time::UNIX_EPOCH + std::time::Duration::from_millis(1_718_000_000_123),
1544 delta: false,
1545 };
1546 let json = serde_json::to_string(&m).unwrap();
1547 assert!(json.contains("\"timestamp_ms\":1718000000123"));
1548 assert!(json.contains("\"origin\":\"set\""));
1549 let back: StateMutation = serde_json::from_str(&json).unwrap();
1550 assert_eq!(back, m);
1551 }
1552
1553 #[test]
1554 fn file_journal_sink_round_trip() {
1555 let dir = std::env::temp_dir().join(format!(
1556 "gemini-rs-journal-test-{}-{}",
1557 std::process::id(),
1558 std::time::SystemTime::now()
1559 .duration_since(std::time::UNIX_EPOCH)
1560 .unwrap()
1561 .as_nanos()
1562 ));
1563 std::fs::create_dir_all(&dir).unwrap();
1564 let path = dir.join("session.journal.jsonl");
1565
1566 let state = State::new();
1567 {
1568 let sink = Arc::new(FileJournalSink::create(&path).unwrap());
1569 state.set_journal_sink(sink);
1570 let _ = state.set("a", 1);
1571 let _ = state.set("a", 2);
1572 state.remove("a");
1573 state.set_journal_sink(Arc::new(MemoryJournalSink::new()));
1575 }
1576
1577 let data = std::fs::read_to_string(&path).unwrap();
1578 let parsed: Vec<StateMutation> = data
1579 .lines()
1580 .filter(|l| !l.trim().is_empty())
1581 .map(|l| serde_json::from_str(l).unwrap())
1582 .collect();
1583 assert_eq!(parsed.len(), 3);
1584 assert_eq!(parsed[0].new, Some(serde_json::json!(1)));
1585 assert_eq!(parsed[2].origin, StateMutationOrigin::Remove);
1586
1587 let _ = std::fs::remove_dir_all(&dir);
1588 }
1589
1590 #[test]
1591 fn set_and_get_string() {
1592 let state = State::new();
1593 let _ = state.set("name", "Alice");
1594 assert_eq!(state.get::<String>("name"), Some("Alice".to_string()));
1595 }
1596
1597 #[test]
1598 fn set_and_get_json() {
1599 let state = State::new();
1600 let _ = state.set("data", serde_json::json!({"temp": 22}));
1601 let v: Value = state.get("data").unwrap();
1602 assert_eq!(v["temp"], 22);
1603 }
1604
1605 #[test]
1606 fn pick_subset() {
1607 let state = State::new();
1608 let _ = state.set("a", 1);
1609 let _ = state.set("b", 2);
1610 let _ = state.set("c", 3);
1611 let picked = state.pick(&["a", "c"]);
1612 assert!(picked.contains("a"));
1613 assert!(!picked.contains("b"));
1614 assert!(picked.contains("c"));
1615 }
1616
1617 #[test]
1618 fn merge_states() {
1619 let s1 = State::new();
1620 let _ = s1.set("a", 1);
1621 let s2 = State::new();
1622 let _ = s2.set("b", 2);
1623 s1.merge(&s2);
1624 assert!(s1.contains("a"));
1625 assert!(s1.contains("b"));
1626 }
1627
1628 #[test]
1629 fn rename_key() {
1630 let state = State::new();
1631 let _ = state.set("old", "value");
1632 state.rename("old", "new");
1633 assert!(!state.contains("old"));
1634 assert_eq!(state.get::<String>("new"), Some("value".to_string()));
1635 }
1636
1637 #[test]
1638 fn remove_returns_value() {
1639 let state = State::new();
1640 let _ = state.set("key", 42);
1641 let removed = state.remove("key");
1642 assert!(removed.is_some());
1643 assert!(!state.contains("key"));
1644 }
1645
1646 #[test]
1647 fn get_missing_returns_none() {
1648 let state = State::new();
1649 assert_eq!(state.get::<String>("nope"), None);
1650 }
1651
1652 #[test]
1655 fn delta_tracking_writes_to_delta() {
1656 let state = State::new();
1657 let _ = state.set("committed", "yes");
1658
1659 let tracked = state.with_delta_tracking();
1660 let _ = tracked.set("new_key", "new_value");
1661
1662 assert_eq!(
1664 tracked.get::<String>("new_key"),
1665 Some("new_value".to_string())
1666 );
1667 assert!(!state.contains("new_key"));
1669 assert_eq!(tracked.get::<String>("committed"), Some("yes".to_string()));
1671 }
1672
1673 #[test]
1674 fn delta_has_delta_reports_correctly() {
1675 let state = State::new();
1676 let tracked = state.with_delta_tracking();
1677 assert!(!tracked.has_delta());
1678
1679 let _ = tracked.set("key", "val");
1680 assert!(tracked.has_delta());
1681 }
1682
1683 #[test]
1684 fn delta_commit_merges_to_inner() {
1685 let state = State::new();
1686 let tracked = state.with_delta_tracking();
1687 let _ = tracked.set("key", "val");
1688 assert!(!state.contains("key"));
1689
1690 tracked.commit();
1691 assert_eq!(state.get::<String>("key"), Some("val".to_string()));
1693 assert!(!tracked.has_delta());
1694 }
1695
1696 #[test]
1697 fn delta_rollback_discards_changes() {
1698 let state = State::new();
1699 let tracked = state.with_delta_tracking();
1700 let _ = tracked.set("key", "val");
1701 assert!(tracked.has_delta());
1702
1703 tracked.rollback();
1704 assert!(!tracked.has_delta());
1705 assert!(!state.contains("key"));
1706 assert!(!tracked.contains("key"));
1707 }
1708
1709 #[test]
1710 fn delta_snapshot() {
1711 let state = State::new();
1712 let tracked = state.with_delta_tracking();
1713 let _ = tracked.set("a", 1);
1714 let _ = tracked.set("b", 2);
1715
1716 let snapshot = tracked.delta();
1717 assert_eq!(snapshot.len(), 2);
1718 assert!(snapshot.contains_key("a"));
1719 assert!(snapshot.contains_key("b"));
1720 }
1721
1722 #[test]
1723 fn set_committed_bypasses_delta() {
1724 let state = State::new();
1725 let tracked = state.with_delta_tracking();
1726 let _ = tracked.set_committed("direct", "value");
1727
1728 assert_eq!(state.get::<String>("direct"), Some("value".to_string()));
1730 assert!(!tracked.has_delta());
1732 assert_eq!(tracked.get::<String>("direct"), Some("value".to_string()));
1734 }
1735
1736 #[test]
1737 fn mutation_journal_records_set_and_remove() {
1738 let state = State::new();
1739 let _ = state.set("key", "first");
1740 let _ = state.set("key", "second");
1741 state.remove("key");
1742
1743 let mutations = state.recent_mutations();
1744 assert_eq!(mutations.len(), 3);
1745 assert_eq!(mutations[0].key, "key");
1746 assert_eq!(mutations[0].old, None);
1747 assert_eq!(mutations[0].new, Some(serde_json::json!("first")));
1748 assert_eq!(mutations[0].origin, StateMutationOrigin::Set);
1749
1750 assert_eq!(mutations[1].old, Some(serde_json::json!("first")));
1751 assert_eq!(mutations[1].new, Some(serde_json::json!("second")));
1752
1753 assert_eq!(mutations[2].old, Some(serde_json::json!("second")));
1754 assert_eq!(mutations[2].new, None);
1755 assert_eq!(mutations[2].origin, StateMutationOrigin::Remove);
1756 }
1757
1758 #[test]
1759 fn mutation_journal_is_shared_with_delta_tracking() {
1760 let state = State::new();
1761 let _ = state.set("committed", "yes");
1762
1763 let tracked = state.with_delta_tracking();
1764 let _ = tracked.set("committed", "maybe");
1765 tracked.commit();
1766
1767 let mutations = state.recent_mutations();
1768 assert_eq!(mutations.len(), 3);
1769 assert_eq!(mutations[1].key, "committed");
1770 assert_eq!(mutations[1].old, Some(serde_json::json!("yes")));
1771 assert_eq!(mutations[1].new, Some(serde_json::json!("maybe")));
1772 assert_eq!(mutations[1].origin, StateMutationOrigin::Set);
1773 assert!(mutations[1].delta);
1774
1775 assert_eq!(mutations[2].origin, StateMutationOrigin::Commit);
1776 assert!(!mutations[2].delta);
1777 }
1778
1779 #[test]
1780 fn drain_mutations_clears_journal() {
1781 let state = State::new();
1782 let _ = state.set("a", 1);
1783 let _ = state.set("b", 2);
1784
1785 let drained = state.drain_mutations();
1786 assert_eq!(drained.len(), 2);
1787 assert!(state.recent_mutations().is_empty());
1788 }
1789
1790 #[test]
1791 fn redacted_keys_are_masked_wherever_state_leaves_the_process() {
1792 let sink = Arc::new(MemoryJournalSink::new());
1793 let state = State::new().with_journal_sink(sink.clone());
1794 state.redact_keys(["card_number"]);
1795 let _ = state.set("card_number", "4111111111111111");
1796 let _ = state.set("app:card_number", "4111111111111111");
1797 let _ = state.set("name", "Ada");
1798
1799 assert_eq!(
1801 state.get::<String>("card_number").as_deref(),
1802 Some("4111111111111111")
1803 );
1804 for entry in sink.entries() {
1806 let leaked = entry
1807 .new
1808 .as_ref()
1809 .is_some_and(|v| v.to_string().contains("4111"));
1810 assert!(!leaked, "{} leaked into the journal sink", entry.key);
1811 }
1812 let snapshot = state.to_redacted_hashmap();
1814 assert_eq!(snapshot["card_number"], serde_json::json!(REDACTED));
1815 assert_eq!(snapshot["app:card_number"], serde_json::json!(REDACTED));
1816 assert_eq!(snapshot["name"], serde_json::json!("Ada"));
1817 assert_eq!(
1819 state.redact_fields(&serde_json::json!({ "card_number": "4111", "name": "Ada" })),
1820 serde_json::json!({ "card_number": REDACTED, "name": "Ada" })
1821 );
1822 assert!(
1824 state
1825 .recent_mutations()
1826 .iter()
1827 .any(|m| m.new == Some(serde_json::json!("4111111111111111")))
1828 );
1829 }
1830
1831 #[test]
1832 fn try_mutations_since_reports_a_gap_the_ring_dropped() {
1833 let state = State::new();
1834 let cursor = state.mutation_cursor();
1835 let _ = state.set("a", 1);
1836 assert_eq!(state.try_mutations_since(cursor).map(|m| m.len()), Some(1));
1837
1838 state.drain_mutations();
1839 let _ = state.set("b", 2);
1840 assert!(
1841 state.try_mutations_since(cursor).is_none(),
1842 "the write to `a` is gone from the journal"
1843 );
1844 let after_drain = state.mutation_cursor() - 1;
1845 assert_eq!(
1846 state.try_mutations_since(after_drain).map(|m| m.len()),
1847 Some(1)
1848 );
1849 assert_eq!(
1850 state
1851 .try_mutations_since(state.mutation_cursor())
1852 .map(|m| m.len()),
1853 Some(0)
1854 );
1855 }
1856
1857 #[test]
1858 fn mutation_cursor_reads_only_later_changes() {
1859 let state = State::new();
1860 let _ = state.set("before", 1);
1861 let cursor = state.mutation_cursor();
1862
1863 let _ = state.set("after", 2);
1864 state.remove("before");
1865
1866 let mutations = state.mutations_since(cursor);
1867 assert_eq!(mutations.len(), 2);
1868 assert_eq!(mutations[0].key, "after");
1869 assert_eq!(mutations[1].key, "before");
1870 }
1871
1872 #[test]
1873 fn no_delta_tracking_preserves_existing_behavior() {
1874 let state = State::new();
1875 assert!(!state.is_tracking_delta());
1876 let _ = state.set("key", "val");
1877 assert_eq!(state.get::<String>("key"), Some("val".to_string()));
1878 assert!(!state.has_delta());
1879 }
1880
1881 #[test]
1884 fn prefix_app_set_and_get() {
1885 let state = State::new();
1886 let _ = state.app().set("flag", true);
1887
1888 assert_eq!(state.app().get::<bool>("flag"), Some(true));
1890 assert_eq!(state.get::<bool>("app:flag"), Some(true));
1892 }
1893
1894 #[test]
1895 fn prefix_user_set_and_get() {
1896 let state = State::new();
1897 let _ = state.user().set("name", "Alice");
1898 assert_eq!(
1899 state.user().get::<String>("name"),
1900 Some("Alice".to_string())
1901 );
1902 assert_eq!(state.get::<String>("user:name"), Some("Alice".to_string()));
1903 }
1904
1905 #[test]
1906 fn prefix_temp_set_and_get() {
1907 let state = State::new();
1908 let _ = state.temp().set("scratch", 42);
1909 assert_eq!(state.temp().get::<i32>("scratch"), Some(42));
1910 }
1911
1912 #[test]
1913 fn prefix_contains_and_remove() {
1914 let state = State::new();
1915 let _ = state.app().set("x", 1);
1916 assert!(state.app().contains("x"));
1917 state.app().remove("x");
1918 assert!(!state.app().contains("x"));
1919 }
1920
1921 #[test]
1922 fn prefix_keys() {
1923 let state = State::new();
1924 let _ = state.app().set("a", 1);
1925 let _ = state.app().set("b", 2);
1926 let _ = state.user().set("c", 3);
1927
1928 let app_keys = state.app().keys();
1929 assert_eq!(app_keys.len(), 2);
1930 assert!(app_keys.contains(&"a".to_string()));
1931 assert!(app_keys.contains(&"b".to_string()));
1932
1933 let user_keys = state.user().keys();
1934 assert_eq!(user_keys.len(), 1);
1935 assert!(user_keys.contains(&"c".to_string()));
1936 }
1937
1938 #[test]
1939 fn prefix_with_delta_tracking() {
1940 let state = State::new();
1941 let tracked = state.with_delta_tracking();
1942 let _ = tracked.app().set("flag", true);
1943
1944 assert_eq!(tracked.app().get::<bool>("flag"), Some(true));
1946 assert!(tracked.has_delta());
1948 assert!(!state.contains("app:flag"));
1949
1950 tracked.commit();
1951 assert_eq!(state.get::<bool>("app:flag"), Some(true));
1952 }
1953
1954 #[test]
1957 fn prefix_session_set_and_get() {
1958 let state = State::new();
1959 let _ = state.session().set("turn_count", 5);
1960 assert_eq!(state.session().get::<i32>("turn_count"), Some(5));
1961 assert_eq!(state.get::<i32>("session:turn_count"), Some(5));
1962 }
1963
1964 #[test]
1965 fn prefix_turn_set_and_get() {
1966 let state = State::new();
1967 let _ = state.turn().set("transcript", "hello");
1968 assert_eq!(
1969 state.turn().get::<String>("transcript"),
1970 Some("hello".to_string())
1971 );
1972 assert_eq!(
1973 state.get::<String>("turn:transcript"),
1974 Some("hello".to_string())
1975 );
1976 }
1977
1978 #[test]
1979 fn prefix_bg_set_and_get() {
1980 let state = State::new();
1981 let _ = state.bg().set("task_id", "abc-123");
1982 assert_eq!(
1983 state.bg().get::<String>("task_id"),
1984 Some("abc-123".to_string())
1985 );
1986 assert_eq!(
1987 state.get::<String>("bg:task_id"),
1988 Some("abc-123".to_string())
1989 );
1990 }
1991
1992 #[test]
1993 fn prefix_session_contains_and_remove() {
1994 let state = State::new();
1995 let _ = state.session().set("x", 1);
1996 assert!(state.session().contains("x"));
1997 state.session().remove("x");
1998 assert!(!state.session().contains("x"));
1999 }
2000
2001 #[test]
2002 fn prefix_turn_keys() {
2003 let state = State::new();
2004 let _ = state.turn().set("a", 1);
2005 let _ = state.turn().set("b", 2);
2006 let _ = state.session().set("c", 3);
2007
2008 let turn_keys = state.turn().keys();
2009 assert_eq!(turn_keys.len(), 2);
2010 assert!(turn_keys.contains(&"a".to_string()));
2011 assert!(turn_keys.contains(&"b".to_string()));
2012 }
2013
2014 #[test]
2017 fn try_get_distinguishes_absent_from_wrong_type() {
2018 let state = State::new();
2019 assert!(matches!(state.try_get::<u32>("missing"), Ok(None)));
2020
2021 state.set("n", 5u32).unwrap();
2022 assert_eq!(state.try_get::<u32>("n").unwrap(), Some(5));
2023
2024 state.set("s", "not a number").unwrap();
2025 assert_eq!(state.get::<u32>("s"), None);
2027 match state.try_get::<u32>("s") {
2029 Err(StateError::WrongType { key, .. }) => assert_eq!(key, "s"),
2030 other => panic!("expected WrongType, got {other:?}"),
2031 }
2032
2033 state.set("derived:risk", 0.5f64).unwrap();
2035 assert_eq!(state.try_get::<f64>("risk").unwrap(), Some(0.5));
2036
2037 const N: StateKey<u32> = StateKey::new("n");
2038 assert_eq!(state.try_get_key(&N).unwrap(), Some(5));
2039 const S: StateKey<u32> = StateKey::new("s");
2040 assert!(state.try_get_key(&S).is_err());
2041 }
2042
2043 #[test]
2046 fn derived_read_only_get() {
2047 let state = State::new();
2048 let _ = state.set("derived:sentiment", "positive");
2050 assert_eq!(
2051 state.derived().get::<String>("sentiment"),
2052 Some("positive".to_string())
2053 );
2054 }
2055
2056 #[test]
2057 fn derived_read_only_get_raw() {
2058 let state = State::new();
2059 let _ = state.set("derived:score", serde_json::json!(0.95));
2060 let raw = state.derived().get_raw("score");
2061 assert!(raw.is_some());
2062 assert_eq!(raw.unwrap(), serde_json::json!(0.95));
2063 }
2064
2065 #[test]
2066 fn derived_read_only_contains() {
2067 let state = State::new();
2068 let _ = state.set("derived:exists", true);
2069 assert!(state.derived().contains("exists"));
2070 assert!(!state.derived().contains("missing"));
2071 }
2072
2073 #[test]
2074 fn derived_read_only_keys() {
2075 let state = State::new();
2076 let _ = state.set("derived:a", 1);
2077 let _ = state.set("derived:b", 2);
2078 let _ = state.set("app:c", 3);
2079
2080 let derived_keys = state.derived().keys();
2081 assert_eq!(derived_keys.len(), 2);
2082 assert!(derived_keys.contains(&"a".to_string()));
2083 assert!(derived_keys.contains(&"b".to_string()));
2084 }
2085
2086 #[test]
2087 fn derived_missing_key_returns_none() {
2088 let state = State::new();
2089 assert_eq!(state.derived().get::<String>("nope"), None);
2090 assert_eq!(state.derived().get_raw("nope"), None);
2091 }
2092
2093 #[test]
2096 fn snapshot_values_captures_existing_keys() {
2097 let state = State::new();
2098 let _ = state.set("a", 1);
2099 let _ = state.set("b", "hello");
2100 let _ = state.set("c", true);
2101
2102 let snap = state.snapshot_values(&["a", "b", "missing"]);
2103 assert_eq!(snap.len(), 2);
2104 assert_eq!(snap.get("a"), Some(&serde_json::json!(1)));
2105 assert_eq!(snap.get("b"), Some(&serde_json::json!("hello")));
2106 assert!(!snap.contains_key("missing"));
2107 }
2108
2109 #[test]
2110 fn snapshot_values_empty_keys() {
2111 let state = State::new();
2112 let _ = state.set("a", 1);
2113 let snap = state.snapshot_values(&[]);
2114 assert!(snap.is_empty());
2115 }
2116
2117 #[test]
2120 fn diff_values_detects_changed_value() {
2121 let state = State::new();
2122 let _ = state.set("x", 1);
2123 let snap = state.snapshot_values(&["x"]);
2124
2125 let _ = state.set("x", 2);
2126 let diffs = state.diff_values(&snap, &["x"]);
2127 assert_eq!(diffs.len(), 1);
2128 assert_eq!(diffs[0].0, "x");
2129 assert_eq!(diffs[0].1, serde_json::json!(1));
2130 assert_eq!(diffs[0].2, serde_json::json!(2));
2131 }
2132
2133 #[test]
2134 fn diff_values_detects_new_key() {
2135 let state = State::new();
2136 let snap = state.snapshot_values(&["y"]);
2137
2138 let _ = state.set("y", "new");
2139 let diffs = state.diff_values(&snap, &["y"]);
2140 assert_eq!(diffs.len(), 1);
2141 assert_eq!(diffs[0].0, "y");
2142 assert_eq!(diffs[0].1, Value::Null);
2143 assert_eq!(diffs[0].2, serde_json::json!("new"));
2144 }
2145
2146 #[test]
2147 fn diff_values_detects_removed_key() {
2148 let state = State::new();
2149 let _ = state.set("z", 42);
2150 let snap = state.snapshot_values(&["z"]);
2151
2152 state.remove("z");
2153 let diffs = state.diff_values(&snap, &["z"]);
2154 assert_eq!(diffs.len(), 1);
2155 assert_eq!(diffs[0].0, "z");
2156 assert_eq!(diffs[0].1, serde_json::json!(42));
2157 assert_eq!(diffs[0].2, Value::Null);
2158 }
2159
2160 #[test]
2161 fn diff_values_no_change() {
2162 let state = State::new();
2163 let _ = state.set("stable", 10);
2164 let snap = state.snapshot_values(&["stable"]);
2165
2166 let diffs = state.diff_values(&snap, &["stable"]);
2168 assert!(diffs.is_empty());
2169 }
2170
2171 #[test]
2172 fn diff_values_multiple_keys_mixed_changes() {
2173 let state = State::new();
2174 let _ = state.set("a", 1);
2175 let _ = state.set("b", 2);
2176 let snap = state.snapshot_values(&["a", "b", "c"]);
2177
2178 let _ = state.set("a", 10); let _ = state.set("c", 3); let diffs = state.diff_values(&snap, &["a", "b", "c"]);
2183 assert_eq!(diffs.len(), 2); let diff_keys: Vec<&str> = diffs.iter().map(|(k, _, _)| k.as_str()).collect();
2185 assert!(diff_keys.contains(&"a"));
2186 assert!(diff_keys.contains(&"c"));
2187 }
2188
2189 #[test]
2192 fn clear_prefix_removes_matching_keys() {
2193 let state = State::new();
2194 let _ = state.set("turn:a", 1);
2195 let _ = state.set("turn:b", 2);
2196 let _ = state.set("app:c", 3);
2197 let _ = state.set("session:d", 4);
2198
2199 state.clear_prefix("turn:");
2200 assert!(!state.contains("turn:a"));
2201 assert!(!state.contains("turn:b"));
2202 assert!(state.contains("app:c"));
2203 assert!(state.contains("session:d"));
2204 }
2205
2206 #[test]
2207 fn clear_prefix_no_matching_keys_is_noop() {
2208 let state = State::new();
2209 let _ = state.set("app:x", 1);
2210 state.clear_prefix("turn:");
2211 assert!(state.contains("app:x"));
2212 }
2213
2214 #[test]
2215 fn clear_prefix_also_clears_delta() {
2216 let state = State::new();
2217 let _ = state.set("turn:committed", 1);
2218 let tracked = state.with_delta_tracking();
2219 let _ = tracked.set("turn:delta_val", 2);
2220
2221 assert!(tracked.contains("turn:committed"));
2223 assert!(tracked.contains("turn:delta_val"));
2224
2225 tracked.clear_prefix("turn:");
2226 assert!(!tracked.contains("turn:committed"));
2227 assert!(!tracked.contains("turn:delta_val"));
2228 }
2229
2230 #[test]
2231 fn clear_prefix_via_turn_accessor() {
2232 let state = State::new();
2233 let _ = state.turn().set("x", 1);
2234 let _ = state.turn().set("y", 2);
2235 let _ = state.app().set("z", 3);
2236
2237 state.clear_prefix("turn:");
2238 assert!(state.turn().keys().is_empty());
2239 assert!(state.app().contains("z"));
2240 }
2241
2242 #[test]
2245 fn modify_increment_existing() {
2246 let state = State::new();
2247 let _ = state.set("count", 5u32);
2248 let result = state.modify("count", 0u32, |n| n + 1).unwrap();
2249 assert_eq!(result, 6);
2250 assert_eq!(state.get::<u32>("count"), Some(6));
2251 }
2252
2253 #[test]
2254 fn modify_uses_default_when_missing() {
2255 let state = State::new();
2256 let result = state.modify("new_count", 0u32, |n| n + 1).unwrap();
2257 assert_eq!(result, 1);
2258 assert_eq!(state.get::<u32>("new_count"), Some(1));
2259 }
2260
2261 #[test]
2262 fn modify_with_delta_tracking() {
2263 let state = State::new();
2264 let _ = state.set("x", 10u32);
2265 let tracked = state.with_delta_tracking();
2266 let result = tracked.modify("x", 0u32, |n| n * 2).unwrap();
2267 assert_eq!(result, 20);
2268 assert_eq!(tracked.get::<u32>("x"), Some(20));
2270 assert_eq!(state.get::<u32>("x"), Some(10)); }
2272
2273 #[test]
2276 fn get_falls_back_to_derived_prefix() {
2277 let state = State::new();
2278 let _ = state.set("derived:risk", 0.85);
2279 assert_eq!(state.get::<f64>("risk"), Some(0.85));
2281 }
2282
2283 #[test]
2284 fn get_prefers_direct_key_over_derived() {
2285 let state = State::new();
2286 let _ = state.set("score", 1.0);
2287 let _ = state.set("derived:score", 0.5);
2288 assert_eq!(state.get::<f64>("score"), Some(1.0));
2290 }
2291
2292 #[test]
2293 fn get_derived_fallback_skipped_for_prefixed_keys() {
2294 let state = State::new();
2295 let _ = state.set("derived:risk", 0.85);
2296 assert_eq!(state.get::<f64>("app:risk"), None);
2298 }
2299
2300 #[test]
2301 fn get_derived_fallback_with_delta_tracking() {
2302 let state = State::new();
2303 let tracked = state.with_delta_tracking();
2304 let _ = tracked.set("derived:computed_val", 42);
2305 assert_eq!(tracked.get::<i32>("computed_val"), Some(42));
2306 }
2307
2308 #[test]
2311 fn with_reads_from_inner() {
2312 let state = State::new();
2313 let _ = state.set("name", "Alice");
2314 let len = state.with("name", |v| v.as_str().unwrap().len());
2315 assert_eq!(len, Some(5));
2316 }
2317
2318 #[test]
2319 fn with_reads_from_delta_first() {
2320 let state = State::new();
2321 let _ = state.set("x", 1);
2322 let tracked = state.with_delta_tracking();
2323 let _ = tracked.set("x", 99);
2324 let val = tracked.with("x", |v| v.as_i64().unwrap());
2325 assert_eq!(val, Some(99));
2326 }
2327
2328 #[test]
2329 fn with_falls_back_to_inner_when_not_in_delta() {
2330 let state = State::new();
2331 let _ = state.set("committed", "yes");
2332 let tracked = state.with_delta_tracking();
2333 let val = tracked.with("committed", |v| v.as_str().unwrap().to_string());
2334 assert_eq!(val, Some("yes".to_string()));
2335 }
2336
2337 #[test]
2338 fn with_falls_back_to_derived() {
2339 let state = State::new();
2340 let _ = state.set("derived:risk", 0.85);
2341 let val = state.with("risk", |v| v.as_f64().unwrap());
2342 assert_eq!(val, Some(0.85));
2343 }
2344
2345 #[test]
2346 fn with_derived_fallback_skipped_for_prefixed() {
2347 let state = State::new();
2348 let _ = state.set("derived:risk", 0.85);
2349 let val = state.with("app:risk", |v| v.as_f64().unwrap());
2350 assert_eq!(val, None);
2351 }
2352
2353 #[test]
2354 fn with_returns_none_for_missing() {
2355 let state = State::new();
2356 let val = state.with("missing", std::clone::Clone::clone);
2357 assert_eq!(val, None);
2358 }
2359
2360 #[test]
2361 fn with_on_prefixed_state() {
2362 let state = State::new();
2363 let _ = state.app().set("flag", true);
2364 let val = state.app().with("flag", |v| v.as_bool().unwrap());
2365 assert_eq!(val, Some(true));
2366 }
2367
2368 #[test]
2369 fn with_on_read_only_prefixed_state() {
2370 let state = State::new();
2371 let _ = state.set("derived:score", serde_json::json!(0.95));
2372 let val = state.derived().with("score", |v| v.as_f64().unwrap());
2373 assert_eq!(val, Some(0.95));
2374 }
2375
2376 const TURN_COUNT: StateKey<u32> = StateKey::new("session:turn_count");
2379 const NAME: StateKey<String> = StateKey::new("user:name");
2380
2381 #[test]
2382 fn state_key_get_and_set() {
2383 let state = State::new();
2384 let _ = state.set_key(&TURN_COUNT, 5);
2385 assert_eq!(state.get_key(&TURN_COUNT), Some(5));
2386 }
2387
2388 #[test]
2389 fn state_key_get_missing() {
2390 let state = State::new();
2391 assert_eq!(state.get_key(&TURN_COUNT), None);
2392 }
2393
2394 #[test]
2395 fn state_key_string_type() {
2396 let state = State::new();
2397 let _ = state.set_key(&NAME, "Alice".to_string());
2398 assert_eq!(state.get_key(&NAME), Some("Alice".to_string()));
2399 }
2400
2401 #[test]
2402 fn state_key_with() {
2403 let state = State::new();
2404 let _ = state.set_key(&TURN_COUNT, 42);
2405 let val = state.with_key(&TURN_COUNT, |v| v.as_u64().unwrap());
2406 assert_eq!(val, Some(42));
2407 }
2408
2409 #[test]
2410 fn state_key_interop_with_raw() {
2411 let state = State::new();
2412 let _ = state.set_key(&TURN_COUNT, 10);
2413 assert_eq!(state.get::<u32>("session:turn_count"), Some(10));
2415 }
2416
2417 #[test]
2418 fn slot_evidence_aggregates_value_provenance_and_journal() {
2419 let state = State::new();
2420 let _ = state.set("party_size", 6u8);
2421 let _ = state.set(
2423 "state_meta:party_size",
2424 serde_json::json!({ "source": "extraction", "confidence": 0.9 }),
2425 );
2426
2427 let ev = state.evidence("party_size");
2428 assert!(ev.present);
2429 assert_eq!(ev.value, Some(serde_json::json!(6)));
2430 assert_eq!(ev.source.as_deref(), Some("extraction"));
2431 assert_eq!(ev.confidence, Some(0.9));
2432 assert!(ev.last_sequence.is_some());
2433 assert_eq!(ev.last_origin, Some(StateMutationOrigin::Set));
2434
2435 let missing = state.evidence("nope");
2437 assert!(!missing.present);
2438 assert!(missing.source.is_none());
2439 }
2440
2441 #[test]
2444 fn rollback_restores_base_after_remove() {
2445 let base = State::new();
2448 let _ = base.set("k", "original");
2449
2450 let tx = base.with_delta_tracking();
2451 assert_eq!(tx.remove("k"), Some(serde_json::json!("original")));
2452 assert_eq!(tx.get::<String>("k"), None); assert_eq!(base.get::<String>("k"), Some("original".into())); tx.rollback();
2456 assert_eq!(tx.get::<String>("k"), Some("original".into()));
2457 assert_eq!(base.get::<String>("k"), Some("original".into()));
2458 }
2459
2460 #[test]
2461 fn rollback_restores_base_after_clear_prefix() {
2462 let base = State::new();
2464 let _ = base.set("app:a", 1u32);
2465 let _ = base.set("app:b", 2u32);
2466 let _ = base.set("user:c", 3u32);
2467
2468 let tx = base.with_delta_tracking();
2469 tx.clear_prefix("app:");
2470 assert_eq!(tx.get::<u32>("app:a"), None);
2471 assert_eq!(tx.get::<u32>("app:b"), None);
2472 assert_eq!(tx.get::<u32>("user:c"), Some(3));
2473 assert_eq!(base.get::<u32>("app:a"), Some(1));
2475
2476 tx.rollback();
2477 assert_eq!(tx.get::<u32>("app:a"), Some(1));
2478 assert_eq!(tx.get::<u32>("app:b"), Some(2));
2479 }
2480
2481 #[test]
2482 fn commit_applies_removals() {
2483 let base = State::new();
2484 let _ = base.set("k", "v");
2485 let tx = base.with_delta_tracking();
2486 tx.remove("k");
2487 tx.commit();
2488 assert_eq!(base.get::<String>("k"), None);
2489 }
2490
2491 #[test]
2492 fn commit_applies_prefix_clear() {
2493 let base = State::new();
2494 let _ = base.set("app:a", 1u32);
2495 let _ = base.set("user:c", 3u32);
2496 let tx = base.with_delta_tracking();
2497 tx.clear_prefix("app:");
2498 tx.commit();
2499 assert_eq!(base.get::<u32>("app:a"), None);
2500 assert_eq!(base.get::<u32>("user:c"), Some(3));
2501 }
2502
2503 #[test]
2504 fn modify_is_atomic_under_concurrency() {
2505 use std::sync::Arc;
2506 use std::thread;
2507
2508 let state = Arc::new(State::new());
2509 let _ = state.set("count", 0u64);
2510
2511 let threads = 8;
2512 let per_thread = 1000;
2513 let handles: Vec<_> = (0..threads)
2514 .map(|_| {
2515 let state = state.clone();
2516 thread::spawn(move || {
2517 for _ in 0..per_thread {
2518 let _ = state.modify("count", 0u64, |n| n + 1);
2519 }
2520 })
2521 })
2522 .collect();
2523 for h in handles {
2524 h.join().unwrap();
2525 }
2526 assert_eq!(
2528 state.get::<u64>("count"),
2529 Some((threads * per_thread) as u64)
2530 );
2531 }
2532}
2533
2534#[cfg(test)]
2535mod proptests {
2536 use super::*;
2537 use proptest::prelude::*;
2538
2539 proptest! {
2542 #[test]
2543 fn rollback_always_restores_base(
2544 base_keys in proptest::collection::vec(("[a-c]", 0u32..5), 0..6),
2545 ops in proptest::collection::vec(
2546 prop_oneof![
2547 ("[a-c]", 0u32..5).prop_map(|(k, v)| (k, Some(v))),
2548 "[a-c]".prop_map(|k| (k, None)),
2549 ],
2550 0..12,
2551 ),
2552 ) {
2553 let base = State::new();
2554 for (k, v) in &base_keys {
2555 let _ = base.set(k.clone(), *v);
2556 }
2557 let snapshot = |s: &State| -> std::collections::BTreeMap<String, Value> {
2558 s.keys().into_iter().filter_map(|k| s.get_raw(&k).map(|v| (k, v))).collect()
2559 };
2560 let before = snapshot(&base);
2561
2562 let tx = base.with_delta_tracking();
2563 for (k, v) in &ops {
2564 match v {
2565 Some(v) => { let _ = tx.set(k.clone(), *v); }
2566 None => { tx.remove(k); }
2567 }
2568 }
2569 prop_assert_eq!(&before, &snapshot(&base));
2571
2572 tx.rollback();
2573 prop_assert_eq!(&before, &snapshot(&tx));
2574 }
2575 }
2576}
2577
2578#[cfg(test)]
2579mod derived_contains_fallback {
2580 use super::State;
2585
2586 #[test]
2587 fn contains_sees_a_derived_value_through_the_unprefixed_key() {
2588 let state = State::new();
2589 state.set("derived:risk", 0.85).unwrap();
2590 assert_eq!(
2591 state.get::<f64>("risk"),
2592 Some(0.85),
2593 "precondition: get falls back"
2594 );
2595 assert!(
2596 state.contains("risk"),
2597 "contains must fall back the same way get does"
2598 );
2599 }
2600
2601 #[test]
2602 fn contains_fallback_respects_delta_tracking_and_tombstones() {
2603 let state = State::new();
2604 state.set("derived:score", 1u32).unwrap();
2605 let tracked = state.with_delta_tracking();
2606 assert!(
2607 tracked.contains("score"),
2608 "inner derived value visible through tracked view"
2609 );
2610 tracked.remove("derived:score");
2611 assert!(
2612 !tracked.contains("score"),
2613 "a tombstone on the derived key shadows inner"
2614 );
2615 }
2616
2617 #[test]
2618 fn contains_does_not_fall_back_for_prefixed_keys() {
2619 let state = State::new();
2620 state.set("derived:flag", true).unwrap();
2621 assert!(
2622 !state.contains("session:flag"),
2623 "only unprefixed keys get the fallback"
2624 );
2625 }
2626}