1use std::collections::HashMap;
21use std::fmt;
22
23use chrono::{DateTime, Utc};
24use serde::{Deserialize, Serialize};
25
26use crate::crypto::SecureKey;
27use crate::error::{Result, VaultError};
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum VaultState {
36 Uninitialized,
38 Locked {
40 vault_name: String,
41 model_count: usize,
42 },
43 Unlocked {
45 vault_name: String,
46 model_count: usize,
47 unlocked_at: DateTime<Utc>,
48 operations_count: u64,
49 },
50 Error { message: String },
52}
53
54impl fmt::Display for VaultState {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 VaultState::Uninitialized => write!(f, "Uninitialized"),
58 VaultState::Locked { vault_name, .. } => write!(f, "Locked({})", vault_name),
59 VaultState::Unlocked { vault_name, .. } => write!(f, "Unlocked({})", vault_name),
60 VaultState::Error { message } => write!(f, "Error({})", message),
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq)]
78pub struct AimvUri {
79 pub vault: Option<String>,
81 pub model: Option<String>,
83 pub version: Option<u32>,
85 pub resource: Option<String>,
87 pub query: HashMap<String, String>,
89}
90
91impl AimvUri {
92 pub const SCHEME: &'static str = "aimv://";
94
95 pub fn parse(uri: &str) -> Result<Self> {
108 let stripped = uri.strip_prefix(Self::SCHEME).ok_or_else(|| {
109 VaultError::InvalidInput(format!("URI must start with '{}': {}", Self::SCHEME, uri))
110 })?;
111
112 let (path, query) = if let Some(idx) = stripped.find('?') {
114 (&stripped[..idx], Self::parse_query(&stripped[idx + 1..]))
115 } else {
116 (stripped, HashMap::new())
117 };
118
119 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
121
122 let (vault, model_part, resource) = match segments.len() {
123 0 => (None, None, None),
124 1 => (Some(segments[0].to_string()), None, None),
125 2 => (Some(segments[0].to_string()), Some(segments[1]), None),
126 3 => (
127 Some(segments[0].to_string()),
128 Some(segments[1]),
129 Some(segments[2].to_string()),
130 ),
131 _ => {
132 return Err(VaultError::InvalidInput(format!(
133 "URI has too many path segments: {}",
134 uri
135 )));
136 }
137 };
138
139 let (model, version) = if let Some(m) = model_part {
141 if let Some(idx) = m.find('@') {
142 let model_name = &m[..idx];
143 let ver_str = &m[idx + 1..];
144 let ver = ver_str.parse::<u32>().map_err(|_| {
145 VaultError::InvalidInput(format!("Invalid version number: {}", ver_str))
146 })?;
147 (Some(model_name.to_string()), Some(ver))
148 } else {
149 (Some(m.to_string()), None)
150 }
151 } else {
152 (None, None)
153 };
154
155 Ok(Self {
156 vault,
157 model,
158 version,
159 resource,
160 query,
161 })
162 }
163
164 fn parse_query(query: &str) -> HashMap<String, String> {
166 query
167 .split('&')
168 .filter_map(|pair| {
169 let mut parts = pair.splitn(2, '=');
170 let key = parts.next()?;
171 let value = parts.next().unwrap_or("");
172 if key.is_empty() {
173 None
174 } else {
175 Some((key.to_string(), value.to_string()))
176 }
177 })
178 .collect()
179 }
180}
181
182impl fmt::Display for AimvUri {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.write_str(Self::SCHEME)?;
185
186 if let Some(vault) = &self.vault {
187 f.write_str(vault)?;
188
189 if let Some(model) = &self.model {
190 f.write_str("/")?;
191 f.write_str(model)?;
192
193 if let Some(ver) = self.version {
194 write!(f, "@{}", ver)?;
195 }
196
197 if let Some(resource) = &self.resource {
198 f.write_str("/")?;
199 f.write_str(resource)?;
200 }
201 }
202 }
203
204 if !self.query.is_empty() {
205 f.write_str("?")?;
206 let pairs: Vec<String> = self
207 .query
208 .iter()
209 .map(|(k, v)| {
210 if v.is_empty() {
211 k.clone()
212 } else {
213 format!("{}={}", k, v)
214 }
215 })
216 .collect();
217 f.write_str(&pairs.join("&"))?;
218 }
219
220 Ok(())
221 }
222}
223
224pub trait CryptoProvider: Send + Sync {
234 fn derive_key(
238 &self,
239 passphrase: Vec<u8>,
240 salt: Option<Vec<u8>>,
241 ) -> Result<(SecureKey, Vec<u8>)>;
242
243 fn encrypt(&self, data: &[u8], key: &SecureKey) -> Result<Vec<u8>>;
245
246 fn decrypt(&self, encrypted_data: &[u8], key: &SecureKey) -> Result<Vec<u8>>;
248
249 fn hash(&self, data: &[u8]) -> Vec<u8>;
251
252 fn hash_hex(&self, data: &[u8]) -> String {
254 hex::encode(self.hash(data))
255 }
256
257 fn random_bytes(&self, length: usize) -> Vec<u8>;
259}
260
261pub trait BlobStore: Send + Sync {
271 fn put(&self, key: &str, data: &[u8], encryption_key: &SecureKey) -> Result<(u64, u64)>;
273
274 fn get(&self, key: &str, encryption_key: &SecureKey) -> Result<Vec<u8>>;
276
277 fn remove(&self, key: &str) -> Result<bool>;
279
280 fn exists(&self, key: &str) -> bool;
282
283 fn size(&self, key: &str) -> Result<u64>;
285
286 fn list_keys(&self) -> Result<Vec<String>>;
288
289 fn stats(&self) -> Result<BlobStoreStats>;
291}
292
293#[derive(Debug, Clone, Serialize)]
295pub struct BlobStoreStats {
296 pub total_size_bytes: u64,
297 pub file_count: usize,
298}
299
300#[derive(Debug, Clone, Serialize)]
302pub struct BlobReceipt {
303 pub key: String,
305 pub size_bytes: u64,
307 pub stored_at: DateTime<Utc>,
309}
310
311#[derive(Debug, Clone, Serialize)]
313pub struct BlobInfo {
314 pub key: String,
316 pub size_bytes: u64,
318}
319
320#[async_trait::async_trait]
332pub trait AsyncBlobStore: Send + Sync {
333 async fn put(&self, key: &str, data: &[u8]) -> Result<BlobReceipt>;
335
336 async fn get(&self, key: &str) -> Result<Vec<u8>>;
338
339 async fn delete(&self, key: &str) -> Result<bool>;
341
342 async fn exists(&self, key: &str) -> Result<bool>;
344
345 async fn list(&self, prefix: Option<&str>) -> Result<Vec<BlobInfo>>;
347
348 async fn stat(&self, key: &str) -> Result<BlobInfo>;
350}
351
352pub struct AsyncBlobStoreAdapter<B: crate::storage::StorageBackend> {
357 inner: B,
358}
359
360impl<B: crate::storage::StorageBackend> AsyncBlobStoreAdapter<B> {
361 pub fn new(backend: B) -> Self {
363 Self { inner: backend }
364 }
365}
366
367#[async_trait::async_trait]
368impl<B: crate::storage::StorageBackend + 'static> AsyncBlobStore for AsyncBlobStoreAdapter<B> {
369 async fn put(&self, key: &str, data: &[u8]) -> Result<BlobReceipt> {
370 self.inner.upload(key, data).await?;
371 Ok(BlobReceipt {
372 key: key.to_string(),
373 size_bytes: data.len() as u64,
374 stored_at: Utc::now(),
375 })
376 }
377
378 async fn get(&self, key: &str) -> Result<Vec<u8>> {
379 self.inner.download(key).await
380 }
381
382 async fn delete(&self, key: &str) -> Result<bool> {
383 self.inner.delete(key).await
384 }
385
386 async fn exists(&self, key: &str) -> Result<bool> {
387 self.inner.exists(key).await
388 }
389
390 async fn list(&self, _prefix: Option<&str>) -> Result<Vec<BlobInfo>> {
391 let keys = self.inner.list().await?;
392 let mut infos = Vec::with_capacity(keys.len());
393 for key in keys {
394 let size = self.inner.size(&key).await.unwrap_or(0);
395 infos.push(BlobInfo {
396 key,
397 size_bytes: size,
398 });
399 }
400 Ok(infos)
401 }
402
403 async fn stat(&self, key: &str) -> Result<BlobInfo> {
404 let size = self.inner.size(key).await?;
405 Ok(BlobInfo {
406 key: key.to_string(),
407 size_bytes: size,
408 })
409 }
410}
411
412#[allow(clippy::too_many_arguments)]
419pub trait VersionRepo: Send + Sync {
420 fn add_version(
422 &mut self,
423 model: &str,
424 file_path: &str,
425 format: &str,
426 size_bytes: u64,
427 compressed_size_bytes: u64,
428 checksum: &str,
429 metadata: Option<HashMap<String, String>>,
430 parent_version: Option<u32>,
431 ) -> Result<crate::version::ModelVersion>;
432
433 fn get_version(
435 &self,
436 model: &str,
437 version: Option<u32>,
438 ) -> Option<&crate::version::ModelVersion>;
439
440 fn list_versions(&self, model: &str) -> Vec<&crate::version::ModelVersion>;
442
443 fn get_lineage(&self, model: &str, version: u32) -> Vec<&crate::version::ModelVersion>;
445
446 fn delete_version(&mut self, model: &str, version: u32) -> Result<bool>;
448
449 fn cleanup_old_versions(&mut self, model: &str, keep_count: usize) -> Result<Vec<u32>>;
451
452 fn verify_checksum(&self, model: &str, version: u32, data: &[u8]) -> bool;
454
455 fn update_metadata(
457 &mut self,
458 model: &str,
459 version: u32,
460 key: &str,
461 value: String,
462 ) -> Result<()>;
463
464 fn get_metadata(&self, model: &str, version: u32, key: &str) -> Option<String>;
466
467 fn list_models(&self) -> Vec<String>;
469}
470
471pub trait AuditSink: Send + Sync {
479 fn emit(&self, entry: crate::audit::AuditEntry) -> Result<()>;
481
482 fn query(&self, limit: Option<usize>) -> Result<Vec<crate::audit::AuditEntry>>;
484}
485
486#[derive(Debug, Clone, Serialize)]
495#[serde(tag = "type")]
496pub enum VaultEvent {
497 VaultCreated {
498 vault: String,
499 timestamp: DateTime<Utc>,
500 },
501 VaultUnlocked {
502 vault: String,
503 timestamp: DateTime<Utc>,
504 },
505 VaultLocked {
506 vault: String,
507 timestamp: DateTime<Utc>,
508 },
509 ModelStored {
510 vault: String,
511 model: String,
512 version: u32,
513 format: String,
514 size: u64,
515 checksum: String,
516 timestamp: DateTime<Utc>,
517 },
518 ModelRetrieved {
519 vault: String,
520 model: String,
521 version: u32,
522 timestamp: DateTime<Utc>,
523 },
524 ModelDeleted {
525 vault: String,
526 model: String,
527 version: u32,
528 timestamp: DateTime<Utc>,
529 },
530 PassphraseChanged {
531 vault: String,
532 files_reencrypted: usize,
533 timestamp: DateTime<Utc>,
534 },
535 IntegrityFailed {
536 vault: String,
537 model: String,
538 version: u32,
539 expected: String,
540 actual: String,
541 timestamp: DateTime<Utc>,
542 },
543 ComplianceChecked {
544 vault: String,
545 passed: bool,
546 timestamp: DateTime<Utc>,
547 },
548}
549
550impl VaultEvent {
551 pub fn timestamp(&self) -> DateTime<Utc> {
553 match self {
554 VaultEvent::VaultCreated { timestamp, .. }
555 | VaultEvent::VaultUnlocked { timestamp, .. }
556 | VaultEvent::VaultLocked { timestamp, .. }
557 | VaultEvent::ModelStored { timestamp, .. }
558 | VaultEvent::ModelRetrieved { timestamp, .. }
559 | VaultEvent::ModelDeleted { timestamp, .. }
560 | VaultEvent::PassphraseChanged { timestamp, .. }
561 | VaultEvent::IntegrityFailed { timestamp, .. }
562 | VaultEvent::ComplianceChecked { timestamp, .. } => *timestamp,
563 }
564 }
565
566 pub fn vault_name(&self) -> &str {
568 match self {
569 VaultEvent::VaultCreated { vault, .. }
570 | VaultEvent::VaultUnlocked { vault, .. }
571 | VaultEvent::VaultLocked { vault, .. }
572 | VaultEvent::ModelStored { vault, .. }
573 | VaultEvent::ModelRetrieved { vault, .. }
574 | VaultEvent::ModelDeleted { vault, .. }
575 | VaultEvent::PassphraseChanged { vault, .. }
576 | VaultEvent::IntegrityFailed { vault, .. }
577 | VaultEvent::ComplianceChecked { vault, .. } => vault,
578 }
579 }
580
581 pub fn event_type(&self) -> &'static str {
583 match self {
584 VaultEvent::VaultCreated { .. } => "vault_created",
585 VaultEvent::VaultUnlocked { .. } => "vault_unlocked",
586 VaultEvent::VaultLocked { .. } => "vault_locked",
587 VaultEvent::ModelStored { .. } => "model_stored",
588 VaultEvent::ModelRetrieved { .. } => "model_retrieved",
589 VaultEvent::ModelDeleted { .. } => "model_deleted",
590 VaultEvent::PassphraseChanged { .. } => "passphrase_changed",
591 VaultEvent::IntegrityFailed { .. } => "integrity_failed",
592 VaultEvent::ComplianceChecked { .. } => "compliance_checked",
593 }
594 }
595}
596
597impl fmt::Display for VaultEvent {
598 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
599 write!(
600 f,
601 "[{}] {}",
602 self.timestamp().format("%Y-%m-%dT%H:%M:%SZ"),
603 self.event_type()
604 )
605 }
606}
607
608pub trait EventSubscriber: Send + Sync {
613 fn accepts(&self, _event: &VaultEvent) -> bool {
616 true
617 }
618
619 fn on_event(&self, event: &VaultEvent) -> Result<()>;
621
622 fn name(&self) -> &str;
624}
625
626pub struct EventBus {
628 subscribers: Vec<Box<dyn EventSubscriber>>,
629}
630
631impl EventBus {
632 pub fn new() -> Self {
634 Self {
635 subscribers: Vec::new(),
636 }
637 }
638
639 pub fn subscribe(&mut self, subscriber: Box<dyn EventSubscriber>) {
641 self.subscribers.push(subscriber);
642 }
643
644 pub fn emit(&self, event: &VaultEvent) {
649 for subscriber in &self.subscribers {
650 if subscriber.accepts(event) {
651 if let Err(e) = subscriber.on_event(event) {
652 eprintln!(
653 "[EventBus] subscriber '{}' failed on {}: {}",
654 subscriber.name(),
655 event.event_type(),
656 e
657 );
658 }
659 }
660 }
661 }
662
663 pub fn subscriber_count(&self) -> usize {
665 self.subscribers.len()
666 }
667}
668
669impl Default for EventBus {
670 fn default() -> Self {
671 Self::new()
672 }
673}
674
675pub struct AuditLogSubscriber {
681 sink: Box<dyn AuditSink>,
682}
683
684impl AuditLogSubscriber {
685 pub fn new(sink: Box<dyn AuditSink>) -> Self {
686 Self { sink }
687 }
688}
689
690impl EventSubscriber for AuditLogSubscriber {
691 fn on_event(&self, event: &VaultEvent) -> Result<()> {
692 use crate::audit::{AuditEntry, AuditEventType};
693
694 let (event_type, description, model_name, version, success) = match event {
695 VaultEvent::VaultCreated { vault, .. } => (
696 AuditEventType::VaultCreated,
697 format!("Vault '{}' created", vault),
698 None,
699 None,
700 true,
701 ),
702 VaultEvent::VaultUnlocked { vault, .. } => (
703 AuditEventType::AuthSuccess,
704 format!("Vault '{}' unlocked", vault),
705 None,
706 None,
707 true,
708 ),
709 VaultEvent::VaultLocked { vault, .. } => (
710 AuditEventType::VaultOpened,
711 format!("Vault '{}' locked", vault),
712 None,
713 None,
714 true,
715 ),
716 VaultEvent::ModelStored {
717 model, version: v, ..
718 } => (
719 AuditEventType::ModelStored,
720 format!("Model '{}' version {} stored", model, v),
721 Some(model.clone()),
722 Some(*v),
723 true,
724 ),
725 VaultEvent::ModelRetrieved {
726 model, version: v, ..
727 } => (
728 AuditEventType::ModelRetrieved,
729 format!("Model '{}' version {} retrieved", model, v),
730 Some(model.clone()),
731 Some(*v),
732 true,
733 ),
734 VaultEvent::ModelDeleted {
735 model, version: v, ..
736 } => (
737 AuditEventType::VersionDeleted,
738 format!("Model '{}' version {} deleted", model, v),
739 Some(model.clone()),
740 Some(*v),
741 true,
742 ),
743 VaultEvent::PassphraseChanged {
744 files_reencrypted, ..
745 } => (
746 AuditEventType::ConfigChanged,
747 format!(
748 "Passphrase changed, {} files re-encrypted",
749 files_reencrypted
750 ),
751 None,
752 None,
753 true,
754 ),
755 VaultEvent::IntegrityFailed {
756 model,
757 version: v,
758 expected,
759 actual,
760 ..
761 } => (
762 AuditEventType::IntegrityFailure,
763 format!(
764 "Integrity check failed for model '{}' version {}: expected={}, actual={}",
765 model, v, expected, actual
766 ),
767 Some(model.clone()),
768 Some(*v),
769 false,
770 ),
771 VaultEvent::ComplianceChecked { passed, .. } => (
772 AuditEventType::SecurityViolation,
773 format!(
774 "Compliance check: {}",
775 if *passed { "PASSED" } else { "FAILED" }
776 ),
777 None,
778 None,
779 *passed,
780 ),
781 };
782
783 self.sink.emit(AuditEntry {
784 timestamp: event.timestamp(),
785 event_type,
786 description,
787 model_name,
788 version,
789 success,
790 metadata: None,
791 })
792 }
793
794 fn name(&self) -> &str {
795 "AuditLogSubscriber"
796 }
797}
798
799pub struct VaultMetrics {
803 pub models_stored_total: std::sync::atomic::AtomicU64,
805 pub models_retrieved_total: std::sync::atomic::AtomicU64,
806 pub models_deleted_total: std::sync::atomic::AtomicU64,
807 pub bytes_stored_total: std::sync::atomic::AtomicU64,
808 pub errors_total: std::sync::atomic::AtomicU64,
809
810 pub vault_unlocked: std::sync::atomic::AtomicBool,
812}
813
814impl VaultMetrics {
815 pub fn new() -> Self {
816 Self {
817 models_stored_total: std::sync::atomic::AtomicU64::new(0),
818 models_retrieved_total: std::sync::atomic::AtomicU64::new(0),
819 models_deleted_total: std::sync::atomic::AtomicU64::new(0),
820 bytes_stored_total: std::sync::atomic::AtomicU64::new(0),
821 errors_total: std::sync::atomic::AtomicU64::new(0),
822 vault_unlocked: std::sync::atomic::AtomicBool::new(false),
823 }
824 }
825
826 pub fn snapshot(&self) -> MetricsSnapshot {
828 use std::sync::atomic::Ordering::Relaxed;
829 MetricsSnapshot {
830 models_stored_total: self.models_stored_total.load(Relaxed),
831 models_retrieved_total: self.models_retrieved_total.load(Relaxed),
832 models_deleted_total: self.models_deleted_total.load(Relaxed),
833 bytes_stored_total: self.bytes_stored_total.load(Relaxed),
834 errors_total: self.errors_total.load(Relaxed),
835 vault_unlocked: self.vault_unlocked.load(Relaxed),
836 }
837 }
838}
839
840impl Default for VaultMetrics {
841 fn default() -> Self {
842 Self::new()
843 }
844}
845
846#[derive(Debug, Clone, Serialize)]
848pub struct MetricsSnapshot {
849 pub models_stored_total: u64,
850 pub models_retrieved_total: u64,
851 pub models_deleted_total: u64,
852 pub bytes_stored_total: u64,
853 pub errors_total: u64,
854 pub vault_unlocked: bool,
855}
856
857pub struct MetricsSubscriber {
859 metrics: std::sync::Arc<VaultMetrics>,
860}
861
862impl MetricsSubscriber {
863 pub fn new(metrics: std::sync::Arc<VaultMetrics>) -> Self {
864 Self { metrics }
865 }
866}
867
868impl EventSubscriber for MetricsSubscriber {
869 fn on_event(&self, event: &VaultEvent) -> Result<()> {
870 use std::sync::atomic::Ordering::Relaxed;
871
872 match event {
873 VaultEvent::ModelStored { size, .. } => {
874 self.metrics.models_stored_total.fetch_add(1, Relaxed);
875 self.metrics.bytes_stored_total.fetch_add(*size, Relaxed);
876 }
877 VaultEvent::ModelRetrieved { .. } => {
878 self.metrics.models_retrieved_total.fetch_add(1, Relaxed);
879 }
880 VaultEvent::ModelDeleted { .. } => {
881 self.metrics.models_deleted_total.fetch_add(1, Relaxed);
882 }
883 VaultEvent::VaultUnlocked { .. } => {
884 self.metrics.vault_unlocked.store(true, Relaxed);
885 }
886 VaultEvent::VaultLocked { .. } => {
887 self.metrics.vault_unlocked.store(false, Relaxed);
888 }
889 VaultEvent::IntegrityFailed { .. } => {
890 self.metrics.errors_total.fetch_add(1, Relaxed);
891 }
892 _ => {}
893 }
894 Ok(())
895 }
896
897 fn name(&self) -> &str {
898 "MetricsSubscriber"
899 }
900}
901
902pub struct NullAuditSink;
904
905impl AuditSink for NullAuditSink {
906 fn emit(&self, _entry: crate::audit::AuditEntry) -> Result<()> {
907 Ok(())
908 }
909
910 fn query(&self, _limit: Option<usize>) -> Result<Vec<crate::audit::AuditEntry>> {
911 Ok(Vec::new())
912 }
913}
914
915#[cfg(test)]
920mod tests {
921 use super::*;
922
923 #[test]
924 fn test_aimv_uri_root() {
925 let uri = AimvUri::parse("aimv://").unwrap();
926 assert_eq!(uri.vault, None);
927 assert_eq!(uri.model, None);
928 assert_eq!(uri.version, None);
929 assert_eq!(uri.resource, None);
930 }
931
932 #[test]
933 fn test_aimv_uri_vault() {
934 let uri = AimvUri::parse("aimv://default/").unwrap();
935 assert_eq!(uri.vault, Some("default".into()));
936 assert_eq!(uri.model, None);
937 }
938
939 #[test]
940 fn test_aimv_uri_model_latest() {
941 let uri = AimvUri::parse("aimv://default/llama-3").unwrap();
942 assert_eq!(uri.vault, Some("default".into()));
943 assert_eq!(uri.model, Some("llama-3".into()));
944 assert_eq!(uri.version, None);
945 }
946
947 #[test]
948 fn test_aimv_uri_model_version() {
949 let uri = AimvUri::parse("aimv://default/llama-3@3").unwrap();
950 assert_eq!(uri.vault, Some("default".into()));
951 assert_eq!(uri.model, Some("llama-3".into()));
952 assert_eq!(uri.version, Some(3));
953 }
954
955 #[test]
956 fn test_aimv_uri_with_resource() {
957 let uri = AimvUri::parse("aimv://default/llama-3@3/card").unwrap();
958 assert_eq!(uri.vault, Some("default".into()));
959 assert_eq!(uri.model, Some("llama-3".into()));
960 assert_eq!(uri.version, Some(3));
961 assert_eq!(uri.resource, Some("card".into()));
962 }
963
964 #[test]
965 fn test_aimv_uri_stats_resource() {
966 let uri = AimvUri::parse("aimv://default/_stats").unwrap();
967 assert_eq!(uri.vault, Some("default".into()));
968 assert_eq!(uri.model, Some("_stats".into()));
969 }
970
971 #[test]
972 fn test_aimv_uri_with_query() {
973 let uri = AimvUri::parse("aimv://default/_events?since=2026-01-01&limit=100").unwrap();
974 assert_eq!(uri.vault, Some("default".into()));
975 assert_eq!(uri.model, Some("_events".into()));
976 assert_eq!(uri.query.get("since"), Some(&"2026-01-01".into()));
977 assert_eq!(uri.query.get("limit"), Some(&"100".into()));
978 }
979
980 #[test]
981 fn test_aimv_uri_roundtrip() {
982 let uri = AimvUri::parse("aimv://default/llama-3@3/card").unwrap();
983 let s = uri.to_string();
984 assert_eq!(s, "aimv://default/llama-3@3/card");
985 }
986
987 #[test]
988 fn test_aimv_uri_invalid_scheme() {
989 assert!(AimvUri::parse("http://default/").is_err());
990 }
991
992 #[test]
993 fn test_event_bus() {
994 use std::sync::{Arc, Mutex};
995
996 struct TestSubscriber {
997 events: Arc<Mutex<Vec<String>>>,
998 }
999
1000 impl EventSubscriber for TestSubscriber {
1001 fn on_event(&self, event: &VaultEvent) -> Result<()> {
1002 self.events
1003 .lock()
1004 .unwrap()
1005 .push(event.event_type().to_string());
1006 Ok(())
1007 }
1008 fn name(&self) -> &str {
1009 "TestSubscriber"
1010 }
1011 }
1012
1013 let events = Arc::new(Mutex::new(Vec::new()));
1014 let mut bus = EventBus::new();
1015 bus.subscribe(Box::new(TestSubscriber {
1016 events: events.clone(),
1017 }));
1018
1019 bus.emit(&VaultEvent::VaultCreated {
1020 vault: "test".into(),
1021 timestamp: Utc::now(),
1022 });
1023 bus.emit(&VaultEvent::ModelStored {
1024 vault: "test".into(),
1025 model: "m1".into(),
1026 version: 1,
1027 format: "pytorch".into(),
1028 size: 1024,
1029 checksum: "abc".into(),
1030 timestamp: Utc::now(),
1031 });
1032
1033 let collected = events.lock().unwrap();
1034 assert_eq!(collected.len(), 2);
1035 assert_eq!(collected[0], "vault_created");
1036 assert_eq!(collected[1], "model_stored");
1037 }
1038
1039 #[test]
1040 fn test_metrics_subscriber() {
1041 let metrics = std::sync::Arc::new(VaultMetrics::new());
1042 let sub = MetricsSubscriber::new(metrics.clone());
1043 let bus_sub: Box<dyn EventSubscriber> = Box::new(sub);
1044
1045 let mut bus = EventBus::new();
1046 bus.subscribe(bus_sub);
1047
1048 bus.emit(&VaultEvent::ModelStored {
1049 vault: "test".into(),
1050 model: "m1".into(),
1051 version: 1,
1052 format: "pytorch".into(),
1053 size: 2048,
1054 checksum: "abc".into(),
1055 timestamp: Utc::now(),
1056 });
1057 bus.emit(&VaultEvent::ModelRetrieved {
1058 vault: "test".into(),
1059 model: "m1".into(),
1060 version: 1,
1061 timestamp: Utc::now(),
1062 });
1063
1064 let snap = metrics.snapshot();
1065 assert_eq!(snap.models_stored_total, 1);
1066 assert_eq!(snap.models_retrieved_total, 1);
1067 assert_eq!(snap.bytes_stored_total, 2048);
1068 }
1069
1070 #[test]
1071 fn test_vault_state_display() {
1072 let s = VaultState::Locked {
1073 vault_name: "default".into(),
1074 model_count: 5,
1075 };
1076 assert!(format!("{}", s).contains("default"));
1077 }
1078
1079 #[test]
1080 fn test_aimv_uri_model_without_version_to_string() {
1081 let uri = AimvUri {
1083 vault: Some("default".into()),
1084 model: Some("llama-3".into()),
1085 version: None,
1086 resource: None,
1087 query: HashMap::new(),
1088 };
1089 let s = uri.to_string();
1090 assert_eq!(s, "aimv://default/llama-3");
1091 }
1092
1093 #[test]
1094 fn test_aimv_uri_with_empty_value_query() {
1095 let mut query = HashMap::new();
1097 query.insert("verbose".into(), String::new());
1098 let uri = AimvUri {
1099 vault: Some("default".into()),
1100 model: Some("test".into()),
1101 version: None,
1102 resource: None,
1103 query,
1104 };
1105 let s = uri.to_string();
1106 assert!(s.contains("?verbose"));
1107 assert!(!s.contains('='));
1108 }
1109
1110 #[test]
1111 fn test_aimv_uri_display_trait() {
1112 let uri = AimvUri::parse("aimv://v1/m1@1").unwrap();
1113 let displayed = format!("{}", uri);
1114 assert_eq!(displayed, "aimv://v1/m1@1");
1115 }
1116
1117 #[test]
1118 fn test_crypto_provider_hash_hex() {
1119 use crate::crypto::FipsCrypto;
1121 let crypto = FipsCrypto::new().unwrap();
1122 let hex_hash = crypto.hash_hex(b"hello");
1123 assert_eq!(hex_hash.len(), 64);
1124 assert!(hex_hash.chars().all(|c| c.is_ascii_hexdigit()));
1125 }
1126
1127 #[test]
1128 fn test_integrity_failed_event_through_audit_subscriber() {
1129 use std::sync::{Arc, Mutex};
1131
1132 struct CollectingSink {
1133 entries: Arc<Mutex<Vec<crate::audit::AuditEntry>>>,
1134 }
1135 impl AuditSink for CollectingSink {
1136 fn emit(&self, entry: crate::audit::AuditEntry) -> Result<()> {
1137 self.entries.lock().unwrap().push(entry);
1138 Ok(())
1139 }
1140 fn query(&self, _limit: Option<usize>) -> Result<Vec<crate::audit::AuditEntry>> {
1141 Ok(self.entries.lock().unwrap().clone())
1142 }
1143 }
1144
1145 let entries = Arc::new(Mutex::new(Vec::new()));
1146 let sink = CollectingSink {
1147 entries: entries.clone(),
1148 };
1149 let sub = AuditLogSubscriber::new(Box::new(sink));
1150
1151 let mut bus = EventBus::new();
1152 bus.subscribe(Box::new(sub));
1153
1154 bus.emit(&VaultEvent::IntegrityFailed {
1155 vault: "test".into(),
1156 model: "bad_model".into(),
1157 version: 1,
1158 expected: "abc123".into(),
1159 actual: "def456".into(),
1160 timestamp: Utc::now(),
1161 });
1162
1163 let collected = entries.lock().unwrap();
1164 assert_eq!(collected.len(), 1);
1165 assert!(!collected[0].success);
1166 assert!(collected[0].description.contains("bad_model"));
1167 }
1168
1169 #[test]
1170 fn test_metrics_subscriber_integrity_failed() {
1171 let metrics = std::sync::Arc::new(VaultMetrics::new());
1173 let sub = MetricsSubscriber::new(metrics.clone());
1174 let mut bus = EventBus::new();
1175 bus.subscribe(Box::new(sub));
1176
1177 bus.emit(&VaultEvent::IntegrityFailed {
1178 vault: "test".into(),
1179 model: "bad".into(),
1180 version: 1,
1181 expected: "a".into(),
1182 actual: "b".into(),
1183 timestamp: Utc::now(),
1184 });
1185
1186 let snap = metrics.snapshot();
1187 assert_eq!(snap.errors_total, 1);
1188 }
1189
1190 #[test]
1191 fn test_metrics_subscriber_lock_unlock() {
1192 let metrics = std::sync::Arc::new(VaultMetrics::new());
1193 let sub = MetricsSubscriber::new(metrics.clone());
1194 let mut bus = EventBus::new();
1195 bus.subscribe(Box::new(sub));
1196
1197 bus.emit(&VaultEvent::VaultUnlocked {
1198 vault: "test".into(),
1199 timestamp: Utc::now(),
1200 });
1201 assert!(metrics.snapshot().vault_unlocked);
1202
1203 bus.emit(&VaultEvent::VaultLocked {
1204 vault: "test".into(),
1205 timestamp: Utc::now(),
1206 });
1207 assert!(!metrics.snapshot().vault_unlocked);
1208 }
1209
1210 #[test]
1211 fn test_metrics_subscriber_delete() {
1212 let metrics = std::sync::Arc::new(VaultMetrics::new());
1213 let sub = MetricsSubscriber::new(metrics.clone());
1214 let mut bus = EventBus::new();
1215 bus.subscribe(Box::new(sub));
1216
1217 bus.emit(&VaultEvent::ModelDeleted {
1218 vault: "test".into(),
1219 model: "m1".into(),
1220 version: 1,
1221 timestamp: Utc::now(),
1222 });
1223
1224 assert_eq!(metrics.snapshot().models_deleted_total, 1);
1225 }
1226
1227 #[test]
1228 fn test_null_audit_sink() {
1229 let sink = NullAuditSink;
1230 let entry = crate::audit::AuditEntry {
1231 timestamp: Utc::now(),
1232 event_type: crate::audit::AuditEventType::VaultCreated,
1233 description: "test".into(),
1234 model_name: None,
1235 version: None,
1236 success: true,
1237 metadata: None,
1238 };
1239 assert!(sink.emit(entry).is_ok());
1240 assert!(sink.query(Some(10)).unwrap().is_empty());
1241 }
1242
1243 #[test]
1244 fn test_audit_log_subscriber_all_event_types() {
1245 use std::sync::{Arc, Mutex};
1247
1248 struct RecordSink {
1249 entries: Arc<Mutex<Vec<crate::audit::AuditEntry>>>,
1250 }
1251 impl AuditSink for RecordSink {
1252 fn emit(&self, entry: crate::audit::AuditEntry) -> Result<()> {
1253 self.entries.lock().unwrap().push(entry);
1254 Ok(())
1255 }
1256 fn query(&self, _: Option<usize>) -> Result<Vec<crate::audit::AuditEntry>> {
1257 Ok(Vec::new())
1258 }
1259 }
1260
1261 let entries = Arc::new(Mutex::new(Vec::new()));
1262 let sink = RecordSink {
1263 entries: entries.clone(),
1264 };
1265 let sub = AuditLogSubscriber::new(Box::new(sink));
1266 let mut bus = EventBus::new();
1267 bus.subscribe(Box::new(sub));
1268
1269 let now = Utc::now();
1270 bus.emit(&VaultEvent::VaultCreated {
1272 vault: "v".into(),
1273 timestamp: now,
1274 });
1275 bus.emit(&VaultEvent::VaultUnlocked {
1276 vault: "v".into(),
1277 timestamp: now,
1278 });
1279 bus.emit(&VaultEvent::VaultLocked {
1280 vault: "v".into(),
1281 timestamp: now,
1282 });
1283 bus.emit(&VaultEvent::ModelStored {
1284 vault: "v".into(),
1285 model: "m".into(),
1286 version: 1,
1287 format: "pt".into(),
1288 size: 100,
1289 checksum: "x".into(),
1290 timestamp: now,
1291 });
1292 bus.emit(&VaultEvent::ModelRetrieved {
1293 vault: "v".into(),
1294 model: "m".into(),
1295 version: 1,
1296 timestamp: now,
1297 });
1298 bus.emit(&VaultEvent::ModelDeleted {
1299 vault: "v".into(),
1300 model: "m".into(),
1301 version: 1,
1302 timestamp: now,
1303 });
1304 bus.emit(&VaultEvent::PassphraseChanged {
1305 vault: "v".into(),
1306 files_reencrypted: 3,
1307 timestamp: now,
1308 });
1309 bus.emit(&VaultEvent::ComplianceChecked {
1310 vault: "v".into(),
1311 passed: false,
1312 timestamp: now,
1313 });
1314
1315 let collected = entries.lock().unwrap();
1316 assert_eq!(collected.len(), 8);
1317 }
1318
1319 #[test]
1320 fn test_aimv_uri_to_string_with_version_and_query() {
1321 let uri = AimvUri {
1322 vault: Some("myvault".into()),
1323 model: Some("llama".into()),
1324 version: Some(3),
1325 resource: Some("weights".into()),
1326 query: {
1327 let mut m = std::collections::HashMap::new();
1328 m.insert("format".into(), "gguf".into());
1329 m.insert("flag".into(), String::new());
1330 m
1331 },
1332 };
1333 let s = uri.to_string();
1334 assert!(s.starts_with("aimv://"));
1335 assert!(s.contains("myvault"));
1336 assert!(s.contains("llama"));
1337 assert!(s.contains("@3"));
1338 assert!(s.contains("weights"));
1339 assert!(s.contains("format=gguf"));
1340 assert!(s.contains("flag"));
1341 }
1342
1343 #[test]
1344 fn test_vault_state_display_all_variants() {
1345 let states = vec![
1346 (VaultState::Uninitialized, "Uninitialized"),
1347 (
1348 VaultState::Locked {
1349 vault_name: "v1".into(),
1350 model_count: 3,
1351 },
1352 "Locked(v1)",
1353 ),
1354 (
1355 VaultState::Unlocked {
1356 vault_name: "v2".into(),
1357 model_count: 5,
1358 unlocked_at: Utc::now(),
1359 operations_count: 10,
1360 },
1361 "Unlocked(v2)",
1362 ),
1363 (
1364 VaultState::Error {
1365 message: "broken".into(),
1366 },
1367 "Error(broken)",
1368 ),
1369 ];
1370 for (state, expected) in &states {
1371 let display = format!("{}", state);
1372 assert_eq!(&display, expected);
1373 }
1374 }
1375
1376 #[test]
1377 fn test_aimv_uri_too_many_segments() {
1378 let result = AimvUri::parse("aimv://vault/model/resource/extra");
1379 assert!(result.is_err());
1380 let err_msg = format!("{}", result.unwrap_err());
1381 assert!(err_msg.contains("too many path segments"));
1382 }
1383
1384 #[test]
1385 fn test_aimv_uri_invalid_version_number() {
1386 let result = AimvUri::parse("aimv://vault/model@abc");
1387 assert!(result.is_err());
1388 let err_msg = format!("{}", result.unwrap_err());
1389 assert!(err_msg.contains("Invalid version number"));
1390 }
1391
1392 #[test]
1393 fn test_vault_event_timestamp_all_variants() {
1394 let now = Utc::now();
1395 let events = vec![
1396 VaultEvent::VaultCreated {
1397 vault: "v".into(),
1398 timestamp: now,
1399 },
1400 VaultEvent::VaultUnlocked {
1401 vault: "v".into(),
1402 timestamp: now,
1403 },
1404 VaultEvent::VaultLocked {
1405 vault: "v".into(),
1406 timestamp: now,
1407 },
1408 VaultEvent::ModelStored {
1409 vault: "v".into(),
1410 model: "m".into(),
1411 version: 1,
1412 format: "pt".into(),
1413 size: 100,
1414 checksum: "abc".into(),
1415 timestamp: now,
1416 },
1417 VaultEvent::ModelRetrieved {
1418 vault: "v".into(),
1419 model: "m".into(),
1420 version: 1,
1421 timestamp: now,
1422 },
1423 VaultEvent::ModelDeleted {
1424 vault: "v".into(),
1425 model: "m".into(),
1426 version: 1,
1427 timestamp: now,
1428 },
1429 VaultEvent::PassphraseChanged {
1430 vault: "v".into(),
1431 files_reencrypted: 3,
1432 timestamp: now,
1433 },
1434 VaultEvent::IntegrityFailed {
1435 vault: "v".into(),
1436 model: "m".into(),
1437 version: 1,
1438 expected: "a".into(),
1439 actual: "b".into(),
1440 timestamp: now,
1441 },
1442 VaultEvent::ComplianceChecked {
1443 vault: "v".into(),
1444 passed: true,
1445 timestamp: now,
1446 },
1447 ];
1448 for event in &events {
1449 assert_eq!(event.timestamp(), now);
1450 assert_eq!(event.vault_name(), "v");
1451 assert!(!event.event_type().is_empty());
1452 }
1453 }
1454
1455 #[test]
1456 fn test_vault_event_display() {
1457 let event = VaultEvent::VaultCreated {
1458 vault: "test".into(),
1459 timestamp: Utc::now(),
1460 };
1461 let display = format!("{}", event);
1462 assert!(display.contains("vault_created"));
1463 }
1464
1465 #[test]
1466 fn test_event_bus_error_handling() {
1467 struct FailingSubscriber;
1468 impl EventSubscriber for FailingSubscriber {
1469 fn on_event(&self, _event: &VaultEvent) -> Result<()> {
1470 Err(crate::error::VaultError::AuditError(
1471 "subscriber failed".into(),
1472 ))
1473 }
1474 fn name(&self) -> &str {
1475 "FailingSubscriber"
1476 }
1477 }
1478 let mut bus = EventBus::new();
1479 bus.subscribe(Box::new(FailingSubscriber));
1480 assert_eq!(bus.subscriber_count(), 1);
1481 bus.emit(&VaultEvent::VaultCreated {
1482 vault: "test".into(),
1483 timestamp: Utc::now(),
1484 });
1485 }
1486
1487 #[test]
1488 fn test_event_bus_selective_accepts() {
1489 use std::sync::{Arc, Mutex};
1490 struct SelectiveSubscriber {
1491 events: Arc<Mutex<Vec<String>>>,
1492 }
1493 impl EventSubscriber for SelectiveSubscriber {
1494 fn accepts(&self, event: &VaultEvent) -> bool {
1495 event.event_type() == "model_stored"
1496 }
1497 fn on_event(&self, event: &VaultEvent) -> Result<()> {
1498 self.events
1499 .lock()
1500 .unwrap()
1501 .push(event.event_type().to_string());
1502 Ok(())
1503 }
1504 fn name(&self) -> &str {
1505 "SelectiveSubscriber"
1506 }
1507 }
1508 let events = Arc::new(Mutex::new(Vec::new()));
1509 let mut bus = EventBus::new();
1510 bus.subscribe(Box::new(SelectiveSubscriber {
1511 events: events.clone(),
1512 }));
1513 bus.emit(&VaultEvent::VaultCreated {
1514 vault: "v".into(),
1515 timestamp: Utc::now(),
1516 });
1517 bus.emit(&VaultEvent::ModelStored {
1518 vault: "v".into(),
1519 model: "m".into(),
1520 version: 1,
1521 format: "pt".into(),
1522 size: 100,
1523 checksum: "abc".into(),
1524 timestamp: Utc::now(),
1525 });
1526 let collected = events.lock().unwrap();
1527 assert_eq!(collected.len(), 1);
1528 assert_eq!(collected[0], "model_stored");
1529 }
1530
1531 #[test]
1532 fn test_subscriber_name_methods() {
1533 let sink = NullAuditSink;
1534 let audit_sub = AuditLogSubscriber::new(Box::new(sink));
1535 assert_eq!(audit_sub.name(), "AuditLogSubscriber");
1536 let metrics = std::sync::Arc::new(VaultMetrics::new());
1537 let metrics_sub = MetricsSubscriber::new(metrics);
1538 assert_eq!(metrics_sub.name(), "MetricsSubscriber");
1539 }
1540
1541 #[test]
1542 fn test_metrics_subscriber_passphrase_changed() {
1543 let metrics = std::sync::Arc::new(VaultMetrics::new());
1544 let sub = MetricsSubscriber::new(metrics.clone());
1545 let mut bus = EventBus::new();
1546 bus.subscribe(Box::new(sub));
1547 bus.emit(&VaultEvent::PassphraseChanged {
1548 vault: "v".into(),
1549 files_reencrypted: 5,
1550 timestamp: Utc::now(),
1551 });
1552 bus.emit(&VaultEvent::ComplianceChecked {
1553 vault: "v".into(),
1554 passed: true,
1555 timestamp: Utc::now(),
1556 });
1557 let snap = metrics.snapshot();
1558 assert_eq!(snap.models_stored_total, 0);
1559 }
1560
1561 #[test]
1562 fn test_vault_metrics_default() {
1563 let metrics = VaultMetrics::default();
1564 let snap = metrics.snapshot();
1565 assert_eq!(snap.models_stored_total, 0);
1566 assert!(!snap.vault_unlocked);
1567 }
1568
1569 #[test]
1570 fn test_event_bus_default() {
1571 let bus = EventBus::default();
1572 assert_eq!(bus.subscriber_count(), 0);
1573 }
1574
1575 #[test]
1576 fn test_parse_query_empty_key() {
1577 let uri = crate::traits::AimvUri::parse("aimv://vault/model?=value&good=yes").unwrap();
1579 assert!(uri.query.contains_key("good"));
1581 assert!(!uri.query.contains_key(""));
1582 }
1583
1584 #[test]
1585 fn test_vault_event_display_format() {
1586 let event = VaultEvent::VaultUnlocked {
1588 vault: "test_vault".to_string(),
1589 timestamp: chrono::Utc::now(),
1590 };
1591 let display = format!("{}", event);
1592 assert!(!display.is_empty());
1593 }
1594
1595 #[test]
1596 fn test_async_blob_store_adapter() {
1597 use crate::storage::local::LocalBackend;
1599 use tokio::runtime::Runtime;
1600
1601 let rt = Runtime::new().unwrap();
1602 let temp_dir = tempfile::tempdir().unwrap();
1603 let backend = LocalBackend::new(temp_dir.path().to_path_buf()).unwrap();
1604 let adapter = AsyncBlobStoreAdapter::new(backend);
1605
1606 rt.block_on(async {
1607 let receipt = adapter.put("test_blob", b"hello blob").await.unwrap();
1609 assert_eq!(receipt.key, "test_blob");
1610 assert_eq!(receipt.size_bytes, 10);
1611
1612 let data = adapter.get("test_blob").await.unwrap();
1614 assert_eq!(data, b"hello blob");
1615
1616 assert!(adapter.exists("test_blob").await.unwrap());
1618
1619 let info = adapter.stat("test_blob").await.unwrap();
1621 assert_eq!(info.key, "test_blob");
1622 assert_eq!(info.size_bytes, 10);
1623
1624 let items = adapter.list(None).await.unwrap();
1626 assert_eq!(items.len(), 1);
1627 assert_eq!(items[0].key, "test_blob");
1628
1629 let deleted = adapter.delete("test_blob").await.unwrap();
1631 assert!(deleted);
1632
1633 assert!(!adapter.exists("test_blob").await.unwrap());
1635 });
1636 }
1637
1638 #[test]
1639 fn test_event_subscriber_default_accepts() {
1640 struct AllAcceptor;
1642 impl crate::traits::EventSubscriber for AllAcceptor {
1643 fn on_event(&self, _event: &VaultEvent) -> crate::error::Result<()> {
1644 Ok(())
1645 }
1646 fn name(&self) -> &str {
1647 "all_acceptor"
1648 }
1649 }
1650 let sub = AllAcceptor;
1651 let event = VaultEvent::ModelStored {
1652 vault: "v".into(),
1653 model: "m".into(),
1654 version: 1,
1655 format: "safetensors".into(),
1656 size: 100,
1657 checksum: "abc".into(),
1658 timestamp: chrono::Utc::now(),
1659 };
1660 assert!(sub.accepts(&event));
1661 }
1662}