1use punch_types::{FighterId, FighterManifest, FighterStatus, PunchError, PunchResult};
2use tracing::debug;
3
4use crate::MemorySubstrate;
5
6impl MemorySubstrate {
7 pub async fn save_fighter(
9 &self,
10 id: &FighterId,
11 manifest: &FighterManifest,
12 status: FighterStatus,
13 ) -> PunchResult<()> {
14 let manifest_json = serde_json::to_string(manifest)
15 .map_err(|e| PunchError::Memory(format!("failed to serialize manifest: {e}")))?;
16 let id_str = id.to_string();
17 let status_str = status.to_string();
18 let name = manifest.name.clone();
19
20 let conn = self.conn.lock().await;
21 conn.execute(
22 "INSERT OR REPLACE INTO fighters (id, name, manifest, status, updated_at)
23 VALUES (?1, ?2, ?3, ?4, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))",
24 rusqlite::params![id_str, name, manifest_json, status_str],
25 )
26 .map_err(|e| PunchError::Memory(format!("failed to save fighter: {e}")))?;
27
28 debug!(fighter_id = %id, "fighter saved");
29 Ok(())
30 }
31
32 pub async fn load_fighter(&self, id: &FighterId) -> PunchResult<Option<FighterManifest>> {
34 let id_str = id.to_string();
35 let conn = self.conn.lock().await;
36
37 let result = conn.query_row(
38 "SELECT manifest FROM fighters WHERE id = ?1",
39 [&id_str],
40 |row| {
41 let json: String = row.get(0)?;
42 Ok(json)
43 },
44 );
45
46 match result {
47 Ok(json) => {
48 let manifest: FighterManifest = serde_json::from_str(&json)
49 .map_err(|e| PunchError::Memory(format!("corrupt fighter manifest: {e}")))?;
50 Ok(Some(manifest))
51 }
52 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
53 Err(e) => Err(PunchError::Memory(format!("failed to load fighter: {e}"))),
54 }
55 }
56
57 pub async fn list_fighters(&self) -> PunchResult<Vec<(FighterId, String, FighterStatus)>> {
59 let conn = self.conn.lock().await;
60 let mut stmt = conn
61 .prepare("SELECT id, name, status FROM fighters ORDER BY name")
62 .map_err(|e| PunchError::Memory(format!("failed to list fighters: {e}")))?;
63
64 let rows = stmt
65 .query_map([], |row| {
66 let id_str: String = row.get(0)?;
67 let name: String = row.get(1)?;
68 let status_str: String = row.get(2)?;
69 Ok((id_str, name, status_str))
70 })
71 .map_err(|e| PunchError::Memory(format!("failed to list fighters: {e}")))?;
72
73 let mut fighters = Vec::new();
74 for row in rows {
75 let (id_str, name, status_str) =
76 row.map_err(|e| PunchError::Memory(format!("failed to read fighter row: {e}")))?;
77
78 let id = FighterId(
79 uuid::Uuid::parse_str(&id_str)
80 .map_err(|e| PunchError::Memory(format!("invalid fighter id: {e}")))?,
81 );
82 let status = parse_fighter_status(&status_str)?;
83 fighters.push((id, name, status));
84 }
85
86 Ok(fighters)
87 }
88
89 pub async fn update_fighter_status(
91 &self,
92 id: &FighterId,
93 status: FighterStatus,
94 ) -> PunchResult<()> {
95 let id_str = id.to_string();
96 let status_str = status.to_string();
97 let conn = self.conn.lock().await;
98
99 let changed = conn
100 .execute(
101 "UPDATE fighters SET status = ?1, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?2",
102 rusqlite::params![status_str, id_str],
103 )
104 .map_err(|e| PunchError::Memory(format!("failed to update fighter status: {e}")))?;
105
106 if changed == 0 {
107 return Err(PunchError::Fighter(format!("fighter {id} not found")));
108 }
109
110 debug!(fighter_id = %id, %status, "fighter status updated");
111 Ok(())
112 }
113
114 pub async fn delete_fighter(&self, id: &FighterId) -> PunchResult<()> {
116 let id_str = id.to_string();
117 let conn = self.conn.lock().await;
118
119 conn.execute("DELETE FROM fighters WHERE id = ?1", [&id_str])
120 .map_err(|e| PunchError::Memory(format!("failed to delete fighter: {e}")))?;
121
122 debug!(fighter_id = %id, "fighter deleted");
123 Ok(())
124 }
125}
126
127fn parse_fighter_status(s: &str) -> PunchResult<FighterStatus> {
128 match s {
129 "idle" => Ok(FighterStatus::Idle),
130 "fighting" => Ok(FighterStatus::Fighting),
131 "resting" => Ok(FighterStatus::Resting),
132 "knocked_out" => Ok(FighterStatus::KnockedOut),
133 "training" => Ok(FighterStatus::Training),
134 other => Err(PunchError::Memory(format!(
135 "unknown fighter status: {other}"
136 ))),
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use punch_types::{FighterManifest, FighterStatus, ModelConfig, Provider, WeightClass};
143
144 use crate::MemorySubstrate;
145
146 fn test_manifest() -> FighterManifest {
147 FighterManifest {
148 name: "Test Fighter".into(),
149 description: "A test fighter".into(),
150 model: ModelConfig {
151 provider: Provider::Anthropic,
152 model: "claude-sonnet-4-20250514".into(),
153 api_key_env: None,
154 base_url: None,
155 max_tokens: Some(4096),
156 temperature: Some(0.7),
157 },
158 system_prompt: "You are a test fighter.".into(),
159 capabilities: Vec::new(),
160 weight_class: WeightClass::Middleweight,
161 tenant_id: None,
162 }
163 }
164
165 #[tokio::test]
166 async fn test_save_and_load_fighter() {
167 let substrate = MemorySubstrate::in_memory().unwrap();
168 let id = punch_types::FighterId::new();
169 let manifest = test_manifest();
170
171 substrate
172 .save_fighter(&id, &manifest, FighterStatus::Idle)
173 .await
174 .unwrap();
175
176 let loaded = substrate.load_fighter(&id).await.unwrap();
177 assert!(loaded.is_some());
178 assert_eq!(loaded.unwrap().name, "Test Fighter");
179 }
180
181 #[tokio::test]
182 async fn test_list_fighters() {
183 let substrate = MemorySubstrate::in_memory().unwrap();
184 let id = punch_types::FighterId::new();
185 let manifest = test_manifest();
186
187 substrate
188 .save_fighter(&id, &manifest, FighterStatus::Idle)
189 .await
190 .unwrap();
191
192 let fighters = substrate.list_fighters().await.unwrap();
193 assert_eq!(fighters.len(), 1);
194 assert_eq!(fighters[0].1, "Test Fighter");
195 }
196
197 #[tokio::test]
198 async fn test_load_nonexistent_fighter() {
199 let substrate = MemorySubstrate::in_memory().unwrap();
200 let id = punch_types::FighterId::new();
201 let loaded = substrate.load_fighter(&id).await.unwrap();
202 assert!(loaded.is_none());
203 }
204
205 #[tokio::test]
206 async fn test_list_multiple_fighters() {
207 let substrate = MemorySubstrate::in_memory().unwrap();
208 let id1 = punch_types::FighterId::new();
209 let id2 = punch_types::FighterId::new();
210
211 let mut m1 = test_manifest();
212 m1.name = "Alpha Fighter".into();
213 let mut m2 = test_manifest();
214 m2.name = "Beta Fighter".into();
215
216 substrate.save_fighter(&id1, &m1, FighterStatus::Idle).await.unwrap();
217 substrate.save_fighter(&id2, &m2, FighterStatus::Fighting).await.unwrap();
218
219 let fighters = substrate.list_fighters().await.unwrap();
220 assert_eq!(fighters.len(), 2);
221 }
222
223 #[tokio::test]
224 async fn test_update_nonexistent_fighter_status() {
225 let substrate = MemorySubstrate::in_memory().unwrap();
226 let id = punch_types::FighterId::new();
227 let result = substrate.update_fighter_status(&id, FighterStatus::Fighting).await;
228 assert!(result.is_err());
229 }
230
231 #[tokio::test]
232 async fn test_delete_and_verify_gone() {
233 let substrate = MemorySubstrate::in_memory().unwrap();
234 let id = punch_types::FighterId::new();
235 substrate.save_fighter(&id, &test_manifest(), FighterStatus::Idle).await.unwrap();
236
237 substrate.delete_fighter(&id).await.unwrap();
238 let loaded = substrate.load_fighter(&id).await.unwrap();
239 assert!(loaded.is_none());
240
241 let fighters = substrate.list_fighters().await.unwrap();
242 assert!(fighters.is_empty());
243 }
244
245 #[tokio::test]
246 async fn test_save_fighter_overwrites() {
247 let substrate = MemorySubstrate::in_memory().unwrap();
248 let id = punch_types::FighterId::new();
249
250 let mut m1 = test_manifest();
251 m1.name = "Original".into();
252 substrate.save_fighter(&id, &m1, FighterStatus::Idle).await.unwrap();
253
254 let mut m2 = test_manifest();
255 m2.name = "Updated".into();
256 substrate.save_fighter(&id, &m2, FighterStatus::Fighting).await.unwrap();
257
258 let loaded = substrate.load_fighter(&id).await.unwrap().unwrap();
259 assert_eq!(loaded.name, "Updated");
260
261 let fighters = substrate.list_fighters().await.unwrap();
263 assert_eq!(fighters.len(), 1);
264 }
265
266 #[tokio::test]
267 async fn test_update_and_delete_fighter() {
268 let substrate = MemorySubstrate::in_memory().unwrap();
269 let id = punch_types::FighterId::new();
270 let manifest = test_manifest();
271
272 substrate
273 .save_fighter(&id, &manifest, FighterStatus::Idle)
274 .await
275 .unwrap();
276
277 substrate
278 .update_fighter_status(&id, FighterStatus::Fighting)
279 .await
280 .unwrap();
281
282 substrate.delete_fighter(&id).await.unwrap();
283
284 let loaded = substrate.load_fighter(&id).await.unwrap();
285 assert!(loaded.is_none());
286 }
287}