1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub enum AuditEvent {
10 VersionCreated { version_id: String, model: String, timestamp: DateTime<Utc> },
12 BranchCreated { branch: String, head_version_id: String, timestamp: DateTime<Utc> },
14 Rollback { from_version_id: String, to_version_id: String, timestamp: DateTime<Utc> },
16 DiffComputed { from_id: String, to_id: String, similarity: f64, timestamp: DateTime<Utc> },
18}
19
20impl AuditEvent {
21 pub fn kind(&self) -> &'static str {
23 match self {
24 AuditEvent::VersionCreated { .. } => "VersionCreated",
25 AuditEvent::BranchCreated { .. } => "BranchCreated",
26 AuditEvent::Rollback { .. } => "Rollback",
27 AuditEvent::DiffComputed { .. } => "DiffComputed",
28 }
29 }
30
31 pub fn timestamp(&self) -> DateTime<Utc> {
33 match self {
34 AuditEvent::VersionCreated { timestamp, .. }
35 | AuditEvent::BranchCreated { timestamp, .. }
36 | AuditEvent::Rollback { timestamp, .. }
37 | AuditEvent::DiffComputed { timestamp, .. } => *timestamp,
38 }
39 }
40}
41
42pub struct AuditLog {
44 events: Vec<AuditEvent>,
45}
46
47impl AuditLog {
48 pub fn new() -> Self { Self { events: Vec::new() } }
50
51 pub fn record(&mut self, event: AuditEvent) { self.events.push(event); }
53
54 pub fn events(&self) -> &[AuditEvent] { &self.events }
56
57 pub fn len(&self) -> usize { self.events.len() }
59
60 pub fn is_empty(&self) -> bool { self.events.is_empty() }
62
63 pub fn to_json(&self) -> Result<String, serde_json::Error> {
68 serde_json::to_string(&self.events)
69 }
70
71 pub fn events_of_kind(&self, kind: &str) -> Vec<&AuditEvent> {
73 self.events.iter().filter(|e| e.kind() == kind).collect()
74 }
75}
76
77impl Default for AuditLog {
78 fn default() -> Self { Self::new() }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_audit_log_record_increments_len() {
87 let mut log = AuditLog::new();
88 log.record(AuditEvent::VersionCreated {
89 version_id: "v1".into(),
90 model: "claude".into(),
91 timestamp: Utc::now(),
92 });
93 assert_eq!(log.len(), 1);
94 }
95
96 #[test]
97 fn test_audit_log_is_empty_initially() {
98 let log = AuditLog::new();
99 assert!(log.is_empty());
100 }
101
102 #[test]
103 fn test_audit_log_filter_by_kind_version_created() {
104 let mut log = AuditLog::new();
105 log.record(AuditEvent::VersionCreated {
106 version_id: "v1".into(),
107 model: "m".into(),
108 timestamp: Utc::now(),
109 });
110 log.record(AuditEvent::Rollback {
111 from_version_id: "v2".into(),
112 to_version_id: "v1".into(),
113 timestamp: Utc::now(),
114 });
115 assert_eq!(log.events_of_kind("VersionCreated").len(), 1);
116 assert_eq!(log.events_of_kind("Rollback").len(), 1);
117 }
118
119 #[test]
120 fn test_audit_log_to_json_contains_event_kind() {
121 let mut log = AuditLog::new();
122 log.record(AuditEvent::DiffComputed {
123 from_id: "a".into(),
124 to_id: "b".into(),
125 similarity: 0.8,
126 timestamp: Utc::now(),
127 });
128 let json = log.to_json().unwrap();
129 assert!(json.contains("DiffComputed"));
130 }
131
132 #[test]
133 fn test_audit_log_grows_monotonically() {
134 let mut log = AuditLog::new();
135 for i in 0..5u32 {
136 log.record(AuditEvent::VersionCreated {
137 version_id: format!("v{i}"),
138 model: "m".into(),
139 timestamp: Utc::now(),
140 });
141 assert_eq!(log.len(), (i + 1) as usize);
142 }
143 }
144
145 #[test]
146 fn test_audit_event_kind_labels_correct() {
147 assert_eq!(AuditEvent::VersionCreated { version_id: "".into(), model: "".into(), timestamp: Utc::now() }.kind(), "VersionCreated");
148 assert_eq!(AuditEvent::BranchCreated { branch: "".into(), head_version_id: "".into(), timestamp: Utc::now() }.kind(), "BranchCreated");
149 assert_eq!(AuditEvent::Rollback { from_version_id: "".into(), to_version_id: "".into(), timestamp: Utc::now() }.kind(), "Rollback");
150 assert_eq!(AuditEvent::DiffComputed { from_id: "".into(), to_id: "".into(), similarity: 0.0, timestamp: Utc::now() }.kind(), "DiffComputed");
151 }
152
153 #[test]
154 fn test_audit_log_events_of_kind_empty_when_no_match() {
155 let mut log = AuditLog::new();
156 log.record(AuditEvent::VersionCreated { version_id: "v1".into(), model: "m".into(), timestamp: Utc::now() });
157 assert!(log.events_of_kind("Rollback").is_empty());
158 }
159}