1use serde::{Deserialize, Serialize};
5use zeph_common::SkillTrustLevel;
6#[allow(unused_imports)]
7use zeph_db::sql;
8
9use super::SqliteStore;
10use crate::error::MemoryError;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15#[non_exhaustive]
16pub enum SourceKind {
17 Local,
18 Hub,
19 File,
20 Bundled,
22}
23
24impl SourceKind {
25 fn as_str(&self) -> &'static str {
26 match self {
27 Self::Local => "local",
28 Self::Hub => "hub",
29 Self::File => "file",
30 Self::Bundled => "bundled",
31 }
32 }
33}
34
35impl std::fmt::Display for SourceKind {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.pad(self.as_str())
38 }
39}
40
41impl std::str::FromStr for SourceKind {
42 type Err = String;
43
44 fn from_str(s: &str) -> Result<Self, Self::Err> {
45 match s {
46 "local" => Ok(Self::Local),
47 "hub" => Ok(Self::Hub),
48 "file" => Ok(Self::File),
49 "bundled" => Ok(Self::Bundled),
50 other => Err(format!("unknown source_kind: {other}")),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
56pub struct SkillTrustRow {
57 pub skill_name: String,
58 pub trust_level: SkillTrustLevel,
59 pub source_kind: SourceKind,
60 pub source_url: Option<String>,
61 pub source_path: Option<String>,
62 pub blake3_hash: String,
63 pub updated_at: String,
64 pub git_hash: Option<String>,
66 pub requires_trust_check: bool,
70}
71
72type TrustTuple = (
74 String,
75 String,
76 String,
77 Option<String>,
78 Option<String>,
79 String,
80 String,
81 Option<String>,
82 i32,
83);
84
85fn row_from_tuple(t: TrustTuple) -> SkillTrustRow {
86 let source_kind = t.2.parse::<SourceKind>().unwrap_or(SourceKind::Local);
87 let trust_level = t.1.parse::<SkillTrustLevel>().unwrap_or_default();
88 SkillTrustRow {
89 skill_name: t.0,
90 trust_level,
91 source_kind,
92 source_url: t.3,
93 source_path: t.4,
94 blake3_hash: t.5,
95 updated_at: t.6,
96 git_hash: t.7,
97 requires_trust_check: t.8 != 0,
98 }
99}
100
101impl SqliteStore {
102 #[tracing::instrument(name = "memory.trust.upsert", skip_all, fields(skill = %skill_name))]
108 pub async fn upsert_skill_trust(
109 &self,
110 skill_name: &str,
111 trust_level: SkillTrustLevel,
112 source_kind: SourceKind,
113 source_url: Option<&str>,
114 source_path: Option<&str>,
115 blake3_hash: &str,
116 ) -> Result<(), MemoryError> {
117 self.upsert_skill_trust_with_git_hash(
118 skill_name,
119 trust_level,
120 source_kind,
121 source_url,
122 source_path,
123 blake3_hash,
124 None,
125 )
126 .await
127 }
128
129 #[allow(clippy::too_many_arguments)]
140 #[tracing::instrument(name = "memory.trust.upsert_with_git_hash", skip_all, fields(skill = %skill_name))]
141 pub async fn upsert_skill_trust_with_git_hash(
142 &self,
143 skill_name: &str,
144 trust_level: SkillTrustLevel,
145 source_kind: SourceKind,
146 source_url: Option<&str>,
147 source_path: Option<&str>,
148 blake3_hash: &str,
149 git_hash: Option<&str>,
150 ) -> Result<(), MemoryError> {
151 zeph_db::query(
152 sql!("INSERT INTO skill_trust \
153 (skill_name, trust_level, source_kind, source_url, source_path, blake3_hash, git_hash, updated_at) \
154 VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) \
155 ON CONFLICT(skill_name) DO UPDATE SET \
156 trust_level = excluded.trust_level, \
157 source_kind = excluded.source_kind, \
158 source_url = excluded.source_url, \
159 source_path = excluded.source_path, \
160 blake3_hash = excluded.blake3_hash, \
161 git_hash = excluded.git_hash, \
162 updated_at = CURRENT_TIMESTAMP"),
163 )
164 .bind(skill_name)
165 .bind(trust_level.as_str())
166 .bind(source_kind.as_str())
167 .bind(source_url)
168 .bind(source_path)
169 .bind(blake3_hash)
170 .bind(git_hash)
171 .execute(&self.pool)
172 .await?;
173 Ok(())
174 }
175
176 #[tracing::instrument(name = "memory.trust.load", skip_all, fields(skill = %skill_name))]
182 pub async fn load_skill_trust(
183 &self,
184 skill_name: &str,
185 ) -> Result<Option<SkillTrustRow>, MemoryError> {
186 let updated_at_sel =
189 <zeph_db::ActiveDialect as zeph_db::dialect::Dialect>::select_as_text("updated_at");
190 let raw = format!(
191 "SELECT skill_name, trust_level, source_kind, source_url, source_path, \
192 blake3_hash, {updated_at_sel}, git_hash, requires_trust_check \
193 FROM skill_trust WHERE skill_name = ?"
194 );
195 let query_sql = zeph_db::rewrite_placeholders(&raw);
196 let row: Option<TrustTuple> = zeph_db::query_as(sqlx::AssertSqlSafe(query_sql))
197 .bind(skill_name)
198 .fetch_optional(&self.pool)
199 .await?;
200 Ok(row.map(row_from_tuple))
201 }
202
203 #[tracing::instrument(name = "memory.trust.load_all", skip_all)]
209 pub async fn load_all_skill_trust(&self) -> Result<Vec<SkillTrustRow>, MemoryError> {
210 let updated_at_sel =
212 <zeph_db::ActiveDialect as zeph_db::dialect::Dialect>::select_as_text("updated_at");
213 let raw = format!(
214 "SELECT skill_name, trust_level, source_kind, source_url, source_path, \
215 blake3_hash, {updated_at_sel}, git_hash, requires_trust_check \
216 FROM skill_trust ORDER BY skill_name"
217 );
218 let query_sql = zeph_db::rewrite_placeholders(&raw);
219 let rows: Vec<TrustTuple> = zeph_db::query_as(sqlx::AssertSqlSafe(query_sql))
220 .fetch_all(&self.pool)
221 .await?;
222 Ok(rows.into_iter().map(row_from_tuple).collect())
223 }
224
225 #[tracing::instrument(name = "memory.trust.set_level", skip_all, fields(skill = %skill_name))]
231 pub async fn set_skill_trust_level(
232 &self,
233 skill_name: &str,
234 trust_level: SkillTrustLevel,
235 ) -> Result<bool, MemoryError> {
236 let result = zeph_db::query(
237 sql!("UPDATE skill_trust SET trust_level = ?, updated_at = CURRENT_TIMESTAMP WHERE skill_name = ?"),
238 )
239 .bind(trust_level.as_str())
240 .bind(skill_name)
241 .execute(&self.pool)
242 .await?;
243 Ok(result.rows_affected() > 0)
244 }
245
246 #[tracing::instrument(name = "memory.trust.delete", skip_all, fields(skill = %skill_name))]
252 pub async fn delete_skill_trust(&self, skill_name: &str) -> Result<bool, MemoryError> {
253 let result = zeph_db::query(sql!("DELETE FROM skill_trust WHERE skill_name = ?"))
254 .bind(skill_name)
255 .execute(&self.pool)
256 .await?;
257 Ok(result.rows_affected() > 0)
258 }
259
260 #[tracing::instrument(name = "memory.trust.set_check_flag", skip_all, fields(skill = %skill_name))]
269 pub async fn set_requires_trust_check(
270 &self,
271 skill_name: &str,
272 enabled: bool,
273 ) -> Result<bool, MemoryError> {
274 let flag = i64::from(enabled);
275 let result = zeph_db::query(
276 sql!("UPDATE skill_trust SET requires_trust_check = ?, updated_at = CURRENT_TIMESTAMP WHERE skill_name = ?"),
277 )
278 .bind(flag)
279 .bind(skill_name)
280 .execute(&self.pool)
281 .await?;
282 Ok(result.rows_affected() > 0)
283 }
284
285 #[tracing::instrument(name = "memory.trust.update_hash", skip_all, fields(skill = %skill_name))]
291 pub async fn update_skill_hash(
292 &self,
293 skill_name: &str,
294 blake3_hash: &str,
295 ) -> Result<bool, MemoryError> {
296 let result = zeph_db::query(
297 sql!("UPDATE skill_trust SET blake3_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE skill_name = ?"),
298 )
299 .bind(blake3_hash)
300 .bind(skill_name)
301 .execute(&self.pool)
302 .await?;
303 Ok(result.rows_affected() > 0)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 async fn test_store() -> SqliteStore {
312 SqliteStore::new(":memory:").await.unwrap()
313 }
314
315 #[tokio::test]
316 async fn upsert_and_load() {
317 let store = test_store().await;
318
319 store
320 .upsert_skill_trust(
321 "git",
322 SkillTrustLevel::Trusted,
323 SourceKind::Local,
324 None,
325 None,
326 "abc123",
327 )
328 .await
329 .unwrap();
330
331 let row = store.load_skill_trust("git").await.unwrap().unwrap();
332 assert_eq!(row.skill_name, "git");
333 assert_eq!(row.trust_level, SkillTrustLevel::Trusted);
334 assert_eq!(row.source_kind, SourceKind::Local);
335 assert_eq!(row.blake3_hash, "abc123");
336 }
337
338 #[tokio::test]
339 async fn upsert_updates_existing() {
340 let store = test_store().await;
341
342 store
343 .upsert_skill_trust(
344 "git",
345 SkillTrustLevel::Quarantined,
346 SourceKind::Local,
347 None,
348 None,
349 "hash1",
350 )
351 .await
352 .unwrap();
353 store
354 .upsert_skill_trust(
355 "git",
356 SkillTrustLevel::Trusted,
357 SourceKind::Local,
358 None,
359 None,
360 "hash2",
361 )
362 .await
363 .unwrap();
364
365 let row = store.load_skill_trust("git").await.unwrap().unwrap();
366 assert_eq!(row.trust_level, SkillTrustLevel::Trusted);
367 assert_eq!(row.blake3_hash, "hash2");
368 }
369
370 #[tokio::test]
371 async fn load_nonexistent() {
372 let store = test_store().await;
373 let row = store.load_skill_trust("nope").await.unwrap();
374 assert!(row.is_none());
375 }
376
377 #[tokio::test]
378 async fn load_all() {
379 let store = test_store().await;
380
381 store
382 .upsert_skill_trust(
383 "alpha",
384 SkillTrustLevel::Trusted,
385 SourceKind::Local,
386 None,
387 None,
388 "h1",
389 )
390 .await
391 .unwrap();
392 store
393 .upsert_skill_trust(
394 "beta",
395 SkillTrustLevel::Quarantined,
396 SourceKind::Hub,
397 Some("https://hub.example.com"),
398 None,
399 "h2",
400 )
401 .await
402 .unwrap();
403
404 let rows = store.load_all_skill_trust().await.unwrap();
405 assert_eq!(rows.len(), 2);
406 assert_eq!(rows[0].skill_name, "alpha");
407 assert_eq!(rows[1].skill_name, "beta");
408 }
409
410 #[tokio::test]
411 async fn set_trust_level() {
412 let store = test_store().await;
413
414 store
415 .upsert_skill_trust(
416 "git",
417 SkillTrustLevel::Quarantined,
418 SourceKind::Local,
419 None,
420 None,
421 "h1",
422 )
423 .await
424 .unwrap();
425
426 let updated = store
427 .set_skill_trust_level("git", SkillTrustLevel::Blocked)
428 .await
429 .unwrap();
430 assert!(updated);
431
432 let row = store.load_skill_trust("git").await.unwrap().unwrap();
433 assert_eq!(row.trust_level, SkillTrustLevel::Blocked);
434 }
435
436 #[tokio::test]
437 async fn set_trust_level_nonexistent() {
438 let store = test_store().await;
439 let updated = store
440 .set_skill_trust_level("nope", SkillTrustLevel::Blocked)
441 .await
442 .unwrap();
443 assert!(!updated);
444 }
445
446 #[tokio::test]
447 async fn delete_trust() {
448 let store = test_store().await;
449
450 store
451 .upsert_skill_trust(
452 "git",
453 SkillTrustLevel::Trusted,
454 SourceKind::Local,
455 None,
456 None,
457 "h1",
458 )
459 .await
460 .unwrap();
461
462 let deleted = store.delete_skill_trust("git").await.unwrap();
463 assert!(deleted);
464
465 let row = store.load_skill_trust("git").await.unwrap();
466 assert!(row.is_none());
467 }
468
469 #[tokio::test]
470 async fn delete_nonexistent() {
471 let store = test_store().await;
472 let deleted = store.delete_skill_trust("nope").await.unwrap();
473 assert!(!deleted);
474 }
475
476 #[tokio::test]
477 async fn set_requires_trust_check_flag() {
478 let store = test_store().await;
479
480 store
481 .upsert_skill_trust(
482 "git",
483 SkillTrustLevel::Trusted,
484 SourceKind::Local,
485 None,
486 None,
487 "hash1",
488 )
489 .await
490 .unwrap();
491
492 let row = store.load_skill_trust("git").await.unwrap().unwrap();
493 assert!(
494 !row.requires_trust_check,
495 "requires_trust_check must default to false on insert"
496 );
497
498 let updated = store.set_requires_trust_check("git", true).await.unwrap();
499 assert!(updated);
500 let row = store.load_skill_trust("git").await.unwrap().unwrap();
501 assert!(row.requires_trust_check);
502
503 let updated = store.set_requires_trust_check("git", false).await.unwrap();
504 assert!(updated);
505 let row = store.load_skill_trust("git").await.unwrap().unwrap();
506 assert!(!row.requires_trust_check);
507 }
508
509 #[tokio::test]
510 async fn set_requires_trust_check_nonexistent() {
511 let store = test_store().await;
512 let updated = store.set_requires_trust_check("nope", true).await.unwrap();
513 assert!(!updated);
514 }
515
516 #[tokio::test]
517 async fn update_hash() {
518 let store = test_store().await;
519
520 store
521 .upsert_skill_trust(
522 "git",
523 SkillTrustLevel::Verified,
524 SourceKind::Local,
525 None,
526 None,
527 "old_hash",
528 )
529 .await
530 .unwrap();
531
532 let updated = store.update_skill_hash("git", "new_hash").await.unwrap();
533 assert!(updated);
534
535 let row = store.load_skill_trust("git").await.unwrap().unwrap();
536 assert_eq!(row.blake3_hash, "new_hash");
537 }
538
539 #[tokio::test]
540 async fn source_with_url() {
541 let store = test_store().await;
542
543 store
544 .upsert_skill_trust(
545 "remote-skill",
546 SkillTrustLevel::Quarantined,
547 SourceKind::Hub,
548 Some("https://hub.example.com/skill"),
549 None,
550 "h1",
551 )
552 .await
553 .unwrap();
554
555 let row = store
556 .load_skill_trust("remote-skill")
557 .await
558 .unwrap()
559 .unwrap();
560 assert_eq!(row.source_kind, SourceKind::Hub);
561 assert_eq!(
562 row.source_url.as_deref(),
563 Some("https://hub.example.com/skill")
564 );
565 }
566
567 #[tokio::test]
568 async fn source_with_path() {
569 let store = test_store().await;
570
571 store
572 .upsert_skill_trust(
573 "file-skill",
574 SkillTrustLevel::Quarantined,
575 SourceKind::File,
576 None,
577 Some("/tmp/skill.tar.gz"),
578 "h1",
579 )
580 .await
581 .unwrap();
582
583 let row = store.load_skill_trust("file-skill").await.unwrap().unwrap();
584 assert_eq!(row.source_kind, SourceKind::File);
585 assert_eq!(row.source_path.as_deref(), Some("/tmp/skill.tar.gz"));
586 }
587
588 #[test]
589 fn source_kind_display_local() {
590 assert_eq!(SourceKind::Local.to_string(), "local");
591 }
592
593 #[test]
594 fn source_kind_display_hub() {
595 assert_eq!(SourceKind::Hub.to_string(), "hub");
596 }
597
598 #[test]
599 fn source_kind_display_file() {
600 assert_eq!(SourceKind::File.to_string(), "file");
601 }
602
603 #[test]
607 fn source_kind_display_respects_width() {
608 assert_eq!(
609 format!("{:<10}", SourceKind::Local),
610 format!("{:<10}", "local")
611 );
612 assert_eq!(
613 format!("{:>10}", SourceKind::Bundled),
614 format!("{:>10}", "bundled")
615 );
616 }
617
618 #[test]
619 fn source_kind_from_str_local() {
620 let kind: SourceKind = "local".parse().unwrap();
621 assert_eq!(kind, SourceKind::Local);
622 }
623
624 #[test]
625 fn source_kind_from_str_hub() {
626 let kind: SourceKind = "hub".parse().unwrap();
627 assert_eq!(kind, SourceKind::Hub);
628 }
629
630 #[test]
631 fn source_kind_from_str_file() {
632 let kind: SourceKind = "file".parse().unwrap();
633 assert_eq!(kind, SourceKind::File);
634 }
635
636 #[test]
637 fn source_kind_from_str_unknown_returns_error() {
638 let result: Result<SourceKind, _> = "s3".parse();
639 assert!(result.is_err());
640 assert!(result.unwrap_err().contains("unknown source_kind"));
641 }
642
643 #[test]
644 fn source_kind_serde_json_roundtrip_local() {
645 let original = SourceKind::Local;
646 let json = serde_json::to_string(&original).unwrap();
647 assert_eq!(json, r#""local""#);
648 let back: SourceKind = serde_json::from_str(&json).unwrap();
649 assert_eq!(back, original);
650 }
651
652 #[test]
653 fn source_kind_serde_json_roundtrip_hub() {
654 let original = SourceKind::Hub;
655 let json = serde_json::to_string(&original).unwrap();
656 assert_eq!(json, r#""hub""#);
657 let back: SourceKind = serde_json::from_str(&json).unwrap();
658 assert_eq!(back, original);
659 }
660
661 #[test]
662 fn source_kind_serde_json_roundtrip_file() {
663 let original = SourceKind::File;
664 let json = serde_json::to_string(&original).unwrap();
665 assert_eq!(json, r#""file""#);
666 let back: SourceKind = serde_json::from_str(&json).unwrap();
667 assert_eq!(back, original);
668 }
669
670 #[test]
671 fn source_kind_serde_json_invalid_value_errors() {
672 let result: Result<SourceKind, _> = serde_json::from_str(r#""unknown""#);
673 assert!(result.is_err());
674 }
675
676 #[tokio::test]
677 async fn trust_row_includes_git_hash() {
678 let store = test_store().await;
679
680 store
681 .upsert_skill_trust_with_git_hash(
682 "versioned-skill",
683 SkillTrustLevel::Trusted,
684 SourceKind::Hub,
685 Some("https://hub.example.com/skill"),
686 None,
687 "blake3abc",
688 Some("deadbeef1234"),
689 )
690 .await
691 .unwrap();
692
693 let row = store
694 .load_skill_trust("versioned-skill")
695 .await
696 .unwrap()
697 .unwrap();
698 assert_eq!(row.git_hash.as_deref(), Some("deadbeef1234"));
699 assert_eq!(row.blake3_hash, "blake3abc");
700 }
701
702 #[tokio::test]
703 async fn upsert_without_git_hash_leaves_it_null() {
704 let store = test_store().await;
705
706 store
707 .upsert_skill_trust(
708 "git",
709 SkillTrustLevel::Trusted,
710 SourceKind::Local,
711 None,
712 None,
713 "hash1",
714 )
715 .await
716 .unwrap();
717
718 let row = store.load_skill_trust("git").await.unwrap().unwrap();
719 assert!(row.git_hash.is_none());
720 }
721
722 #[tokio::test]
723 async fn upsert_each_source_kind_roundtrip() {
724 let store = test_store().await;
725 let variants = [
726 ("skill-local", SourceKind::Local),
727 ("skill-hub", SourceKind::Hub),
728 ("skill-file", SourceKind::File),
729 ("skill-bundled", SourceKind::Bundled),
730 ];
731 for (name, kind) in &variants {
732 store
733 .upsert_skill_trust(
734 name,
735 SkillTrustLevel::Trusted,
736 kind.clone(),
737 None,
738 None,
739 "hash",
740 )
741 .await
742 .unwrap();
743 let row = store.load_skill_trust(name).await.unwrap().unwrap();
744 assert_eq!(&row.source_kind, kind);
745 }
746 }
747
748 #[test]
749 fn source_kind_display_bundled() {
750 assert_eq!(SourceKind::Bundled.to_string(), "bundled");
751 }
752
753 #[test]
754 fn source_kind_from_str_bundled() {
755 let kind: SourceKind = "bundled".parse().unwrap();
756 assert_eq!(kind, SourceKind::Bundled);
757 }
758
759 #[test]
760 fn source_kind_serde_json_roundtrip_bundled() {
761 let original = SourceKind::Bundled;
762 let json = serde_json::to_string(&original).unwrap();
763 assert_eq!(json, r#""bundled""#);
764 let back: SourceKind = serde_json::from_str(&json).unwrap();
765 assert_eq!(back, original);
766 }
767
768 #[test]
769 fn source_kind_from_str_unknown_falls_back_to_local_in_row_from_tuple() {
770 let result: Result<SourceKind, _> = "future_variant".parse();
773 assert!(result.is_err());
774 let fallback = result.unwrap_or(SourceKind::Local);
776 assert_eq!(fallback, SourceKind::Local);
777 }
778
779 #[tokio::test]
782 async fn bundled_trust_preserved_on_same_source_kind_upsert() {
783 let store = test_store().await;
784
785 store
786 .upsert_skill_trust(
787 "web-search",
788 SkillTrustLevel::Trusted,
789 SourceKind::Bundled,
790 None,
791 None,
792 "hash1",
793 )
794 .await
795 .unwrap();
796
797 store
799 .upsert_skill_trust(
800 "web-search",
801 SkillTrustLevel::Trusted,
802 SourceKind::Bundled,
803 None,
804 None,
805 "hash1",
806 )
807 .await
808 .unwrap();
809
810 let row = store.load_skill_trust("web-search").await.unwrap().unwrap();
811 assert_eq!(row.source_kind, SourceKind::Bundled);
812 assert_eq!(row.trust_level, SkillTrustLevel::Trusted);
813 }
814
815 #[tokio::test]
818 async fn migration_hub_quarantined_to_bundled_trusted() {
819 let store = test_store().await;
820
821 store
823 .upsert_skill_trust(
824 "git",
825 SkillTrustLevel::Quarantined,
826 SourceKind::Hub,
827 None,
828 None,
829 "hash1",
830 )
831 .await
832 .unwrap();
833
834 let row = store.load_skill_trust("git").await.unwrap().unwrap();
835 assert_eq!(row.source_kind, SourceKind::Hub);
836 assert_eq!(row.trust_level, SkillTrustLevel::Quarantined);
837
838 store
840 .upsert_skill_trust(
841 "git",
842 SkillTrustLevel::Trusted,
843 SourceKind::Bundled,
844 None,
845 None,
846 "hash1",
847 )
848 .await
849 .unwrap();
850
851 let row = store.load_skill_trust("git").await.unwrap().unwrap();
852 assert_eq!(row.source_kind, SourceKind::Bundled);
853 assert_eq!(row.trust_level, SkillTrustLevel::Trusted);
854 }
855
856 #[tokio::test]
859 async fn operator_blocked_bundled_skill_stays_blocked_when_upserted_with_blocked() {
860 let store = test_store().await;
861
862 store
864 .upsert_skill_trust(
865 "web-search",
866 SkillTrustLevel::Blocked,
867 SourceKind::Hub,
868 None,
869 None,
870 "hash1",
871 )
872 .await
873 .unwrap();
874
875 store
877 .upsert_skill_trust(
878 "web-search",
879 SkillTrustLevel::Blocked,
880 SourceKind::Bundled,
881 None,
882 None,
883 "hash1",
884 )
885 .await
886 .unwrap();
887
888 let row = store.load_skill_trust("web-search").await.unwrap().unwrap();
889 assert_eq!(row.source_kind, SourceKind::Bundled);
890 assert_eq!(
891 row.trust_level,
892 SkillTrustLevel::Blocked,
893 "operator block must survive source_kind migration"
894 );
895 }
896
897 #[tokio::test]
900 async fn bundled_skill_with_configured_quarantined_level() {
901 let store = test_store().await;
902
903 store
904 .upsert_skill_trust(
905 "git",
906 SkillTrustLevel::Quarantined,
907 SourceKind::Bundled,
908 None,
909 None,
910 "hash1",
911 )
912 .await
913 .unwrap();
914
915 let row = store.load_skill_trust("git").await.unwrap().unwrap();
916 assert_eq!(row.source_kind, SourceKind::Bundled);
917 assert_eq!(row.trust_level, SkillTrustLevel::Quarantined);
918 }
919}