Skip to main content

zeph_memory/sqlite/
trust.rs

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    /// Upsert trust metadata for a skill.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if the database operation fails.
43    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    /// Load trust metadata for a single skill.
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if the query fails.
79    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    /// Load all skill trust entries.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if the query fails.
98    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    /// Update only the trust level for a skill.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the skill does not exist or the update fails.
113    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    /// Delete trust entry for a skill.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the delete fails.
133    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    /// Update the blake3 hash for a skill.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error if the update fails.
146    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}