1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5pub enum GapReason {
6 BufferFull,
7 NetworkTimeout,
8 ProcessCrash,
9 SidecarUnreachable,
10 DiskFull,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct EvidenceGapOpenedV1 {
15 pub gap_id: String,
16 pub tenant_id: String,
17 pub source_instance_id: String,
18 pub first_missing_sequence: u64,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub last_missing_sequence: Option<u64>,
21 pub started_at: String,
22 pub detected_at: String,
23 pub reason: GapReason,
24 pub affected_event_types: Vec<String>,
25 pub runtime_version: String,
26 pub previous_evidence_hash: String,
27 pub signature: String,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct EvidenceGapClosedV1 {
32 pub gap_id: String,
33 pub last_missing_sequence: u64,
34 pub ended_at: String,
35 pub resolved_at: String,
36 pub total_missing_events: u64,
37 pub billing_period_status: BillingPeriodStatus,
38 pub signature: String,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum BillingPeriodStatus {
43 Complete,
44 Incomplete,
45 NonBillable,
46}
47
48#[cfg(test)]
49mod tests {
50 use crate::{BillingPeriodStatus, EvidenceGapClosedV1, EvidenceGapOpenedV1, GapReason};
51
52 #[test]
53 fn opened_serialization_round_trip() {
54 let opened = EvidenceGapOpenedV1 {
55 gap_id: "gap-1".to_owned(),
56 tenant_id: "tenant-1".to_owned(),
57 source_instance_id: "runtime-1".to_owned(),
58 first_missing_sequence: 7,
59 last_missing_sequence: None,
60 started_at: "2026-01-01T00:00:00Z".to_owned(),
61 detected_at: "2026-01-01T00:01:00Z".to_owned(),
62 reason: GapReason::NetworkTimeout,
63 affected_event_types: vec!["observation".to_owned()],
64 runtime_version: "1.0.0".to_owned(),
65 previous_evidence_hash: "sha256:abc".to_owned(),
66 signature: "signature".to_owned(),
67 };
68 let json = serde_json::to_string(&opened).expect("opened gap should serialize");
69 let decoded = serde_json::from_str(&json).expect("opened gap should deserialize");
70 assert_eq!(opened, decoded);
71 }
72
73 #[test]
74 fn closed_serialization_round_trip() {
75 let closed = EvidenceGapClosedV1 {
76 gap_id: "gap-1".to_owned(),
77 last_missing_sequence: 9,
78 ended_at: "2026-01-01T00:02:00Z".to_owned(),
79 resolved_at: "2026-01-01T00:03:00Z".to_owned(),
80 total_missing_events: 3,
81 billing_period_status: BillingPeriodStatus::Incomplete,
82 signature: "signature".to_owned(),
83 };
84 let json = serde_json::to_string(&closed).expect("closed gap should serialize");
85 let decoded = serde_json::from_str(&json).expect("closed gap should deserialize");
86 assert_eq!(closed, decoded);
87 }
88}