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