1use super::SqliteStore;
5use crate::error::MemoryError;
6
7#[derive(Debug, Clone)]
8pub struct SkillTrustRow {
9 pub skill_name: String,
10 pub trust_level: String,
11 pub source_kind: String,
12 pub source_url: Option<String>,
13 pub source_path: Option<String>,
14 pub blake3_hash: String,
15 pub updated_at: String,
16}
17
18type TrustTuple = (
19 String,
20 String,
21 String,
22 Option<String>,
23 Option<String>,
24 String,
25 String,
26);
27
28fn row_from_tuple(t: TrustTuple) -> SkillTrustRow {
29 SkillTrustRow {
30 skill_name: t.0,
31 trust_level: t.1,
32 source_kind: t.2,
33 source_url: t.3,
34 source_path: t.4,
35 blake3_hash: t.5,
36 updated_at: t.6,
37 }
38}
39
40impl SqliteStore {
41 pub async fn upsert_skill_trust(
47 &self,
48 skill_name: &str,
49 trust_level: &str,
50 source_kind: &str,
51 source_url: Option<&str>,
52 source_path: Option<&str>,
53 blake3_hash: &str,
54 ) -> Result<(), MemoryError> {
55 sqlx::query(
56 "INSERT INTO skill_trust (skill_name, trust_level, source_kind, source_url, source_path, blake3_hash, updated_at) \
57 VALUES (?, ?, ?, ?, ?, ?, datetime('now')) \
58 ON CONFLICT(skill_name) DO UPDATE SET \
59 trust_level = excluded.trust_level, \
60 source_kind = excluded.source_kind, \
61 source_url = excluded.source_url, \
62 source_path = excluded.source_path, \
63 blake3_hash = excluded.blake3_hash, \
64 updated_at = datetime('now')",
65 )
66 .bind(skill_name)
67 .bind(trust_level)
68 .bind(source_kind)
69 .bind(source_url)
70 .bind(source_path)
71 .bind(blake3_hash)
72 .execute(&self.pool)
73 .await?;
74 Ok(())
75 }
76
77 pub async fn load_skill_trust(
83 &self,
84 skill_name: &str,
85 ) -> Result<Option<SkillTrustRow>, MemoryError> {
86 let row: Option<TrustTuple> = sqlx::query_as(
87 "SELECT skill_name, trust_level, source_kind, source_url, source_path, blake3_hash, updated_at \
88 FROM skill_trust WHERE skill_name = ?",
89 )
90 .bind(skill_name)
91 .fetch_optional(&self.pool)
92 .await?;
93 Ok(row.map(row_from_tuple))
94 }
95
96 pub async fn load_all_skill_trust(&self) -> Result<Vec<SkillTrustRow>, MemoryError> {
102 let rows: Vec<TrustTuple> = sqlx::query_as(
103 "SELECT skill_name, trust_level, source_kind, source_url, source_path, blake3_hash, updated_at \
104 FROM skill_trust ORDER BY skill_name",
105 )
106 .fetch_all(&self.pool)
107 .await?;
108 Ok(rows.into_iter().map(row_from_tuple).collect())
109 }
110
111 pub async fn set_skill_trust_level(
117 &self,
118 skill_name: &str,
119 trust_level: &str,
120 ) -> Result<bool, MemoryError> {
121 let result = sqlx::query(
122 "UPDATE skill_trust SET trust_level = ?, updated_at = datetime('now') WHERE skill_name = ?",
123 )
124 .bind(trust_level)
125 .bind(skill_name)
126 .execute(&self.pool)
127 .await?;
128 Ok(result.rows_affected() > 0)
129 }
130
131 pub async fn delete_skill_trust(&self, skill_name: &str) -> Result<bool, MemoryError> {
137 let result = sqlx::query("DELETE FROM skill_trust WHERE skill_name = ?")
138 .bind(skill_name)
139 .execute(&self.pool)
140 .await?;
141 Ok(result.rows_affected() > 0)
142 }
143
144 pub async fn update_skill_hash(
150 &self,
151 skill_name: &str,
152 blake3_hash: &str,
153 ) -> Result<bool, MemoryError> {
154 let result = sqlx::query(
155 "UPDATE skill_trust SET blake3_hash = ?, updated_at = datetime('now') WHERE skill_name = ?",
156 )
157 .bind(blake3_hash)
158 .bind(skill_name)
159 .execute(&self.pool)
160 .await?;
161 Ok(result.rows_affected() > 0)
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 async fn test_store() -> SqliteStore {
170 SqliteStore::new(":memory:").await.unwrap()
171 }
172
173 #[tokio::test]
174 async fn upsert_and_load() {
175 let store = test_store().await;
176
177 store
178 .upsert_skill_trust("git", "trusted", "local", None, None, "abc123")
179 .await
180 .unwrap();
181
182 let row = store.load_skill_trust("git").await.unwrap().unwrap();
183 assert_eq!(row.skill_name, "git");
184 assert_eq!(row.trust_level, "trusted");
185 assert_eq!(row.source_kind, "local");
186 assert_eq!(row.blake3_hash, "abc123");
187 }
188
189 #[tokio::test]
190 async fn upsert_updates_existing() {
191 let store = test_store().await;
192
193 store
194 .upsert_skill_trust("git", "quarantined", "local", None, None, "hash1")
195 .await
196 .unwrap();
197 store
198 .upsert_skill_trust("git", "trusted", "local", None, None, "hash2")
199 .await
200 .unwrap();
201
202 let row = store.load_skill_trust("git").await.unwrap().unwrap();
203 assert_eq!(row.trust_level, "trusted");
204 assert_eq!(row.blake3_hash, "hash2");
205 }
206
207 #[tokio::test]
208 async fn load_nonexistent() {
209 let store = test_store().await;
210 let row = store.load_skill_trust("nope").await.unwrap();
211 assert!(row.is_none());
212 }
213
214 #[tokio::test]
215 async fn load_all() {
216 let store = test_store().await;
217
218 store
219 .upsert_skill_trust("alpha", "trusted", "local", None, None, "h1")
220 .await
221 .unwrap();
222 store
223 .upsert_skill_trust(
224 "beta",
225 "quarantined",
226 "hub",
227 Some("https://hub.example.com"),
228 None,
229 "h2",
230 )
231 .await
232 .unwrap();
233
234 let rows = store.load_all_skill_trust().await.unwrap();
235 assert_eq!(rows.len(), 2);
236 assert_eq!(rows[0].skill_name, "alpha");
237 assert_eq!(rows[1].skill_name, "beta");
238 }
239
240 #[tokio::test]
241 async fn set_trust_level() {
242 let store = test_store().await;
243
244 store
245 .upsert_skill_trust("git", "quarantined", "local", None, None, "h1")
246 .await
247 .unwrap();
248
249 let updated = store.set_skill_trust_level("git", "blocked").await.unwrap();
250 assert!(updated);
251
252 let row = store.load_skill_trust("git").await.unwrap().unwrap();
253 assert_eq!(row.trust_level, "blocked");
254 }
255
256 #[tokio::test]
257 async fn set_trust_level_nonexistent() {
258 let store = test_store().await;
259 let updated = store
260 .set_skill_trust_level("nope", "blocked")
261 .await
262 .unwrap();
263 assert!(!updated);
264 }
265
266 #[tokio::test]
267 async fn delete_trust() {
268 let store = test_store().await;
269
270 store
271 .upsert_skill_trust("git", "trusted", "local", None, None, "h1")
272 .await
273 .unwrap();
274
275 let deleted = store.delete_skill_trust("git").await.unwrap();
276 assert!(deleted);
277
278 let row = store.load_skill_trust("git").await.unwrap();
279 assert!(row.is_none());
280 }
281
282 #[tokio::test]
283 async fn delete_nonexistent() {
284 let store = test_store().await;
285 let deleted = store.delete_skill_trust("nope").await.unwrap();
286 assert!(!deleted);
287 }
288
289 #[tokio::test]
290 async fn update_hash() {
291 let store = test_store().await;
292
293 store
294 .upsert_skill_trust("git", "verified", "local", None, None, "old_hash")
295 .await
296 .unwrap();
297
298 let updated = store.update_skill_hash("git", "new_hash").await.unwrap();
299 assert!(updated);
300
301 let row = store.load_skill_trust("git").await.unwrap().unwrap();
302 assert_eq!(row.blake3_hash, "new_hash");
303 }
304
305 #[tokio::test]
306 async fn source_with_url() {
307 let store = test_store().await;
308
309 store
310 .upsert_skill_trust(
311 "remote-skill",
312 "quarantined",
313 "hub",
314 Some("https://hub.example.com/skill"),
315 None,
316 "h1",
317 )
318 .await
319 .unwrap();
320
321 let row = store
322 .load_skill_trust("remote-skill")
323 .await
324 .unwrap()
325 .unwrap();
326 assert_eq!(row.source_kind, "hub");
327 assert_eq!(
328 row.source_url.as_deref(),
329 Some("https://hub.example.com/skill")
330 );
331 }
332
333 #[tokio::test]
334 async fn source_with_path() {
335 let store = test_store().await;
336
337 store
338 .upsert_skill_trust(
339 "file-skill",
340 "quarantined",
341 "file",
342 None,
343 Some("/tmp/skill.tar.gz"),
344 "h1",
345 )
346 .await
347 .unwrap();
348
349 let row = store.load_skill_trust("file-skill").await.unwrap().unwrap();
350 assert_eq!(row.source_kind, "file");
351 assert_eq!(row.source_path.as_deref(), Some("/tmp/skill.tar.gz"));
352 }
353}