agentmux_srv/backend/storage/
agent_groups.rs1use rusqlite::params;
17
18use super::error::StoreError;
19use super::store::Store;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct AgentGroup {
23 pub id: String,
24 pub name: String,
25 pub member_ids: Vec<String>,
26 pub created_at: i64,
27}
28
29impl Store {
30 pub fn agent_group_create(
31 &self,
32 id: &str,
33 name: &str,
34 member_ids: &[String],
35 created_at: i64,
36 ) -> Result<(), StoreError> {
37 let member_ids_json = serde_json::to_string(member_ids)?;
38 let conn = self.conn.lock().unwrap();
39 conn.execute(
40 "INSERT INTO db_agent_groups (id, name, member_ids, created_at) VALUES (?1, ?2, ?3, ?4)",
41 params![id, name, member_ids_json, created_at],
42 )?;
43 Ok(())
44 }
45
46 pub fn agent_group_list(&self) -> Result<Vec<AgentGroup>, StoreError> {
47 let conn = self.conn.lock().unwrap();
48 let mut stmt = conn.prepare(
49 "SELECT id, name, member_ids, created_at FROM db_agent_groups ORDER BY created_at ASC",
50 )?;
51 let iter = stmt.query_map([], map_row)?;
52 iter.collect::<Result<Vec<_>, _>>().map_err(Into::into)
53 }
54
55 pub fn agent_group_get(&self, id: &str) -> Result<Option<AgentGroup>, StoreError> {
56 let conn = self.conn.lock().unwrap();
57 let mut stmt = conn.prepare(
58 "SELECT id, name, member_ids, created_at FROM db_agent_groups WHERE id = ?1",
59 )?;
60 let mut rows = stmt.query_map(params![id], map_row)?;
61 match rows.next() {
62 Some(r) => Ok(Some(r?)),
63 None => Ok(None),
64 }
65 }
66
67 pub fn agent_group_update(
70 &self,
71 id: &str,
72 name: Option<&str>,
73 member_ids: Option<&[String]>,
74 ) -> Result<bool, StoreError> {
75 let conn = self.conn.lock().unwrap();
76 if let Some(name) = name {
77 conn.execute(
78 "UPDATE db_agent_groups SET name = ?2 WHERE id = ?1",
79 params![id, name],
80 )?;
81 }
82 if let Some(member_ids) = member_ids {
83 let member_ids_json = serde_json::to_string(member_ids)?;
84 conn.execute(
85 "UPDATE db_agent_groups SET member_ids = ?2 WHERE id = ?1",
86 params![id, member_ids_json],
87 )?;
88 }
89 let exists: bool = conn
90 .query_row("SELECT 1 FROM db_agent_groups WHERE id = ?1", params![id], |_| Ok(()))
91 .is_ok();
92 Ok(exists)
93 }
94
95 pub fn agent_group_delete(&self, id: &str) -> Result<bool, StoreError> {
96 let conn = self.conn.lock().unwrap();
97 let affected = conn.execute("DELETE FROM db_agent_groups WHERE id = ?1", params![id])?;
98 Ok(affected > 0)
99 }
100}
101
102fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<AgentGroup> {
103 let member_ids_json: String = row.get(2)?;
104 let member_ids: Vec<String> = serde_json::from_str(&member_ids_json).unwrap_or_default();
105 Ok(AgentGroup {
106 id: row.get(0)?,
107 name: row.get(1)?,
108 member_ids,
109 created_at: row.get(3)?,
110 })
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn object_store() -> Store {
118 Store::open(std::path::Path::new(":memory:")).unwrap()
119 }
120
121 #[test]
122 fn create_and_list_roundtrips() {
123 let store = object_store();
124 store
125 .agent_group_create("g1", "backend", &["block-a".to_string(), "block-b".to_string()], 1000)
126 .unwrap();
127 let groups = store.agent_group_list().unwrap();
128 assert_eq!(groups.len(), 1);
129 assert_eq!(groups[0].id, "g1");
130 assert_eq!(groups[0].name, "backend");
131 assert_eq!(groups[0].member_ids, vec!["block-a".to_string(), "block-b".to_string()]);
132 assert_eq!(groups[0].created_at, 1000);
133 }
134
135 #[test]
136 fn get_returns_none_for_missing_id() {
137 let store = object_store();
138 assert!(store.agent_group_get("nope").unwrap().is_none());
139 }
140
141 #[test]
142 fn update_name_only_leaves_members_untouched() {
143 let store = object_store();
144 store.agent_group_create("g1", "old-name", &["block-a".to_string()], 1000).unwrap();
145 let updated = store.agent_group_update("g1", Some("new-name"), None).unwrap();
146 assert!(updated);
147 let g = store.agent_group_get("g1").unwrap().unwrap();
148 assert_eq!(g.name, "new-name");
149 assert_eq!(g.member_ids, vec!["block-a".to_string()]);
150 }
151
152 #[test]
153 fn update_members_only_leaves_name_untouched() {
154 let store = object_store();
155 store.agent_group_create("g1", "name", &["block-a".to_string()], 1000).unwrap();
156 let updated = store
157 .agent_group_update("g1", None, Some(&["block-b".to_string(), "block-c".to_string()]))
158 .unwrap();
159 assert!(updated);
160 let g = store.agent_group_get("g1").unwrap().unwrap();
161 assert_eq!(g.name, "name");
162 assert_eq!(g.member_ids, vec!["block-b".to_string(), "block-c".to_string()]);
163 }
164
165 #[test]
166 fn update_returns_false_for_missing_id() {
167 let store = object_store();
168 assert!(!store.agent_group_update("nope", Some("x"), None).unwrap());
169 }
170
171 #[test]
172 fn delete_removes_the_row_and_returns_true() {
173 let store = object_store();
174 store.agent_group_create("g1", "name", &[], 1000).unwrap();
175 assert!(store.agent_group_delete("g1").unwrap());
176 assert!(store.agent_group_get("g1").unwrap().is_none());
177 }
178
179 #[test]
180 fn delete_returns_false_for_missing_id() {
181 let store = object_store();
182 assert!(!store.agent_group_delete("nope").unwrap());
183 }
184
185 #[test]
186 fn list_orders_by_created_at() {
187 let store = object_store();
188 store.agent_group_create("g2", "second", &[], 2000).unwrap();
189 store.agent_group_create("g1", "first", &[], 1000).unwrap();
190 let groups = store.agent_group_list().unwrap();
191 assert_eq!(groups.iter().map(|g| g.id.as_str()).collect::<Vec<_>>(), vec!["g1", "g2"]);
192 }
193}