Skip to main content

ai_model_vault/
traits.rs

1//! Core trait definitions for AI Model Vault architecture v2.
2//!
3//! These traits define the boundaries between subsystems, enabling:
4//! - Dependency injection (concrete types injected, never hard-coded)
5//! - Testability (mock implementations for unit tests)
6//! - Extensibility (new backends without changing core logic)
7//! - Composability (middleware/decorator pattern)
8//!
9//! # Architecture Layers
10//!
11//! ```text
12//! interface → service → domain ← infra
13//!                          ↑
14//!                       platform
15//! ```
16//!
17//! - **domain** traits define WHAT operations exist
18//! - **infra** modules provide HOW they're implemented
19
20use 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// ──────────────────────────────────────────────────────────────
30// Layer 3: Domain Types
31// ──────────────────────────────────────────────────────────────
32
33/// Observable vault state — agents can query this at any time.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum VaultState {
36    /// Vault directory does not exist.
37    Uninitialized,
38    /// Vault exists but is locked (key not loaded).
39    Locked {
40        vault_name: String,
41        model_count: usize,
42    },
43    /// Vault exists and is unlocked (key in memory).
44    Unlocked {
45        vault_name: String,
46        model_count: usize,
47        unlocked_at: DateTime<Utc>,
48        operations_count: u64,
49    },
50    /// Vault is in an error state.
51    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/// Parsed AIMV URI with typed components.
66///
67/// Format: `aimv://{vault}/{model}@{version}/{resource}?{query}`
68///
69/// Examples:
70/// - `aimv://` — root (list vaults)
71/// - `aimv://default/` — vault "default"
72/// - `aimv://default/llama-3` — model latest
73/// - `aimv://default/llama-3@3` — model version 3
74/// - `aimv://default/llama-3@3/card` — model card
75/// - `aimv://default/_stats` — vault statistics
76/// - `aimv://default/_events?since=2026-01-01` — filtered event log
77#[derive(Debug, Clone, PartialEq)]
78pub struct AimvUri {
79    /// Vault name (None for root).
80    pub vault: Option<String>,
81    /// Model name (None for vault-level resources).
82    pub model: Option<String>,
83    /// Version number (None for latest).
84    pub version: Option<u32>,
85    /// Sub-resource path (e.g., "card", "lineage", "_stats").
86    pub resource: Option<String>,
87    /// Query parameters.
88    pub query: HashMap<String, String>,
89}
90
91impl AimvUri {
92    /// URI scheme prefix.
93    pub const SCHEME: &'static str = "aimv://";
94
95    /// Parse an AIMV URI string.
96    ///
97    /// # Examples
98    /// ```
99    /// use ai_model_vault::traits::AimvUri;
100    ///
101    /// let uri = AimvUri::parse("aimv://default/llama-3@3/card").unwrap();
102    /// assert_eq!(uri.vault, Some("default".into()));
103    /// assert_eq!(uri.model, Some("llama-3".into()));
104    /// assert_eq!(uri.version, Some(3));
105    /// assert_eq!(uri.resource, Some("card".into()));
106    /// ```
107    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        // Split off query string
113        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        // Split path into segments
120        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        // Parse model@version
140        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    /// Parse query string into key-value pairs.
165    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
224// ──────────────────────────────────────────────────────────────
225// Layer 2: Infrastructure Traits
226// ──────────────────────────────────────────────────────────────
227
228/// Crypto operations — encrypt, decrypt, hash, derive keys.
229///
230/// Implementations:
231/// - `FipsCrypto` (default: AES-256-GCM + Argon2id)
232/// - Test mocks
233pub trait CryptoProvider: Send + Sync {
234    /// Derive an encryption key from a passphrase.
235    ///
236    /// Returns `(key, salt)` — the salt must be persisted for deterministic re-derivation.
237    fn derive_key(
238        &self,
239        passphrase: Vec<u8>,
240        salt: Option<Vec<u8>>,
241    ) -> Result<(SecureKey, Vec<u8>)>;
242
243    /// Encrypt data. Returns `nonce || ciphertext || auth_tag`.
244    fn encrypt(&self, data: &[u8], key: &SecureKey) -> Result<Vec<u8>>;
245
246    /// Decrypt data. Verifies authentication tag.
247    fn decrypt(&self, encrypted_data: &[u8], key: &SecureKey) -> Result<Vec<u8>>;
248
249    /// Compute SHA-256 hash.
250    fn hash(&self, data: &[u8]) -> Vec<u8>;
251
252    /// Compute SHA-256 hash as hex string.
253    fn hash_hex(&self, data: &[u8]) -> String {
254        hex::encode(self.hash(data))
255    }
256
257    /// Generate cryptographically secure random bytes.
258    fn random_bytes(&self, length: usize) -> Vec<u8>;
259}
260
261/// Blob storage — content-addressable, backend-agnostic.
262///
263/// Both local filesystem and cloud backends implement this trait.
264/// This is the sync version; async operations use `StorageBackend`.
265///
266/// Implementations:
267/// - `Storage` (local filesystem, compress + encrypt)
268/// - Cloud backends via `StorageBackend` (async)
269/// - `MemoryBlobStore` (for testing)
270pub trait BlobStore: Send + Sync {
271    /// Store data under the given key. Returns `(original_size, stored_size)`.
272    fn put(&self, key: &str, data: &[u8], encryption_key: &SecureKey) -> Result<(u64, u64)>;
273
274    /// Retrieve data by key.
275    fn get(&self, key: &str, encryption_key: &SecureKey) -> Result<Vec<u8>>;
276
277    /// Delete data by key. Returns true if the key existed.
278    fn remove(&self, key: &str) -> Result<bool>;
279
280    /// Check if a key exists.
281    fn exists(&self, key: &str) -> bool;
282
283    /// Get the size of stored data.
284    fn size(&self, key: &str) -> Result<u64>;
285
286    /// List all keys.
287    fn list_keys(&self) -> Result<Vec<String>>;
288
289    /// Get storage statistics.
290    fn stats(&self) -> Result<BlobStoreStats>;
291}
292
293/// Statistics returned by `BlobStore::stats()`.
294#[derive(Debug, Clone, Serialize)]
295pub struct BlobStoreStats {
296    pub total_size_bytes: u64,
297    pub file_count: usize,
298}
299
300/// Receipt returned after a successful async blob put.
301#[derive(Debug, Clone, Serialize)]
302pub struct BlobReceipt {
303    /// Storage key.
304    pub key: String,
305    /// Size in bytes of the stored data.
306    pub size_bytes: u64,
307    /// Timestamp of storage.
308    pub stored_at: DateTime<Utc>,
309}
310
311/// Metadata about a stored blob.
312#[derive(Debug, Clone, Serialize)]
313pub struct BlobInfo {
314    /// Storage key.
315    pub key: String,
316    /// Size in bytes.
317    pub size_bytes: u64,
318}
319
320/// Async blob storage — unified trait for local and cloud backends.
321///
322/// This is the async counterpart of [`BlobStore`]. Cloud backends
323/// (S3, Azure, GCS) and the local async backend implement this trait.
324///
325/// Implementations:
326/// - `StorageBackend` wrappers (via `AsyncBlobStoreAdapter`)
327/// - Can bridge to sync `BlobStore` via `tokio::runtime::Runtime::block_on`
328///
329/// This trait unifies the previously separate `BlobStore` (sync, local-only)
330/// and `StorageBackend` (async, cloud) abstractions.
331#[async_trait::async_trait]
332pub trait AsyncBlobStore: Send + Sync {
333    /// Store data under the given key.
334    async fn put(&self, key: &str, data: &[u8]) -> Result<BlobReceipt>;
335
336    /// Retrieve data by key.
337    async fn get(&self, key: &str) -> Result<Vec<u8>>;
338
339    /// Delete data by key. Returns true if the key existed.
340    async fn delete(&self, key: &str) -> Result<bool>;
341
342    /// Check if a key exists.
343    async fn exists(&self, key: &str) -> Result<bool>;
344
345    /// List all keys, optionally filtered by prefix.
346    async fn list(&self, prefix: Option<&str>) -> Result<Vec<BlobInfo>>;
347
348    /// Get metadata about a stored blob.
349    async fn stat(&self, key: &str) -> Result<BlobInfo>;
350}
351
352/// Adapter that wraps the existing `StorageBackend` trait as an `AsyncBlobStore`.
353///
354/// This bridges the legacy `StorageBackend` implementations (S3, Azure, Local)
355/// to the new unified `AsyncBlobStore` trait.
356pub struct AsyncBlobStoreAdapter<B: crate::storage::StorageBackend> {
357    inner: B,
358}
359
360impl<B: crate::storage::StorageBackend> AsyncBlobStoreAdapter<B> {
361    /// Wrap an existing `StorageBackend` implementation.
362    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/// Version metadata repository.
413///
414/// Implementations:
415/// - `VersionControl` (JSON file backend — current)
416/// - Future: `SqliteVersionRepo` (indexed, concurrent, ACID) — see `version_sqlite` module
417/// - `MemoryVersionRepo` (for testing)
418#[allow(clippy::too_many_arguments)]
419pub trait VersionRepo: Send + Sync {
420    /// Add a new version for a model.
421    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    /// Get a specific version, or the latest if version is None.
434    fn get_version(
435        &self,
436        model: &str,
437        version: Option<u32>,
438    ) -> Option<&crate::version::ModelVersion>;
439
440    /// List all versions of a model, sorted by version number.
441    fn list_versions(&self, model: &str) -> Vec<&crate::version::ModelVersion>;
442
443    /// Get the lineage chain for a version.
444    fn get_lineage(&self, model: &str, version: u32) -> Vec<&crate::version::ModelVersion>;
445
446    /// Delete a specific version. Returns true if it existed.
447    fn delete_version(&mut self, model: &str, version: u32) -> Result<bool>;
448
449    /// Clean up old versions, keeping `keep_count` most recent.
450    fn cleanup_old_versions(&mut self, model: &str, keep_count: usize) -> Result<Vec<u32>>;
451
452    /// Verify data checksum against stored checksum.
453    fn verify_checksum(&self, model: &str, version: u32, data: &[u8]) -> bool;
454
455    /// Update metadata for a version.
456    fn update_metadata(
457        &mut self,
458        model: &str,
459        version: u32,
460        key: &str,
461        value: String,
462    ) -> Result<()>;
463
464    /// Get metadata for a version.
465    fn get_metadata(&self, model: &str, version: u32, key: &str) -> Option<String>;
466
467    /// List all model names.
468    fn list_models(&self) -> Vec<String>;
469}
470
471/// Audit event sink.
472///
473/// Implementations:
474/// - `AuditLogger` (append-only file)
475/// - `BlockchainAudit` (Merkle-chained blocks)
476/// - `EventBusAuditSink` (forwards to event bus)
477/// - `NullAuditSink` (for testing / disabled audit)
478pub trait AuditSink: Send + Sync {
479    /// Emit an audit event.
480    fn emit(&self, entry: crate::audit::AuditEntry) -> Result<()>;
481
482    /// Query audit entries with optional limit.
483    fn query(&self, limit: Option<usize>) -> Result<Vec<crate::audit::AuditEntry>>;
484}
485
486// ──────────────────────────────────────────────────────────────
487// Event System
488// ──────────────────────────────────────────────────────────────
489
490/// Domain events — the canonical record of "what happened."
491///
492/// Every state-changing operation emits a `VaultEvent`. Subscribers
493/// (audit logger, metrics, webhooks, agents) react to these events.
494#[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    /// Get the timestamp for any event variant.
552    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    /// Get the vault name for any event variant.
567    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    /// Get a human-readable event type name.
582    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
608/// Event subscriber receives vault events.
609///
610/// Subscribers are registered with `EventBus` and called for each event.
611/// Errors from subscribers are logged but do NOT block the operation that emitted the event.
612pub trait EventSubscriber: Send + Sync {
613    /// Filter: return true to receive this event type.
614    /// Default: accept all events.
615    fn accepts(&self, _event: &VaultEvent) -> bool {
616        true
617    }
618
619    /// Handle the event. Errors are logged but don't block the operation.
620    fn on_event(&self, event: &VaultEvent) -> Result<()>;
621
622    /// Human-readable name for logging.
623    fn name(&self) -> &str;
624}
625
626/// Event bus dispatches events to registered subscribers.
627pub struct EventBus {
628    subscribers: Vec<Box<dyn EventSubscriber>>,
629}
630
631impl EventBus {
632    /// Create a new event bus with no subscribers.
633    pub fn new() -> Self {
634        Self {
635            subscribers: Vec::new(),
636        }
637    }
638
639    /// Register a subscriber.
640    pub fn subscribe(&mut self, subscriber: Box<dyn EventSubscriber>) {
641        self.subscribers.push(subscriber);
642    }
643
644    /// Dispatch an event to all accepting subscribers.
645    ///
646    /// Errors from subscribers are collected and logged but do NOT propagate
647    /// to the caller — audit/telemetry failures must never block vault operations.
648    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    /// Number of registered subscribers.
664    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
675// ──────────────────────────────────────────────────────────────
676// Built-in Subscribers
677// ──────────────────────────────────────────────────────────────
678
679/// Forwards vault events to an `AuditSink` by converting them to `AuditEntry`.
680pub 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
799/// Collects vault metrics in memory for observability.
800///
801/// Agents can query these metrics via REST API, MCP, or CLI.
802pub struct VaultMetrics {
803    // Counters
804    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    // Gauges (updated via events)
811    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    /// Get a serializable snapshot of metrics.
827    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/// Serializable snapshot of `VaultMetrics`.
847#[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
857/// Event subscriber that updates `VaultMetrics` counters.
858pub 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
902/// Null audit sink — discards all events. Used when audit logging is disabled.
903pub 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// ──────────────────────────────────────────────────────────────
916// Tests
917// ──────────────────────────────────────────────────────────────
918
919#[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        // Covers line 194 — model without version in to_string()
1082        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        // Covers line 217 — key-only query param (v.is_empty())
1096        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        // Covers line 256 — hash_hex default implementation via FipsCrypto
1120        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        // Covers lines 602, 603 — IntegrityFailed arm in AuditLogSubscriber
1130        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        // Covers lines 860, 863 — IntegrityFailed arm in MetricsSubscriber
1172        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        // Cover all AuditLogSubscriber match arms
1246        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        // Emit all event variants
1271        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        // Covers L173 — parse_query filters out empty keys
1578        let uri = crate::traits::AimvUri::parse("aimv://vault/model?=value&good=yes").unwrap();
1579        // Empty key should be filtered out
1580        assert!(uri.query.contains_key("good"));
1581        assert!(!uri.query.contains_key(""));
1582    }
1583
1584    #[test]
1585    fn test_vault_event_display_format() {
1586        // Covers L622 — Display impl for VaultEvent
1587        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        // Covers L369, L376-414 — AsyncBlobStoreAdapter with LocalBackend
1598        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            // put
1608            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            // get
1613            let data = adapter.get("test_blob").await.unwrap();
1614            assert_eq!(data, b"hello blob");
1615
1616            // exists
1617            assert!(adapter.exists("test_blob").await.unwrap());
1618
1619            // stat
1620            let info = adapter.stat("test_blob").await.unwrap();
1621            assert_eq!(info.key, "test_blob");
1622            assert_eq!(info.size_bytes, 10);
1623
1624            // list
1625            let items = adapter.list(None).await.unwrap();
1626            assert_eq!(items.len(), 1);
1627            assert_eq!(items[0].key, "test_blob");
1628
1629            // delete
1630            let deleted = adapter.delete("test_blob").await.unwrap();
1631            assert!(deleted);
1632
1633            // verify deleted
1634            assert!(!adapter.exists("test_blob").await.unwrap());
1635        });
1636    }
1637
1638    #[test]
1639    fn test_event_subscriber_default_accepts() {
1640        // Test the default `accepts()` method which returns `true` for all events.
1641        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}