oxios_kernel/persona/
persistence.rs1use anyhow::{Context, Result};
19use serde::{Deserialize, Serialize};
20
21use super::Persona;
22use crate::state_store::StateStore;
23
24const SCHEMA_VERSION: u32 = 1;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct PersonaSnapshot {
29 pub schema_version: u32,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub active_persona_id: Option<String>,
33 pub personas: Vec<Persona>,
35}
36
37pub async fn load_from_state_store(store: &StateStore) -> Result<Option<PersonaSnapshot>> {
39 let raw: Option<PersonaSnapshot> = store
40 .load_json("personas", "index")
41 .await
42 .context("persona: failed to load index.json")?;
43 let Some(snap) = raw else { return Ok(None) };
44 if snap.schema_version != SCHEMA_VERSION {
45 anyhow::bail!(
46 "persona: schema_version {} not supported (expected {})",
47 snap.schema_version,
48 SCHEMA_VERSION
49 );
50 }
51 Ok(Some(snap))
52}
53
54pub async fn save_to_state_store(store: &StateStore, snapshot: &PersonaSnapshot) -> Result<()> {
56 store
57 .save_json("personas", "index", snapshot)
58 .await
59 .context("persona: failed to save index.json")
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use crate::persona::Persona;
66
67 fn make_persona(id: &str, name: &str, role: &str, enabled: bool) -> Persona {
68 Persona {
69 id: id.to_string(),
70 name: name.to_string(),
71 role: role.to_string(),
72 description: format!("Test persona {name}"),
73 system_prompt: format!("You are {name}."),
74 enabled,
75 model: None,
76 personality_traits: vec!["curious".to_string()],
77 }
78 }
79
80 fn make_store() -> StateStore {
81 let dir = tempfile::tempdir().unwrap();
82 StateStore::new(dir.keep()).unwrap()
83 }
84
85 #[tokio::test]
86 async fn test_round_trip() {
87 let store = make_store();
88 let snap = PersonaSnapshot {
89 schema_version: 1,
90 active_persona_id: Some("dev".to_string()),
91 personas: vec![
92 make_persona("dev", "Dev", "developer", true),
93 make_persona("qa", "Review", "qa", true),
94 ],
95 };
96 save_to_state_store(&store, &snap).await.unwrap();
97 let loaded = load_from_state_store(&store).await.unwrap().unwrap();
98 assert_eq!(loaded.schema_version, 1);
99 assert_eq!(loaded.active_persona_id, Some("dev".to_string()));
100 assert_eq!(loaded.personas.len(), 2);
101 assert_eq!(loaded.personas[0].id, "dev");
102 assert_eq!(loaded.personas[1].role, "qa");
103 }
104
105 #[tokio::test]
106 async fn test_no_file_returns_none() {
107 let store = make_store();
108 let loaded = load_from_state_store(&store).await.unwrap();
109 assert!(loaded.is_none());
110 }
111
112 #[tokio::test]
113 async fn test_schema_version_mismatch_errors() {
114 let store = make_store();
115 let snap = PersonaSnapshot {
116 schema_version: 99,
117 active_persona_id: None,
118 personas: vec![],
119 };
120 save_to_state_store(&store, &snap).await.unwrap();
121 let result = load_from_state_store(&store).await;
122 assert!(result.is_err());
123 let msg = result.unwrap_err().to_string();
124 assert!(msg.contains("schema_version"));
125 }
126
127 #[tokio::test]
128 async fn test_empty_personas_round_trip() {
129 let store = make_store();
130 let snap = PersonaSnapshot {
131 schema_version: 1,
132 active_persona_id: None,
133 personas: vec![],
134 };
135 save_to_state_store(&store, &snap).await.unwrap();
136 let loaded = load_from_state_store(&store).await.unwrap().unwrap();
137 assert!(loaded.personas.is_empty());
138 assert!(loaded.active_persona_id.is_none());
139 }
140
141 #[tokio::test]
142 async fn test_overwrite_on_save() {
143 let store = make_store();
144 let snap1 = PersonaSnapshot {
145 schema_version: 1,
146 active_persona_id: Some("dev".to_string()),
147 personas: vec![make_persona("dev", "Dev", "developer", true)],
148 };
149 save_to_state_store(&store, &snap1).await.unwrap();
150 let snap2 = PersonaSnapshot {
151 schema_version: 1,
152 active_persona_id: Some("qa".to_string()),
153 personas: vec![
154 make_persona("dev", "Dev", "developer", true),
155 make_persona("qa", "Review", "qa", true),
156 ],
157 };
158 save_to_state_store(&store, &snap2).await.unwrap();
159 let loaded = load_from_state_store(&store).await.unwrap().unwrap();
160 assert_eq!(loaded.active_persona_id, Some("qa".to_string()));
161 assert_eq!(loaded.personas.len(), 2);
162 }
163}