Skip to main content

car_state/
lib.rs

1//! State management for Common Agent Runtime.
2//!
3//! Provides structured, typed state with transition logging.
4//! Every mutation produces a StateTransition record for audit and replay.
5
6use chrono::{DateTime, Utc};
7use parking_lot::Mutex;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11
12/// An explicit record of a state change.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct StateTransition {
15    pub key: String,
16    pub old_value: Option<Value>,
17    pub new_value: Option<Value>,
18    pub action_id: String,
19    pub timestamp: DateTime<Utc>,
20}
21
22/// Thread-safe state store with transition logging.
23///
24/// All reads and writes go through this store. Every write produces a
25/// StateTransition record for audit and replay.
26pub struct StateStore {
27    state: Mutex<HashMap<String, Value>>,
28    transitions: Mutex<Vec<StateTransition>>,
29}
30
31impl StateStore {
32    pub fn new() -> Self {
33        Self {
34            state: Mutex::new(HashMap::new()),
35            transitions: Mutex::new(Vec::new()),
36        }
37    }
38
39    pub fn get(&self, key: &str) -> Option<Value> {
40        self.state.lock().get(key).cloned()
41    }
42
43    pub fn get_or(&self, key: &str, default: Value) -> Value {
44        self.state.lock().get(key).cloned().unwrap_or(default)
45    }
46
47    pub fn exists(&self, key: &str) -> bool {
48        self.state.lock().contains_key(key)
49    }
50
51    pub fn set(&self, key: &str, value: Value, action_id: &str) -> StateTransition {
52        let mut state = self.state.lock();
53        let old = state.get(key).cloned();
54        state.insert(key.to_string(), value.clone());
55
56        let t = StateTransition {
57            key: key.to_string(),
58            old_value: old,
59            new_value: Some(value),
60            action_id: action_id.to_string(),
61            timestamp: Utc::now(),
62        };
63
64        self.transitions.lock().push(t.clone());
65        t
66    }
67
68    pub fn delete(&self, key: &str, action_id: &str) -> Option<StateTransition> {
69        let mut state = self.state.lock();
70        let old = state.remove(key)?;
71
72        let t = StateTransition {
73            key: key.to_string(),
74            old_value: Some(old),
75            new_value: None,
76            action_id: action_id.to_string(),
77            timestamp: Utc::now(),
78        };
79
80        self.transitions.lock().push(t.clone());
81        Some(t)
82    }
83
84    /// Deep clone of current state.
85    pub fn snapshot(&self) -> HashMap<String, Value> {
86        self.state.lock().clone()
87    }
88
89    /// Restore state from a snapshot, truncating transitions.
90    pub fn restore(&self, snapshot: HashMap<String, Value>, transition_count: usize) {
91        *self.state.lock() = snapshot;
92        self.transitions.lock().truncate(transition_count);
93    }
94
95    pub fn transition_count(&self) -> usize {
96        self.transitions.lock().len()
97    }
98
99    pub fn transitions(&self) -> Vec<StateTransition> {
100        self.transitions.lock().clone()
101    }
102
103    pub fn transitions_since(&self, index: usize) -> Vec<StateTransition> {
104        let transitions = self.transitions.lock();
105        let start = index.min(transitions.len());
106        transitions[start..].to_vec()
107    }
108
109    pub fn keys(&self) -> Vec<String> {
110        self.state.lock().keys().cloned().collect()
111    }
112
113    /// Replace the entire state map without recording transitions.
114    /// Used by checkpoint restore to avoid synthetic transition history.
115    /// Also clears the transitions log so callers of `transitions_since()`
116    /// don't see stale history from the discarded state.
117    pub fn replace_all(&self, snapshot: HashMap<String, Value>) {
118        *self.state.lock() = snapshot;
119        self.transitions.lock().clear();
120    }
121}
122
123impl Default for StateStore {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl car_ir::precondition::StateView for StateStore {
130    fn get_value(&self, key: &str) -> Option<Value> {
131        self.get(key)
132    }
133    fn key_exists(&self, key: &str) -> bool {
134        self.exists(key)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn set_and_get() {
144        let store = StateStore::new();
145        store.set("x", Value::from(42), "test");
146        assert_eq!(store.get("x"), Some(Value::from(42)));
147    }
148
149    #[test]
150    fn exists() {
151        let store = StateStore::new();
152        assert!(!store.exists("x"));
153        store.set("x", Value::from(1), "test");
154        assert!(store.exists("x"));
155    }
156
157    #[test]
158    fn delete() {
159        let store = StateStore::new();
160        store.set("x", Value::from(1), "test");
161        let t = store.delete("x", "test");
162        assert!(t.is_some());
163        assert!(!store.exists("x"));
164    }
165
166    #[test]
167    fn delete_nonexistent() {
168        let store = StateStore::new();
169        assert!(store.delete("x", "test").is_none());
170    }
171
172    #[test]
173    fn snapshot_and_restore() {
174        let store = StateStore::new();
175        store.set("x", Value::from(1), "a");
176        let snap = store.snapshot();
177        let tc = store.transition_count();
178
179        store.set("y", Value::from(2), "b");
180        assert!(store.exists("y"));
181
182        store.restore(snap, tc);
183        assert!(store.exists("x"));
184        assert!(!store.exists("y"));
185        assert_eq!(store.transition_count(), 1);
186    }
187
188    #[test]
189    fn transitions_logged() {
190        let store = StateStore::new();
191        store.set("a", Value::from(1), "act1");
192        store.set("b", Value::from(2), "act2");
193
194        let transitions = store.transitions();
195        assert_eq!(transitions.len(), 2);
196        assert_eq!(transitions[0].key, "a");
197        assert_eq!(transitions[1].key, "b");
198    }
199
200    #[test]
201    fn transitions_since() {
202        let store = StateStore::new();
203        store.set("a", Value::from(1), "act1");
204        let idx = store.transition_count();
205        store.set("b", Value::from(2), "act2");
206
207        let since = store.transitions_since(idx);
208        assert_eq!(since.len(), 1);
209        assert_eq!(since[0].key, "b");
210    }
211
212    #[test]
213    fn transition_records_old_value() {
214        let store = StateStore::new();
215        store.set("x", Value::from(1), "first");
216        store.set("x", Value::from(2), "second");
217
218        let transitions = store.transitions();
219        assert_eq!(transitions[1].old_value, Some(Value::from(1)));
220        assert_eq!(transitions[1].new_value, Some(Value::from(2)));
221    }
222
223    #[test]
224    fn keys() {
225        let store = StateStore::new();
226        store.set("a", Value::from(1), "t");
227        store.set("b", Value::from(2), "t");
228        let mut keys = store.keys();
229        keys.sort();
230        assert_eq!(keys, vec!["a", "b"]);
231    }
232
233    #[test]
234    fn transitions_since_after_restore_does_not_panic() {
235        let store = StateStore::new();
236        store.set("a", serde_json::json!(1), "test");
237        store.set("b", serde_json::json!(2), "test");
238        let count_before = store.transition_count(); // 2
239
240        // Restore to empty, truncating transitions to 0
241        store.restore(HashMap::new(), 0);
242
243        // Using the stale count_before (2) should not panic
244        let result = store.transitions_since(count_before);
245        assert!(result.is_empty());
246    }
247
248    #[test]
249    fn transitions_since_normal_usage() {
250        let store = StateStore::new();
251        store.set("a", serde_json::json!(1), "test");
252        let mark = store.transition_count();
253        store.set("b", serde_json::json!(2), "test");
254        let since = store.transitions_since(mark);
255        assert_eq!(since.len(), 1);
256        assert_eq!(since[0].key, "b");
257    }
258
259    #[test]
260    fn replace_all_swaps_state_without_transitions() {
261        let store = StateStore::new();
262        store.set("old_key", serde_json::json!("old"), "setup");
263
264        let mut new_state = HashMap::new();
265        new_state.insert("new_key".to_string(), serde_json::json!("new"));
266        store.replace_all(new_state);
267
268        assert_eq!(store.get("new_key"), Some(serde_json::json!("new")));
269        assert_eq!(store.get("old_key"), None);
270        // After replace_all, transitions should be cleared (not preserved)
271        assert_eq!(store.transition_count(), 0);
272    }
273}