Skip to main content

punch_memory/
fighters.rs

1use punch_types::{FighterId, FighterManifest, FighterStatus, PunchError, PunchResult};
2use tracing::debug;
3
4use crate::MemorySubstrate;
5
6impl MemorySubstrate {
7    /// Persist a fighter's manifest and status.
8    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    /// Load a fighter manifest by ID.
33    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    /// List all stored fighters as `(FighterId, name, FighterStatus)` tuples.
58    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    /// Update a fighter's operational status.
90    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    /// Delete a fighter and all related data (cascading).
115    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        // Should still only be 1 fighter
262        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}