Skip to main content

mempill_core/application/
audit.rs

1#![allow(missing_docs)]
2//! AuditUseCase — read-only ledger query.
3//!
4//! Delegates to `engine::audit_ledger::query_ledger`. No Txn opened (read path).
5
6use std::sync::Arc;
7
8use mempill_types::TransactionTime;
9
10use crate::{
11    engine::audit_ledger::{self, AuditQuery},
12    error::MemError,
13    ports::PersistencePort,
14};
15
16use super::dto::{AuditQueryRequest, AuditQueryResponse};
17
18/// Use-case: retrieve ordered audit ledger entries for an agent (or specific claim).
19pub struct AuditUseCase<P>
20where
21    P: PersistencePort + Send + Sync + 'static,
22{
23    persistence: Arc<P>,
24}
25
26impl<P> AuditUseCase<P>
27where
28    P: PersistencePort + Send + Sync + 'static,
29{
30    pub fn new(persistence: Arc<P>) -> Self {
31        Self { persistence }
32    }
33
34    /// Read-only. Loads ledger entries via the audit ledger. No transaction needed.
35    pub fn execute(&self, req: AuditQueryRequest) -> Result<AuditQueryResponse, MemError> {
36        // Map from_tx_time DateTime<Utc> → TransactionTime (for the port).
37        let from_tx_time = req.from_tx_time.map(TransactionTime);
38
39        let query = AuditQuery {
40            agent_id: req.agent_id,
41            claim_ref: req.claim_ref,
42            from_tx_time,
43            limit: req.limit,
44        };
45
46        let result = audit_ledger::query_ledger(&*self.persistence, &query)
47            .map_err(|e| MemError::Persistence { source: Box::new(e) })?;
48
49        Ok(AuditQueryResponse { entries: result.entries })
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::ports::persistence::Txn;
57    use mempill_types::{
58        AgentId, Claim, ClaimEdge, ClaimRef, Disposition, LedgerEntry, LedgerEventKind,
59        TransactionTime, ValidityAssertion,
60    };
61    use std::sync::Mutex;
62    use chrono::Utc;
63
64    struct MockTxn(AgentId);
65    impl Txn for MockTxn {
66        fn agent_id(&self) -> &AgentId { &self.0 }
67    }
68
69    #[derive(Debug, thiserror::Error)]
70    #[error("mock")]
71    struct MockErr;
72
73    struct MockStore {
74        ledger: Mutex<Vec<LedgerEntry>>,
75    }
76
77    impl MockStore {
78        fn with_entries(entries: Vec<LedgerEntry>) -> Self {
79            Self { ledger: Mutex::new(entries) }
80        }
81    }
82
83    impl PersistencePort for MockStore {
84        type Transaction = MockTxn;
85        type Error = MockErr;
86        fn begin_atomic(&self, aid: &AgentId) -> Result<MockTxn, MockErr> { Ok(MockTxn(aid.clone())) }
87        fn append_claim(&self, _t: &mut MockTxn, c: &Claim) -> Result<ClaimRef, MockErr> { Ok(c.claim_ref().clone()) }
88        fn append_validity_assertion(&self, _t: &mut MockTxn, _a: &ValidityAssertion) -> Result<(), MockErr> { Ok(()) }
89        fn append_ledger_entry(&self, _t: &mut MockTxn, _e: &LedgerEntry) -> Result<(), MockErr> { Ok(()) }
90        fn append_claim_edge(&self, _t: &mut MockTxn, _e: &ClaimEdge) -> Result<(), MockErr> { Ok(()) }
91        fn commit(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
92        fn rollback(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
93        fn load_subject_line(&self, _a: &AgentId, _s: &str, _p: &str) -> Result<Vec<Claim>, MockErr> { Ok(vec![]) }
94        fn load_claim(&self, _a: &AgentId, _r: &ClaimRef) -> Result<Option<Claim>, MockErr> { Ok(None) }
95        fn load_validity_assertions_for(&self, _a: &AgentId, _r: &ClaimRef) -> Result<Vec<ValidityAssertion>, MockErr> { Ok(vec![]) }
96        fn load_ledger(&self, _a: &AgentId, _f: Option<&TransactionTime>, limit: usize) -> Result<Vec<LedgerEntry>, MockErr> {
97            // Return newest-first to match the real store's convention.
98            let mut entries = self.ledger.lock().unwrap().clone();
99            entries.reverse(); // newest first
100            entries.truncate(limit);
101            Ok(entries)
102        }
103        fn load_ledger_for_claims(&self, _a: &AgentId, _refs: &[ClaimRef]) -> Result<Vec<LedgerEntry>, MockErr> { Ok(vec![]) }
104        fn load_edges_for(&self, _a: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
105        fn load_injected_claims(&self, _a: &AgentId) -> Result<Vec<ClaimRef>, MockErr> { Ok(vec![]) }
106        fn load_lineage(&self, _a: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
107    }
108
109    fn make_entry(agent_id: &AgentId, at: chrono::DateTime<Utc>) -> LedgerEntry {
110        LedgerEntry {
111            entry_id: uuid::Uuid::new_v4(),
112            agent_id: agent_id.clone(),
113            claim_ref: ClaimRef::new_random(),
114            event_kind: LedgerEventKind::ClaimCommitted,
115            disposition: Disposition::CommittedCheap,
116            rationale: None,
117            recorded_at: TransactionTime(at),
118        }
119    }
120
121    #[test]
122    fn audit_empty_store_returns_empty() {
123        let store = Arc::new(MockStore::with_entries(vec![]));
124        let uc = AuditUseCase::new(Arc::clone(&store));
125        let resp = uc.execute(AuditQueryRequest {
126            agent_id: AgentId("a".into()),
127            claim_ref: None,
128            from_tx_time: None,
129            limit: 100,
130        }).unwrap();
131        assert!(resp.entries.is_empty());
132    }
133
134    #[test]
135    fn audit_returns_entries_in_chronological_asc_order() {
136        let agent = AgentId("a".into());
137        let t1 = chrono::Utc::now();
138        let t2 = t1 + chrono::Duration::seconds(10);
139        let e1 = make_entry(&agent, t1);
140        let e2 = make_entry(&agent, t2);
141        // Store them oldest-first; mock will reverse to newest-first.
142        let store = Arc::new(MockStore::with_entries(vec![e1.clone(), e2.clone()]));
143        let uc = AuditUseCase::new(Arc::clone(&store));
144        let resp = uc.execute(AuditQueryRequest {
145            agent_id: agent,
146            claim_ref: None,
147            from_tx_time: None,
148            limit: 100,
149        }).unwrap();
150        assert_eq!(resp.entries.len(), 2);
151        // audit_ledger reverses back to ASC; oldest entry must come first.
152        assert!(resp.entries[0].recorded_at.0 <= resp.entries[1].recorded_at.0,
153            "entries must be in chronological order (ASC)");
154    }
155}