Skip to main content

ito_domain/audit/
writer.rs

1//! Audit writer trait and no-op implementation.
2//!
3//! The `AuditWriter` trait is object-safe for dynamic dispatch and defines
4//! the single `append` method for recording audit events. The concrete
5//! filesystem writer lives in `ito-core`; this module provides only the
6//! trait and a no-op stub for testing.
7
8use super::event::AuditEvent;
9
10/// Trait for appending audit events to a log.
11///
12/// Implementations must be `Send + Sync` for use across async boundaries.
13/// The trait is object-safe to allow `dyn AuditWriter`.
14pub trait AuditWriter: Send + Sync {
15    /// Append a single event to the audit log.
16    fn append(&self, event: &AuditEvent) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
17}
18
19/// A no-op writer that silently discards all events.
20///
21/// Used when audit logging is disabled or unavailable (e.g., during `ito init`
22/// before the `.ito/` directory exists).
23pub struct NoopAuditWriter;
24
25impl AuditWriter for NoopAuditWriter {
26    fn append(&self, _event: &AuditEvent) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
27        Ok(())
28    }
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34    use crate::audit::event::{EventContext, SCHEMA_VERSION};
35
36    fn test_event() -> AuditEvent {
37        AuditEvent {
38            v: SCHEMA_VERSION,
39            ts: "2026-02-08T14:30:00.000Z".to_string(),
40            entity: "task".to_string(),
41            entity_id: "1.1".to_string(),
42            scope: Some("test-change".to_string()),
43            op: "create".to_string(),
44            from: None,
45            to: Some("pending".to_string()),
46            actor: "cli".to_string(),
47            by: "@test".to_string(),
48            meta: None,
49            ctx: EventContext {
50                session_id: "test-sid".to_string(),
51                harness_session_id: None,
52                branch: None,
53                worktree: None,
54                commit: None,
55            },
56        }
57    }
58
59    #[test]
60    fn noop_writer_returns_ok() {
61        let writer = NoopAuditWriter;
62        let event = test_event();
63        assert!(writer.append(&event).is_ok());
64    }
65
66    #[test]
67    fn noop_writer_is_object_safe() {
68        let writer: Box<dyn AuditWriter> = Box::new(NoopAuditWriter);
69        let event = test_event();
70        assert!(writer.append(&event).is_ok());
71    }
72
73    #[test]
74    fn noop_writer_is_send_sync() {
75        fn assert_send_sync<T: Send + Sync>() {}
76        assert_send_sync::<NoopAuditWriter>();
77    }
78
79    #[test]
80    fn trait_is_object_safe_for_dyn_dispatch() {
81        fn takes_writer(_w: &dyn AuditWriter) {}
82        let noop = NoopAuditWriter;
83        takes_writer(&noop);
84    }
85}