1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10pub use ironflow_store::entities::LogStream;
11use ironflow_store::models::{
12 ApprovalRequirement, Assignee, ProviderAccountWindow, RunStatus, ScheduleMissReason, StepKind,
13};
14
15fn default_approval_count() -> u32 {
18 1
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
38#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
39pub struct RunCreatedEvent {
40 pub run_id: Uuid,
42 pub workflow_name: String,
44 pub at: DateTime<Utc>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
75#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
76pub struct RunStatusChangedEvent {
77 pub run_id: Uuid,
79 pub workflow_name: String,
81 pub from: RunStatus,
83 pub to: RunStatus,
85 pub error: Option<String>,
87 pub cost_usd: Decimal,
89 pub duration_ms: u64,
91 #[serde(default)]
93 pub labels: HashMap<String, String>,
94 pub at: DateTime<Utc>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
122#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
123pub struct RunFailedEvent {
124 pub run_id: Uuid,
126 pub workflow_name: String,
128 pub error: Option<String>,
130 pub cost_usd: Decimal,
132 pub duration_ms: u64,
134 #[serde(default)]
136 pub labels: HashMap<String, String>,
137 pub at: DateTime<Utc>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
162#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
163pub struct RunBudgetExceededEvent {
164 pub run_id: Uuid,
166 pub workflow_name: String,
168 pub limit_usd: Decimal,
170 pub spent_usd: Decimal,
172 pub step_budget_usd: Decimal,
174 pub at: DateTime<Utc>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
197#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
198pub struct RetryForcedEvent {
199 pub run_id: Uuid,
201 pub workflow_name: String,
203 pub original_version: String,
205 pub current_version: String,
207 pub at: DateTime<Utc>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
234#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
235pub struct StepCompletedEvent {
236 pub run_id: Uuid,
238 pub step_id: Uuid,
240 pub step_name: String,
242 #[cfg_attr(feature = "openapi", schema(value_type = String))]
244 pub kind: StepKind,
245 pub duration_ms: u64,
247 pub cost_usd: Decimal,
249 pub at: DateTime<Utc>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
274#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
275pub struct StepFailedEvent {
276 pub run_id: Uuid,
278 pub step_id: Uuid,
280 pub step_name: String,
282 #[cfg_attr(feature = "openapi", schema(value_type = String))]
284 pub kind: StepKind,
285 pub error: String,
287 pub at: DateTime<Utc>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
313#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
314pub struct ApprovalRequestedEvent {
315 pub run_id: Uuid,
317 pub step_id: Uuid,
319 pub message: String,
321 #[serde(default)]
324 pub requirement: Option<ApprovalRequirement>,
325 pub at: DateTime<Utc>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
356#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
357pub struct ApprovalGrantedEvent {
358 pub run_id: Uuid,
360 #[serde(default)]
362 pub step_id: Option<Uuid>,
363 pub approved_by: String,
365 #[serde(default)]
369 pub user_id: Option<Uuid>,
370 #[serde(default = "default_approval_count")]
372 pub approvals_received: u32,
373 #[serde(default = "default_approval_count")]
375 pub approvals_required: u32,
376 #[serde(default)]
378 pub requirement: Option<ApprovalRequirement>,
379 pub at: DateTime<Utc>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
404#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
405pub struct ApprovalRejectedEvent {
406 pub run_id: Uuid,
408 #[serde(default)]
410 pub step_id: Option<Uuid>,
411 pub rejected_by: String,
413 #[serde(default)]
417 pub user_id: Option<Uuid>,
418 #[serde(default)]
421 pub reason: Option<String>,
422 #[serde(default)]
424 pub requirement: Option<ApprovalRequirement>,
425 pub at: DateTime<Utc>,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
457#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
458pub struct ApprovalEscalatedEvent {
459 pub run_id: Uuid,
461 pub step_id: Uuid,
463 pub step_name: String,
465 pub stage: u32,
467 pub policy: String,
469 pub action: String,
471 pub reason: String,
473 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
475 pub assignee: Option<Assignee>,
476 pub at: DateTime<Utc>,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
501#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
502pub struct LogLineEvent {
503 #[serde(default)]
512 pub id: Uuid,
513 pub run_id: Uuid,
515 pub step_id: Uuid,
517 pub step_name: String,
519 pub stream: LogStream,
521 pub line: String,
523 pub at: DateTime<Utc>,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
544#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
545pub struct UserSignedInEvent {
546 pub user_id: Uuid,
548 pub username: String,
550 pub at: DateTime<Utc>,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize)]
571#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
572pub struct UserSignedUpEvent {
573 pub user_id: Uuid,
575 pub username: String,
577 pub at: DateTime<Utc>,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
598#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
599pub struct UserSignedOutEvent {
600 pub user_id: Uuid,
602 pub at: DateTime<Utc>,
604}
605
606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
617#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
618#[serde(rename_all = "snake_case")]
619pub enum ProviderAccountChange {
620 Created,
622 Updated,
624 Deleted,
626 TokenReplaced,
628}
629
630impl ProviderAccountChange {
631 pub fn as_str(&self) -> &'static str {
633 match self {
634 Self::Created => "created",
635 Self::Updated => "updated",
636 Self::Deleted => "deleted",
637 Self::TokenReplaced => "token_replaced",
638 }
639 }
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize)]
662#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
663pub struct ProviderAccountUpdatedEvent {
664 pub account_id: Uuid,
666 pub name: String,
668 pub change: ProviderAccountChange,
670 pub at: DateTime<Utc>,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
692#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
693pub struct ProviderAccountUsageUpdatedEvent {
694 pub account_id: Uuid,
696 pub name: String,
698 pub windows: Vec<ProviderAccountWindow>,
700 pub at: DateTime<Utc>,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize)]
728#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
729pub struct SignalAwaitedEvent {
730 pub run_id: Uuid,
732 pub step_id: Uuid,
734 pub step_name: String,
736 pub name: String,
738 pub key: String,
740 pub deadline_at: DateTime<Utc>,
742 pub at: DateTime<Utc>,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize)]
768#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
769pub struct SignalReceivedEvent {
770 pub signal_id: Uuid,
772 pub name: String,
774 pub key: String,
776 pub resumed_runs: Vec<Uuid>,
778 pub at: DateTime<Utc>,
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize)]
809#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
810pub struct ScheduleOccurrencesMissedEvent {
811 pub schedule_id: Uuid,
813 pub workflow_name: String,
815 pub reason: ScheduleMissReason,
817 pub count: u64,
819 pub first: DateTime<Utc>,
821 pub last: DateTime<Utc>,
823 pub at: DateTime<Utc>,
825}
826
827#[derive(Debug, Clone, Serialize, Deserialize)]
847#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
848pub struct WorkflowPausedEvent {
849 pub workflow_name: String,
851 #[serde(default)]
854 pub user_id: Option<Uuid>,
855 pub at: DateTime<Utc>,
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize)]
879#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
880pub struct WorkflowResumedEvent {
881 pub workflow_name: String,
883 #[serde(default)]
886 pub user_id: Option<Uuid>,
887 pub at: DateTime<Utc>,
889}
890
891#[derive(Debug, Clone, Serialize, Deserialize)]
923#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
924#[serde(tag = "type", rename_all = "snake_case")]
925pub enum Event {
926 RunCreated(RunCreatedEvent),
929
930 RunStatusChanged(RunStatusChangedEvent),
932
933 RunFailed(RunFailedEvent),
939
940 RunBudgetExceeded(RunBudgetExceededEvent),
948
949 RetryForced(RetryForcedEvent),
956
957 StepCompleted(StepCompletedEvent),
960
961 StepFailed(StepFailedEvent),
963
964 ApprovalRequested(ApprovalRequestedEvent),
967
968 ApprovalGranted(ApprovalGrantedEvent),
970
971 ApprovalRejected(ApprovalRejectedEvent),
973
974 ApprovalEscalated(ApprovalEscalatedEvent),
976
977 LogLine(LogLineEvent),
983
984 UserSignedIn(UserSignedInEvent),
987
988 UserSignedUp(UserSignedUpEvent),
990
991 UserSignedOut(UserSignedOutEvent),
993
994 #[serde(rename = "provider_account.updated")]
997 ProviderAccountUpdated(ProviderAccountUpdatedEvent),
998
999 #[serde(rename = "provider_account.usage_updated")]
1001 ProviderAccountUsageUpdated(ProviderAccountUsageUpdatedEvent),
1002
1003 SignalAwaited(SignalAwaitedEvent),
1006
1007 SignalReceived(SignalReceivedEvent),
1009
1010 ScheduleOccurrencesMissed(ScheduleOccurrencesMissedEvent),
1015
1016 WorkflowPaused(WorkflowPausedEvent),
1019
1020 WorkflowResumed(WorkflowResumedEvent),
1022}
1023
1024impl Event {
1025 pub const RUN_CREATED: &'static str = "run_created";
1027 pub const RUN_STATUS_CHANGED: &'static str = "run_status_changed";
1029 pub const RUN_FAILED: &'static str = "run_failed";
1031 pub const RUN_BUDGET_EXCEEDED: &'static str = "run_budget_exceeded";
1033 pub const RETRY_FORCED: &'static str = "retry_forced";
1035 pub const STEP_COMPLETED: &'static str = "step_completed";
1037 pub const STEP_FAILED: &'static str = "step_failed";
1039 pub const APPROVAL_REQUESTED: &'static str = "approval_requested";
1041 pub const APPROVAL_GRANTED: &'static str = "approval_granted";
1043 pub const APPROVAL_REJECTED: &'static str = "approval_rejected";
1045 pub const APPROVAL_ESCALATED: &'static str = "approval_escalated";
1047 pub const LOG_LINE: &'static str = "log_line";
1049 pub const USER_SIGNED_IN: &'static str = "user_signed_in";
1051 pub const USER_SIGNED_UP: &'static str = "user_signed_up";
1053 pub const USER_SIGNED_OUT: &'static str = "user_signed_out";
1055 pub const PROVIDER_ACCOUNT_UPDATED: &'static str = "provider_account.updated";
1057 pub const PROVIDER_ACCOUNT_USAGE_UPDATED: &'static str = "provider_account.usage_updated";
1060 pub const SIGNAL_AWAITED: &'static str = "signal_awaited";
1062 pub const SIGNAL_RECEIVED: &'static str = "signal_received";
1064 pub const SCHEDULE_OCCURRENCES_MISSED: &'static str = "schedule_occurrences_missed";
1067 pub const WORKFLOW_PAUSED: &'static str = "workflow_paused";
1069 pub const WORKFLOW_RESUMED: &'static str = "workflow_resumed";
1071
1072 pub const ALL: &'static [&'static str] = &[
1088 Self::RUN_CREATED,
1089 Self::RUN_STATUS_CHANGED,
1090 Self::RUN_FAILED,
1091 Self::RUN_BUDGET_EXCEEDED,
1092 Self::STEP_COMPLETED,
1093 Self::STEP_FAILED,
1094 Self::APPROVAL_REQUESTED,
1095 Self::APPROVAL_GRANTED,
1096 Self::APPROVAL_REJECTED,
1097 Self::APPROVAL_ESCALATED,
1098 Self::LOG_LINE,
1099 Self::USER_SIGNED_IN,
1100 Self::USER_SIGNED_UP,
1101 Self::USER_SIGNED_OUT,
1102 Self::RETRY_FORCED,
1103 Self::PROVIDER_ACCOUNT_UPDATED,
1104 Self::PROVIDER_ACCOUNT_USAGE_UPDATED,
1105 Self::SIGNAL_AWAITED,
1106 Self::SIGNAL_RECEIVED,
1107 Self::SCHEDULE_OCCURRENCES_MISSED,
1108 Self::WORKFLOW_PAUSED,
1109 Self::WORKFLOW_RESUMED,
1110 ];
1111
1112 #[deny(unreachable_patterns)]
1131 pub fn event_type(&self) -> &'static str {
1132 match self {
1133 Event::RunCreated(_) => Self::RUN_CREATED,
1134 Event::RunStatusChanged(_) => Self::RUN_STATUS_CHANGED,
1135 Event::RunFailed(_) => Self::RUN_FAILED,
1136 Event::RunBudgetExceeded(_) => Self::RUN_BUDGET_EXCEEDED,
1137 Event::RetryForced(_) => Self::RETRY_FORCED,
1138 Event::StepCompleted(_) => Self::STEP_COMPLETED,
1139 Event::StepFailed(_) => Self::STEP_FAILED,
1140 Event::ApprovalRequested(_) => Self::APPROVAL_REQUESTED,
1141 Event::ApprovalGranted(_) => Self::APPROVAL_GRANTED,
1142 Event::ApprovalRejected(_) => Self::APPROVAL_REJECTED,
1143 Event::ApprovalEscalated(_) => Self::APPROVAL_ESCALATED,
1144 Event::LogLine(_) => Self::LOG_LINE,
1145 Event::UserSignedIn(_) => Self::USER_SIGNED_IN,
1146 Event::UserSignedUp(_) => Self::USER_SIGNED_UP,
1147 Event::UserSignedOut(_) => Self::USER_SIGNED_OUT,
1148 Event::ProviderAccountUpdated(_) => Self::PROVIDER_ACCOUNT_UPDATED,
1149 Event::ProviderAccountUsageUpdated(_) => Self::PROVIDER_ACCOUNT_USAGE_UPDATED,
1150 Event::SignalAwaited(_) => Self::SIGNAL_AWAITED,
1151 Event::SignalReceived(_) => Self::SIGNAL_RECEIVED,
1152 Event::ScheduleOccurrencesMissed(_) => Self::SCHEDULE_OCCURRENCES_MISSED,
1153 Event::WorkflowPaused(_) => Self::WORKFLOW_PAUSED,
1154 Event::WorkflowResumed(_) => Self::WORKFLOW_RESUMED,
1155 }
1156 }
1157
1158 #[deny(unreachable_patterns)]
1181 pub fn run_id(&self) -> Option<Uuid> {
1182 match self {
1183 Event::RunCreated(e) => Some(e.run_id),
1184 Event::RunStatusChanged(e) => Some(e.run_id),
1185 Event::RunFailed(e) => Some(e.run_id),
1186 Event::RunBudgetExceeded(e) => Some(e.run_id),
1187 Event::RetryForced(e) => Some(e.run_id),
1188 Event::StepCompleted(e) => Some(e.run_id),
1189 Event::StepFailed(e) => Some(e.run_id),
1190 Event::ApprovalRequested(e) => Some(e.run_id),
1191 Event::ApprovalGranted(e) => Some(e.run_id),
1192 Event::ApprovalRejected(e) => Some(e.run_id),
1193 Event::ApprovalEscalated(e) => Some(e.run_id),
1194 Event::LogLine(e) => Some(e.run_id),
1195 Event::SignalAwaited(e) => Some(e.run_id),
1196 Event::UserSignedIn(_)
1197 | Event::UserSignedUp(_)
1198 | Event::UserSignedOut(_)
1199 | Event::ProviderAccountUpdated(_)
1200 | Event::ProviderAccountUsageUpdated(_)
1201 | Event::SignalReceived(_)
1202 | Event::ScheduleOccurrencesMissed(_)
1203 | Event::WorkflowPaused(_)
1204 | Event::WorkflowResumed(_) => None,
1205 }
1206 }
1207
1208 #[deny(unreachable_patterns)]
1238 pub fn step_id(&self) -> Option<Uuid> {
1239 match self {
1240 Event::StepCompleted(e) => Some(e.step_id),
1241 Event::StepFailed(e) => Some(e.step_id),
1242 Event::ApprovalRequested(e) => Some(e.step_id),
1243 Event::ApprovalEscalated(e) => Some(e.step_id),
1244 Event::ApprovalGranted(e) => e.step_id,
1245 Event::ApprovalRejected(e) => e.step_id,
1246 Event::SignalAwaited(e) => Some(e.step_id),
1247 Event::RunCreated(_)
1248 | Event::RunStatusChanged(_)
1249 | Event::RunFailed(_)
1250 | Event::RunBudgetExceeded(_)
1251 | Event::RetryForced(_)
1252 | Event::LogLine(_)
1253 | Event::UserSignedIn(_)
1254 | Event::UserSignedUp(_)
1255 | Event::UserSignedOut(_)
1256 | Event::ProviderAccountUpdated(_)
1257 | Event::ProviderAccountUsageUpdated(_)
1258 | Event::SignalReceived(_)
1259 | Event::ScheduleOccurrencesMissed(_)
1260 | Event::WorkflowPaused(_)
1261 | Event::WorkflowResumed(_) => None,
1262 }
1263 }
1264
1265 #[deny(unreachable_patterns)]
1293 pub fn user_id(&self) -> Option<Uuid> {
1294 match self {
1295 Event::UserSignedIn(e) => Some(e.user_id),
1296 Event::UserSignedUp(e) => Some(e.user_id),
1297 Event::UserSignedOut(e) => Some(e.user_id),
1298 Event::ApprovalGranted(e) => e.user_id,
1299 Event::ApprovalRejected(e) => e.user_id,
1300 Event::WorkflowPaused(e) => e.user_id,
1301 Event::WorkflowResumed(e) => e.user_id,
1302 Event::RunCreated(_)
1303 | Event::RunStatusChanged(_)
1304 | Event::RunFailed(_)
1305 | Event::RunBudgetExceeded(_)
1306 | Event::RetryForced(_)
1307 | Event::StepCompleted(_)
1308 | Event::StepFailed(_)
1309 | Event::ApprovalRequested(_)
1310 | Event::ApprovalEscalated(_)
1311 | Event::LogLine(_)
1312 | Event::ProviderAccountUpdated(_)
1313 | Event::ProviderAccountUsageUpdated(_)
1314 | Event::SignalAwaited(_)
1315 | Event::SignalReceived(_)
1316 | Event::ScheduleOccurrencesMissed(_) => None,
1317 }
1318 }
1319}
1320
1321#[cfg(test)]
1322mod tests {
1323 use super::*;
1324
1325 #[test]
1326 fn run_status_changed_serde_roundtrip() {
1327 let event = Event::RunStatusChanged(RunStatusChangedEvent {
1328 run_id: Uuid::now_v7(),
1329 workflow_name: "deploy".to_string(),
1330 from: RunStatus::Running,
1331 to: RunStatus::Completed,
1332 error: None,
1333 cost_usd: Decimal::new(42, 2),
1334 duration_ms: 5000,
1335 labels: HashMap::new(),
1336 at: Utc::now(),
1337 });
1338
1339 let json = serde_json::to_string(&event).expect("serialize");
1340 let back: Event = serde_json::from_str(&json).expect("deserialize");
1341
1342 assert_eq!(back.event_type(), "run_status_changed");
1343 assert!(json.contains("\"type\":\"run_status_changed\""));
1344 }
1345
1346 #[test]
1347 fn run_failed_serde_roundtrip() {
1348 let event = Event::RunFailed(RunFailedEvent {
1349 run_id: Uuid::now_v7(),
1350 workflow_name: "deploy".to_string(),
1351 error: Some("step crashed".to_string()),
1352 cost_usd: Decimal::new(10, 2),
1353 duration_ms: 3000,
1354 labels: HashMap::new(),
1355 at: Utc::now(),
1356 });
1357
1358 let json = serde_json::to_string(&event).expect("serialize");
1359 let back: Event = serde_json::from_str(&json).expect("deserialize");
1360
1361 assert_eq!(back.event_type(), "run_failed");
1362 assert!(json.contains("\"type\":\"run_failed\""));
1363 assert!(json.contains("step crashed"));
1364 }
1365
1366 #[test]
1367 fn run_budget_exceeded_serde_roundtrip() {
1368 let event = Event::RunBudgetExceeded(RunBudgetExceededEvent {
1369 run_id: Uuid::now_v7(),
1370 workflow_name: "deploy".to_string(),
1371 limit_usd: Decimal::new(200, 2),
1372 spent_usd: Decimal::new(180, 2),
1373 step_budget_usd: Decimal::new(50, 2),
1374 at: Utc::now(),
1375 });
1376
1377 let json = serde_json::to_string(&event).expect("serialize");
1378 let back: Event = serde_json::from_str(&json).expect("deserialize");
1379
1380 assert_eq!(back.event_type(), "run_budget_exceeded");
1381 assert!(json.contains("\"type\":\"run_budget_exceeded\""));
1382 assert!(json.contains("limit_usd"));
1383 assert!(json.contains("step_budget_usd"));
1384 }
1385
1386 #[test]
1387 fn all_contains_run_budget_exceeded() {
1388 assert!(Event::ALL.contains(&Event::RUN_BUDGET_EXCEEDED));
1389 }
1390
1391 #[test]
1392 fn user_signed_in_serde_roundtrip() {
1393 let event = Event::UserSignedIn(UserSignedInEvent {
1394 user_id: Uuid::now_v7(),
1395 username: "alice".to_string(),
1396 at: Utc::now(),
1397 });
1398
1399 let json = serde_json::to_string(&event).expect("serialize");
1400 let back: Event = serde_json::from_str(&json).expect("deserialize");
1401
1402 assert_eq!(back.event_type(), "user_signed_in");
1403 assert!(json.contains("alice"));
1404 }
1405
1406 #[test]
1407 fn step_failed_serde_roundtrip() {
1408 let event = Event::StepFailed(StepFailedEvent {
1409 run_id: Uuid::now_v7(),
1410 step_id: Uuid::now_v7(),
1411 step_name: "build".to_string(),
1412 kind: StepKind::Shell,
1413 error: "exit code 1".to_string(),
1414 at: Utc::now(),
1415 });
1416
1417 let json = serde_json::to_string(&event).expect("serialize");
1418 let back: Event = serde_json::from_str(&json).expect("deserialize");
1419
1420 assert_eq!(back.event_type(), "step_failed");
1421 }
1422
1423 #[test]
1424 fn legacy_approval_granted_defaults_to_a_single_vote() {
1425 let raw = r#"{"type":"approval_granted","run_id":"01890000-0000-7000-8000-000000000000","approved_by":"alice","at":"2026-01-01T00:00:00Z"}"#;
1426 let event: Event = serde_json::from_str(raw).expect("deserialize");
1427 let Event::ApprovalGranted(event) = event else {
1428 panic!("expected approval_granted");
1429 };
1430
1431 assert_eq!(event.step_id, None);
1432 assert_eq!(event.approvals_received, 1);
1433 assert_eq!(event.approvals_required, 1);
1434 assert!(event.requirement.is_none());
1435 }
1436
1437 #[test]
1438 fn legacy_approval_requested_and_rejected_have_no_requirement() {
1439 let raw = r#"{"type":"approval_requested","run_id":"01890000-0000-7000-8000-000000000000","step_id":"01890000-0000-7000-8000-000000000001","message":"ok?","at":"2026-01-01T00:00:00Z"}"#;
1440 let requested: Event = serde_json::from_str(raw).expect("deserialize");
1441 let Event::ApprovalRequested(requested) = requested else {
1442 panic!("expected approval_requested");
1443 };
1444 assert!(requested.requirement.is_none());
1445
1446 let raw = r#"{"type":"approval_rejected","run_id":"01890000-0000-7000-8000-000000000000","rejected_by":"bob","at":"2026-01-01T00:00:00Z"}"#;
1447 let rejected: Event = serde_json::from_str(raw).expect("deserialize");
1448 let Event::ApprovalRejected(rejected) = rejected else {
1449 panic!("expected approval_rejected");
1450 };
1451 assert_eq!(rejected.step_id, None);
1452 assert!(rejected.requirement.is_none());
1453 }
1454
1455 #[test]
1456 fn approval_granted_roundtrips_the_vote_counts() {
1457 let requirement = ApprovalRequirement {
1458 reason: Some("amount > 10k".to_string()),
1459 required_approvers: 2,
1460 approver_groups: vec!["finance".to_string()],
1461 };
1462 let event = Event::ApprovalGranted(ApprovalGrantedEvent {
1463 run_id: Uuid::now_v7(),
1464 step_id: Some(Uuid::now_v7()),
1465 approved_by: "alice".to_string(),
1466 user_id: None,
1467 approvals_received: 1,
1468 approvals_required: 2,
1469 requirement: Some(requirement.clone()),
1470 at: Utc::now(),
1471 });
1472
1473 let json = serde_json::to_string(&event).expect("serialize");
1474 let back: Event = serde_json::from_str(&json).expect("deserialize");
1475 let Event::ApprovalGranted(back) = back else {
1476 panic!("expected approval_granted");
1477 };
1478 assert_eq!(back.approvals_received, 1);
1479 assert_eq!(back.approvals_required, 2);
1480 assert_eq!(back.requirement, Some(requirement));
1481 }
1482
1483 #[test]
1484 fn approval_requested_serde_roundtrip() {
1485 let event = Event::ApprovalRequested(ApprovalRequestedEvent {
1486 run_id: Uuid::now_v7(),
1487 step_id: Uuid::now_v7(),
1488 message: "Deploy to prod?".to_string(),
1489 requirement: None,
1490 at: Utc::now(),
1491 });
1492
1493 let json = serde_json::to_string(&event).expect("serialize");
1494 assert!(json.contains("approval_requested"));
1495 }
1496
1497 #[test]
1498 fn log_line_serde_roundtrip() {
1499 let event = Event::LogLine(LogLineEvent {
1500 id: Uuid::now_v7(),
1501 run_id: Uuid::now_v7(),
1502 step_id: Uuid::now_v7(),
1503 step_name: "build".to_string(),
1504 stream: LogStream::Stdout,
1505 line: "Compiling ironflow v0.1.0".to_string(),
1506 at: Utc::now(),
1507 });
1508
1509 let json = serde_json::to_string(&event).expect("serialize");
1510 let back: Event = serde_json::from_str(&json).expect("deserialize");
1511
1512 assert_eq!(back.event_type(), "log_line");
1513 assert!(json.contains("\"type\":\"log_line\""));
1514 assert!(json.contains("Compiling ironflow"));
1515 }
1516
1517 #[test]
1522 fn legacy_flat_json_deserializes_into_typed_payload() {
1523 let run_id: Uuid = "01890000-0000-7000-8000-000000000000"
1524 .parse()
1525 .expect("valid uuid");
1526
1527 let raw = r#"{"type":"run_created","run_id":"01890000-0000-7000-8000-000000000000","workflow_name":"deploy","at":"2026-01-01T00:00:00Z"}"#;
1528 let event: Event = serde_json::from_str(raw).expect("legacy payload must deserialize");
1529 match event {
1530 Event::RunCreated(e) => {
1531 assert_eq!(e.run_id, run_id);
1532 assert_eq!(e.workflow_name, "deploy");
1533 }
1534 other => panic!("expected RunCreated, got {other:?}"),
1535 }
1536
1537 let raw = r#"{"type":"run_status_changed","run_id":"01890000-0000-7000-8000-000000000000","workflow_name":"deploy","from":"running","to":"completed","error":null,"cost_usd":0.5,"duration_ms":5000,"labels":{"env":"prod"},"at":"2026-01-01T00:00:00Z"}"#;
1538 let event: Event = serde_json::from_str(raw).expect("legacy payload must deserialize");
1539 match event {
1540 Event::RunStatusChanged(e) => {
1541 assert_eq!(e.from, RunStatus::Running);
1542 assert_eq!(e.to, RunStatus::Completed);
1543 assert_eq!(e.cost_usd, Decimal::new(5, 1));
1544 assert_eq!(e.duration_ms, 5000);
1545 assert_eq!(e.labels.get("env").map(String::as_str), Some("prod"));
1546 }
1547 other => panic!("expected RunStatusChanged, got {other:?}"),
1548 }
1549
1550 let raw = r#"{"type":"run_status_changed","run_id":"01890000-0000-7000-8000-000000000000","workflow_name":"deploy","from":"running","to":"failed","error":"boom","cost_usd":0,"duration_ms":0,"at":"2026-01-01T00:00:00Z"}"#;
1552 let event: Event = serde_json::from_str(raw).expect("missing labels must default");
1553 match event {
1554 Event::RunStatusChanged(e) => {
1555 assert!(e.labels.is_empty());
1556 assert_eq!(e.error.as_deref(), Some("boom"));
1557 }
1558 other => panic!("expected RunStatusChanged, got {other:?}"),
1559 }
1560
1561 let raw = r#"{"type":"run_failed","run_id":"01890000-0000-7000-8000-000000000000","workflow_name":"deploy","error":"boom","cost_usd":0.25,"duration_ms":3000,"at":"2026-01-01T00:00:00Z"}"#;
1562 let event: Event = serde_json::from_str(raw).expect("legacy payload must deserialize");
1563 match event {
1564 Event::RunFailed(e) => {
1565 assert_eq!(e.error.as_deref(), Some("boom"));
1566 assert!(e.labels.is_empty());
1567 }
1568 other => panic!("expected RunFailed, got {other:?}"),
1569 }
1570
1571 let raw = r#"{"type":"step_failed","run_id":"01890000-0000-7000-8000-000000000000","step_id":"01890000-0000-7000-8000-000000000001","step_name":"build","kind":"shell","error":"exit code 1","at":"2026-01-01T00:00:00Z"}"#;
1572 let event: Event = serde_json::from_str(raw).expect("legacy payload must deserialize");
1573 match event {
1574 Event::StepFailed(e) => {
1575 assert_eq!(e.kind, StepKind::Shell);
1576 assert_eq!(e.error, "exit code 1");
1577 }
1578 other => panic!("expected StepFailed, got {other:?}"),
1579 }
1580
1581 let raw = r#"{"type":"log_line","run_id":"01890000-0000-7000-8000-000000000000","step_id":"01890000-0000-7000-8000-000000000001","step_name":"build","stream":"stdout","line":"hello","at":"2026-01-01T00:00:00Z"}"#;
1582 let event: Event = serde_json::from_str(raw).expect("legacy payload must deserialize");
1583 match event {
1584 Event::LogLine(e) => {
1585 assert_eq!(e.stream, LogStream::Stdout);
1586 assert_eq!(e.line, "hello");
1587 assert_eq!(e.id, Uuid::nil());
1590 }
1591 other => panic!("expected LogLine, got {other:?}"),
1592 }
1593
1594 let raw = r#"{"type":"user_signed_in","user_id":"01890000-0000-7000-8000-000000000000","username":"alice","at":"2026-01-01T00:00:00Z"}"#;
1595 let event: Event = serde_json::from_str(raw).expect("legacy payload must deserialize");
1596 match event {
1597 Event::UserSignedIn(e) => assert_eq!(e.username, "alice"),
1598 other => panic!("expected UserSignedIn, got {other:?}"),
1599 }
1600 }
1601
1602 #[test]
1605 fn serialized_event_is_flat_with_type_tag() {
1606 let run_id = Uuid::now_v7();
1607 let event = Event::RunCreated(RunCreatedEvent {
1608 run_id,
1609 workflow_name: "deploy".to_string(),
1610 at: Utc::now(),
1611 });
1612
1613 let value: serde_json::Value = serde_json::to_value(&event).expect("serialize");
1614 let object = value.as_object().expect("event serializes to an object");
1615
1616 assert_eq!(
1617 object.get("type").and_then(|v| v.as_str()),
1618 Some("run_created")
1619 );
1620 assert_eq!(
1621 object.get("workflow_name").and_then(|v| v.as_str()),
1622 Some("deploy")
1623 );
1624 assert_eq!(
1625 object.get("run_id").and_then(|v| v.as_str()),
1626 Some(run_id.to_string().as_str())
1627 );
1628 assert!(object.contains_key("at"));
1629 assert_eq!(object.len(), 4, "no nesting: {object:?}");
1630 assert!(!object.contains_key("RunCreated"));
1631 }
1632
1633 #[test]
1634 fn run_id_returns_some_for_run_events() {
1635 let run_id = Uuid::now_v7();
1636 let now = Utc::now();
1637
1638 let events = vec![
1639 Event::RunCreated(RunCreatedEvent {
1640 run_id,
1641 workflow_name: "w".to_string(),
1642 at: now,
1643 }),
1644 Event::RunStatusChanged(RunStatusChangedEvent {
1645 run_id,
1646 workflow_name: "w".to_string(),
1647 from: RunStatus::Pending,
1648 to: RunStatus::Running,
1649 error: None,
1650 cost_usd: Decimal::ZERO,
1651 duration_ms: 0,
1652 labels: HashMap::new(),
1653 at: now,
1654 }),
1655 Event::RunFailed(RunFailedEvent {
1656 run_id,
1657 workflow_name: "w".to_string(),
1658 error: None,
1659 cost_usd: Decimal::ZERO,
1660 duration_ms: 0,
1661 labels: HashMap::new(),
1662 at: now,
1663 }),
1664 Event::RunBudgetExceeded(RunBudgetExceededEvent {
1665 run_id,
1666 workflow_name: "w".to_string(),
1667 limit_usd: Decimal::ZERO,
1668 spent_usd: Decimal::ZERO,
1669 step_budget_usd: Decimal::ZERO,
1670 at: now,
1671 }),
1672 Event::RetryForced(RetryForcedEvent {
1673 run_id,
1674 workflow_name: "w".to_string(),
1675 original_version: "1".to_string(),
1676 current_version: "2".to_string(),
1677 at: now,
1678 }),
1679 Event::StepCompleted(StepCompletedEvent {
1680 run_id,
1681 step_id: Uuid::now_v7(),
1682 step_name: "s".to_string(),
1683 kind: StepKind::Shell,
1684 duration_ms: 0,
1685 cost_usd: Decimal::ZERO,
1686 at: now,
1687 }),
1688 Event::StepFailed(StepFailedEvent {
1689 run_id,
1690 step_id: Uuid::now_v7(),
1691 step_name: "s".to_string(),
1692 kind: StepKind::Shell,
1693 error: "e".to_string(),
1694 at: now,
1695 }),
1696 Event::ApprovalRequested(ApprovalRequestedEvent {
1697 run_id,
1698 step_id: Uuid::now_v7(),
1699 message: "ok?".to_string(),
1700 requirement: None,
1701 at: now,
1702 }),
1703 Event::ApprovalGranted(ApprovalGrantedEvent {
1704 run_id,
1705 step_id: None,
1706 approved_by: "alice".to_string(),
1707 user_id: None,
1708 approvals_received: 1,
1709 approvals_required: 1,
1710 requirement: None,
1711 at: now,
1712 }),
1713 Event::ApprovalRejected(ApprovalRejectedEvent {
1714 run_id,
1715 step_id: None,
1716 rejected_by: "bob".to_string(),
1717 user_id: None,
1718 reason: None,
1719 requirement: None,
1720 at: now,
1721 }),
1722 Event::LogLine(LogLineEvent {
1723 id: Uuid::now_v7(),
1724 run_id,
1725 step_id: Uuid::now_v7(),
1726 step_name: "s".to_string(),
1727 stream: LogStream::Stdout,
1728 line: "l".to_string(),
1729 at: now,
1730 }),
1731 ];
1732
1733 for event in &events {
1734 assert_eq!(
1735 event.run_id(),
1736 Some(run_id),
1737 "{} should carry a run_id",
1738 event.event_type()
1739 );
1740 }
1741 }
1742
1743 #[test]
1744 fn run_id_returns_none_for_auth_events() {
1745 let user_id = Uuid::now_v7();
1746 let now = Utc::now();
1747
1748 let events = vec![
1749 Event::UserSignedIn(UserSignedInEvent {
1750 user_id,
1751 username: "alice".to_string(),
1752 at: now,
1753 }),
1754 Event::UserSignedUp(UserSignedUpEvent {
1755 user_id,
1756 username: "alice".to_string(),
1757 at: now,
1758 }),
1759 Event::UserSignedOut(UserSignedOutEvent { user_id, at: now }),
1760 ];
1761
1762 for event in &events {
1763 assert_eq!(event.run_id(), None, "{} has no run", event.event_type());
1764 }
1765 }
1766
1767 #[test]
1768 fn step_id_returns_some_only_for_step_events() {
1769 let step_id = Uuid::now_v7();
1770 let run_id = Uuid::now_v7();
1771 let now = Utc::now();
1772
1773 let with_step = vec![
1774 Event::StepCompleted(StepCompletedEvent {
1775 run_id,
1776 step_id,
1777 step_name: "s".to_string(),
1778 kind: StepKind::Shell,
1779 duration_ms: 0,
1780 cost_usd: Decimal::ZERO,
1781 at: now,
1782 }),
1783 Event::StepFailed(StepFailedEvent {
1784 run_id,
1785 step_id,
1786 step_name: "s".to_string(),
1787 kind: StepKind::Shell,
1788 error: "e".to_string(),
1789 at: now,
1790 }),
1791 Event::ApprovalRequested(ApprovalRequestedEvent {
1792 run_id,
1793 step_id,
1794 message: "ok?".to_string(),
1795 requirement: None,
1796 at: now,
1797 }),
1798 Event::ApprovalGranted(ApprovalGrantedEvent {
1799 run_id,
1800 step_id: Some(step_id),
1801 approved_by: "alice".to_string(),
1802 user_id: None,
1803 approvals_received: 1,
1804 approvals_required: 2,
1805 requirement: None,
1806 at: now,
1807 }),
1808 Event::ApprovalRejected(ApprovalRejectedEvent {
1809 run_id,
1810 step_id: Some(step_id),
1811 rejected_by: "bob".to_string(),
1812 user_id: None,
1813 reason: None,
1814 requirement: None,
1815 at: now,
1816 }),
1817 ];
1818
1819 for event in &with_step {
1820 assert_eq!(
1821 event.step_id(),
1822 Some(step_id),
1823 "{} should carry a step_id",
1824 event.event_type()
1825 );
1826 }
1827
1828 let without_step = vec![
1829 Event::RunCreated(RunCreatedEvent {
1830 run_id,
1831 workflow_name: "w".to_string(),
1832 at: now,
1833 }),
1834 Event::ApprovalGranted(ApprovalGrantedEvent {
1836 run_id,
1837 step_id: None,
1838 approved_by: "alice".to_string(),
1839 user_id: None,
1840 approvals_received: 1,
1841 approvals_required: 1,
1842 requirement: None,
1843 at: now,
1844 }),
1845 Event::LogLine(LogLineEvent {
1848 id: Uuid::now_v7(),
1849 run_id,
1850 step_id,
1851 step_name: "s".to_string(),
1852 stream: LogStream::Stdout,
1853 line: "l".to_string(),
1854 at: now,
1855 }),
1856 Event::UserSignedOut(UserSignedOutEvent {
1857 user_id: Uuid::now_v7(),
1858 at: now,
1859 }),
1860 ];
1861
1862 for event in &without_step {
1863 assert_eq!(
1864 event.step_id(),
1865 None,
1866 "{} should not carry a step_id",
1867 event.event_type()
1868 );
1869 }
1870 }
1871
1872 #[test]
1873 fn user_id_returns_the_actor_of_auth_events_decisions_and_pauses() {
1874 let user_id = Uuid::now_v7();
1875 let run_id = Uuid::now_v7();
1876 let now = Utc::now();
1877
1878 let auth = vec![
1879 Event::UserSignedIn(UserSignedInEvent {
1880 user_id,
1881 username: "alice".to_string(),
1882 at: now,
1883 }),
1884 Event::UserSignedUp(UserSignedUpEvent {
1885 user_id,
1886 username: "alice".to_string(),
1887 at: now,
1888 }),
1889 Event::UserSignedOut(UserSignedOutEvent { user_id, at: now }),
1890 Event::ApprovalGranted(ApprovalGrantedEvent {
1891 run_id,
1892 step_id: None,
1893 approved_by: "alice".to_string(),
1894 user_id: Some(user_id),
1895 approvals_received: 1,
1896 approvals_required: 1,
1897 requirement: None,
1898 at: now,
1899 }),
1900 Event::ApprovalRejected(ApprovalRejectedEvent {
1901 run_id,
1902 step_id: None,
1903 rejected_by: "bob".to_string(),
1904 user_id: Some(user_id),
1905 reason: None,
1906 requirement: None,
1907 at: now,
1908 }),
1909 Event::WorkflowPaused(WorkflowPausedEvent {
1910 workflow_name: "w".to_string(),
1911 user_id: Some(user_id),
1912 at: now,
1913 }),
1914 Event::WorkflowResumed(WorkflowResumedEvent {
1915 workflow_name: "w".to_string(),
1916 user_id: Some(user_id),
1917 at: now,
1918 }),
1919 ];
1920
1921 for event in &auth {
1922 assert_eq!(
1923 event.user_id(),
1924 Some(user_id),
1925 "{} should carry a user_id",
1926 event.event_type()
1927 );
1928 }
1929
1930 let non_auth = vec![
1931 Event::RunCreated(RunCreatedEvent {
1932 run_id,
1933 workflow_name: "w".to_string(),
1934 at: now,
1935 }),
1936 Event::StepFailed(StepFailedEvent {
1937 run_id,
1938 step_id: Uuid::now_v7(),
1939 step_name: "s".to_string(),
1940 kind: StepKind::Shell,
1941 error: "e".to_string(),
1942 at: now,
1943 }),
1944 Event::ApprovalRejected(ApprovalRejectedEvent {
1946 run_id,
1947 step_id: None,
1948 rejected_by: "system:timeout".to_string(),
1949 user_id: None,
1950 reason: None,
1951 requirement: None,
1952 at: now,
1953 }),
1954 Event::WorkflowPaused(WorkflowPausedEvent {
1955 workflow_name: "w".to_string(),
1956 user_id: None,
1957 at: now,
1958 }),
1959 ];
1960
1961 for event in &non_auth {
1962 assert_eq!(
1963 event.user_id(),
1964 None,
1965 "{} should not carry a user_id",
1966 event.event_type()
1967 );
1968 }
1969 }
1970
1971 #[test]
1972 fn event_type_all_variants() {
1973 let id = Uuid::now_v7();
1974 let now = Utc::now();
1975
1976 let cases: Vec<(Event, &str)> = vec![
1977 (
1978 Event::RunCreated(RunCreatedEvent {
1979 run_id: id,
1980 workflow_name: "w".to_string(),
1981 at: now,
1982 }),
1983 "run_created",
1984 ),
1985 (
1986 Event::RunStatusChanged(RunStatusChangedEvent {
1987 run_id: id,
1988 workflow_name: "w".to_string(),
1989 from: RunStatus::Pending,
1990 to: RunStatus::Running,
1991 error: None,
1992 cost_usd: Decimal::ZERO,
1993 duration_ms: 0,
1994 labels: HashMap::new(),
1995 at: now,
1996 }),
1997 "run_status_changed",
1998 ),
1999 (
2000 Event::RunFailed(RunFailedEvent {
2001 run_id: id,
2002 workflow_name: "w".to_string(),
2003 error: Some("boom".to_string()),
2004 cost_usd: Decimal::ZERO,
2005 duration_ms: 0,
2006 labels: HashMap::new(),
2007 at: now,
2008 }),
2009 "run_failed",
2010 ),
2011 (
2012 Event::RunBudgetExceeded(RunBudgetExceededEvent {
2013 run_id: id,
2014 workflow_name: "w".to_string(),
2015 limit_usd: Decimal::new(200, 2),
2016 spent_usd: Decimal::new(180, 2),
2017 step_budget_usd: Decimal::new(50, 2),
2018 at: now,
2019 }),
2020 "run_budget_exceeded",
2021 ),
2022 (
2023 Event::RetryForced(RetryForcedEvent {
2024 run_id: id,
2025 workflow_name: "w".to_string(),
2026 original_version: "1".to_string(),
2027 current_version: "2".to_string(),
2028 at: now,
2029 }),
2030 "retry_forced",
2031 ),
2032 (
2033 Event::StepCompleted(StepCompletedEvent {
2034 run_id: id,
2035 step_id: id,
2036 step_name: "s".to_string(),
2037 kind: StepKind::Shell,
2038 duration_ms: 0,
2039 cost_usd: Decimal::ZERO,
2040 at: now,
2041 }),
2042 "step_completed",
2043 ),
2044 (
2045 Event::StepFailed(StepFailedEvent {
2046 run_id: id,
2047 step_id: id,
2048 step_name: "s".to_string(),
2049 kind: StepKind::Shell,
2050 error: "err".to_string(),
2051 at: now,
2052 }),
2053 "step_failed",
2054 ),
2055 (
2056 Event::ApprovalRequested(ApprovalRequestedEvent {
2057 run_id: id,
2058 step_id: id,
2059 message: "ok?".to_string(),
2060 requirement: None,
2061 at: now,
2062 }),
2063 "approval_requested",
2064 ),
2065 (
2066 Event::ApprovalGranted(ApprovalGrantedEvent {
2067 run_id: id,
2068 step_id: Some(id),
2069 approved_by: "alice".to_string(),
2070 user_id: None,
2071 approvals_received: 1,
2072 approvals_required: 1,
2073 requirement: None,
2074 at: now,
2075 }),
2076 "approval_granted",
2077 ),
2078 (
2079 Event::ApprovalRejected(ApprovalRejectedEvent {
2080 run_id: id,
2081 step_id: Some(id),
2082 rejected_by: "bob".to_string(),
2083 user_id: None,
2084 reason: None,
2085 requirement: None,
2086 at: now,
2087 }),
2088 "approval_rejected",
2089 ),
2090 (
2091 Event::ApprovalEscalated(ApprovalEscalatedEvent {
2092 run_id: id,
2093 step_id: id,
2094 step_name: "prod-gate".to_string(),
2095 stage: 0,
2096 policy: "auto_reject".to_string(),
2097 action: "rejected".to_string(),
2098 reason: "approval deadline of 3600s expired".to_string(),
2099 assignee: None,
2100 at: now,
2101 }),
2102 "approval_escalated",
2103 ),
2104 (
2105 Event::LogLine(LogLineEvent {
2106 id,
2107 run_id: id,
2108 step_id: id,
2109 step_name: "build".to_string(),
2110 stream: LogStream::Stdout,
2111 line: "Compiling ironflow v0.1.0".to_string(),
2112 at: now,
2113 }),
2114 "log_line",
2115 ),
2116 (
2117 Event::UserSignedIn(UserSignedInEvent {
2118 user_id: id,
2119 username: "u".to_string(),
2120 at: now,
2121 }),
2122 "user_signed_in",
2123 ),
2124 (
2125 Event::UserSignedUp(UserSignedUpEvent {
2126 user_id: id,
2127 username: "u".to_string(),
2128 at: now,
2129 }),
2130 "user_signed_up",
2131 ),
2132 (
2133 Event::UserSignedOut(UserSignedOutEvent {
2134 user_id: id,
2135 at: now,
2136 }),
2137 "user_signed_out",
2138 ),
2139 (
2140 Event::ProviderAccountUpdated(ProviderAccountUpdatedEvent {
2141 account_id: id,
2142 name: "perso".to_string(),
2143 change: ProviderAccountChange::TokenReplaced,
2144 at: now,
2145 }),
2146 "provider_account.updated",
2147 ),
2148 (
2149 Event::ProviderAccountUsageUpdated(ProviderAccountUsageUpdatedEvent {
2150 account_id: id,
2151 name: "perso".to_string(),
2152 windows: Vec::new(),
2153 at: now,
2154 }),
2155 "provider_account.usage_updated",
2156 ),
2157 (
2158 Event::SignalAwaited(SignalAwaitedEvent {
2159 run_id: id,
2160 step_id: id,
2161 step_name: "wait-ci".to_string(),
2162 name: "ci.done".to_string(),
2163 key: "abc".to_string(),
2164 deadline_at: now,
2165 at: now,
2166 }),
2167 "signal_awaited",
2168 ),
2169 (
2170 Event::SignalReceived(SignalReceivedEvent {
2171 signal_id: id,
2172 name: "ci.done".to_string(),
2173 key: "abc".to_string(),
2174 resumed_runs: vec![id],
2175 at: now,
2176 }),
2177 "signal_received",
2178 ),
2179 (
2180 Event::ScheduleOccurrencesMissed(ScheduleOccurrencesMissedEvent {
2181 schedule_id: id,
2182 workflow_name: "nightly".to_string(),
2183 reason: ScheduleMissReason::Overlap,
2184 count: 1,
2185 first: now,
2186 last: now,
2187 at: now,
2188 }),
2189 "schedule_occurrences_missed",
2190 ),
2191 (
2192 Event::WorkflowPaused(WorkflowPausedEvent {
2193 workflow_name: "w".to_string(),
2194 user_id: Some(id),
2195 at: now,
2196 }),
2197 "workflow_paused",
2198 ),
2199 (
2200 Event::WorkflowResumed(WorkflowResumedEvent {
2201 workflow_name: "w".to_string(),
2202 user_id: Some(id),
2203 at: now,
2204 }),
2205 "workflow_resumed",
2206 ),
2207 ];
2208
2209 assert_eq!(
2210 cases.len(),
2211 Event::ALL.len(),
2212 "every variant must be covered"
2213 );
2214
2215 for (event, expected_type) in cases {
2216 assert_eq!(event.event_type(), expected_type);
2217 }
2218 }
2219
2220 #[test]
2221 fn approval_escalated_serde_roundtrip() {
2222 let run_id = Uuid::now_v7();
2223 let step_id = Uuid::now_v7();
2224 let event = Event::ApprovalEscalated(ApprovalEscalatedEvent {
2225 run_id,
2226 step_id,
2227 step_name: "prod-gate".to_string(),
2228 stage: 1,
2229 policy: "escalate".to_string(),
2230 action: "reassigned to sre-oncall".to_string(),
2231 reason: "approval deadline of 3600s expired".to_string(),
2232 assignee: Some(Assignee::group("sre-oncall")),
2233 at: Utc::now(),
2234 });
2235
2236 let json = serde_json::to_string(&event).expect("serialize");
2237 assert!(
2238 json.contains("\"type\":\"approval_escalated\""),
2239 "got {json}"
2240 );
2241
2242 let back: Event = serde_json::from_str(&json).expect("deserialize");
2243 let Event::ApprovalEscalated(payload) = back else {
2244 panic!("expected an approval_escalated event");
2245 };
2246 assert_eq!(payload.run_id, run_id);
2247 assert_eq!(payload.stage, 1);
2248 assert_eq!(payload.assignee, Some(Assignee::group("sre-oncall")));
2249 }
2250
2251 #[test]
2252 fn approval_escalated_carries_run_and_step_ids() {
2253 let run_id = Uuid::now_v7();
2254 let step_id = Uuid::now_v7();
2255 let event = Event::ApprovalEscalated(ApprovalEscalatedEvent {
2256 run_id,
2257 step_id,
2258 step_name: "prod-gate".to_string(),
2259 stage: 0,
2260 policy: "notify".to_string(),
2261 action: "notified 1 target".to_string(),
2262 reason: "approval deadline of 60s expired".to_string(),
2263 assignee: None,
2264 at: Utc::now(),
2265 });
2266
2267 assert_eq!(event.run_id(), Some(run_id));
2268 assert_eq!(event.step_id(), Some(step_id));
2269 assert_eq!(event.user_id(), None);
2270 }
2271
2272 #[test]
2273 fn signal_awaited_serde_roundtrip() {
2274 let run_id = Uuid::now_v7();
2275 let step_id = Uuid::now_v7();
2276 let event = Event::SignalAwaited(SignalAwaitedEvent {
2277 run_id,
2278 step_id,
2279 step_name: "wait-ci".to_string(),
2280 name: "ci.done".to_string(),
2281 key: "abc".to_string(),
2282 deadline_at: Utc::now(),
2283 at: Utc::now(),
2284 });
2285
2286 let json = serde_json::to_string(&event).expect("serialize");
2287 assert!(json.contains("\"type\":\"signal_awaited\""), "got {json}");
2288 let back: Event = serde_json::from_str(&json).expect("deserialize");
2289 assert_eq!(back.event_type(), Event::SIGNAL_AWAITED);
2290 assert_eq!(back.run_id(), Some(run_id));
2291 assert_eq!(back.step_id(), Some(step_id));
2292 assert_eq!(back.user_id(), None);
2293 }
2294
2295 #[test]
2296 fn signal_received_serde_roundtrip() {
2297 let resumed = Uuid::now_v7();
2298 let event = Event::SignalReceived(SignalReceivedEvent {
2299 signal_id: Uuid::now_v7(),
2300 name: "ci.done".to_string(),
2301 key: "abc".to_string(),
2302 resumed_runs: vec![resumed],
2303 at: Utc::now(),
2304 });
2305
2306 let json = serde_json::to_string(&event).expect("serialize");
2307 assert!(json.contains("\"type\":\"signal_received\""), "got {json}");
2308 let back: Event = serde_json::from_str(&json).expect("deserialize");
2309 let Event::SignalReceived(payload) = &back else {
2310 panic!("expected a signal_received event, got {back:?}");
2311 };
2312 assert_eq!(payload.resumed_runs, vec![resumed]);
2313 assert_eq!(back.run_id(), None);
2314 assert_eq!(back.step_id(), None);
2315 assert_eq!(back.user_id(), None);
2316 }
2317
2318 #[test]
2319 fn schedule_occurrences_missed_serde_roundtrip() {
2320 let schedule_id = Uuid::now_v7();
2321 let first = Utc::now();
2322 let event = Event::ScheduleOccurrencesMissed(ScheduleOccurrencesMissedEvent {
2323 schedule_id,
2324 workflow_name: "nightly".to_string(),
2325 reason: ScheduleMissReason::OutsideWindow,
2326 count: 4,
2327 first,
2328 last: first,
2329 at: first,
2330 });
2331
2332 let json = serde_json::to_string(&event).expect("serialize");
2333 assert!(
2334 json.contains("\"type\":\"schedule_occurrences_missed\""),
2335 "got {json}"
2336 );
2337 assert!(json.contains("\"reason\":\"outside_window\""), "got {json}");
2338 let back: Event = serde_json::from_str(&json).expect("deserialize");
2339 let Event::ScheduleOccurrencesMissed(payload) = &back else {
2340 panic!("expected a schedule_occurrences_missed event, got {back:?}");
2341 };
2342 assert_eq!(payload.schedule_id, schedule_id);
2343 assert_eq!(payload.reason, ScheduleMissReason::OutsideWindow);
2344 assert_eq!(payload.count, 4);
2345 assert_eq!(payload.first, first);
2346 assert!(Event::ALL.contains(&Event::SCHEDULE_OCCURRENCES_MISSED));
2347 assert_eq!(back.run_id(), None);
2348 assert_eq!(back.step_id(), None);
2349 assert_eq!(back.user_id(), None);
2350 }
2351
2352 #[test]
2353 fn legacy_approval_decisions_have_no_user_id_nor_reason() {
2354 let raw = r#"{"type":"approval_granted","run_id":"01890000-0000-7000-8000-000000000000","approved_by":"alice","at":"2026-01-01T00:00:00Z"}"#;
2355 let granted: Event = serde_json::from_str(raw).expect("deserialize");
2356 assert_eq!(granted.user_id(), None);
2357
2358 let raw = r#"{"type":"approval_rejected","run_id":"01890000-0000-7000-8000-000000000000","rejected_by":"bob","at":"2026-01-01T00:00:00Z"}"#;
2359 let rejected: Event = serde_json::from_str(raw).expect("deserialize");
2360 let Event::ApprovalRejected(rejected) = rejected else {
2361 panic!("expected approval_rejected");
2362 };
2363 assert_eq!(rejected.user_id, None);
2364 assert_eq!(rejected.reason, None);
2365 }
2366
2367 #[test]
2368 fn approval_rejected_roundtrips_the_actor_and_reason() {
2369 let user_id = Uuid::now_v7();
2370 let event = Event::ApprovalRejected(ApprovalRejectedEvent {
2371 run_id: Uuid::now_v7(),
2372 step_id: Some(Uuid::now_v7()),
2373 rejected_by: "bob".to_string(),
2374 user_id: Some(user_id),
2375 reason: Some("wrong environment".to_string()),
2376 requirement: None,
2377 at: Utc::now(),
2378 });
2379
2380 let json = serde_json::to_string(&event).expect("serialize");
2381 let back: Event = serde_json::from_str(&json).expect("deserialize");
2382 assert_eq!(back.user_id(), Some(user_id));
2383 let Event::ApprovalRejected(back) = back else {
2384 panic!("expected approval_rejected");
2385 };
2386 assert_eq!(back.reason.as_deref(), Some("wrong environment"));
2387 }
2388
2389 #[test]
2390 fn workflow_paused_and_resumed_serde_roundtrip() {
2391 let user_id = Uuid::now_v7();
2392 let now = Utc::now();
2393 let events = vec![
2394 Event::WorkflowPaused(WorkflowPausedEvent {
2395 workflow_name: "deploy".to_string(),
2396 user_id: Some(user_id),
2397 at: now,
2398 }),
2399 Event::WorkflowResumed(WorkflowResumedEvent {
2400 workflow_name: "deploy".to_string(),
2401 user_id: Some(user_id),
2402 at: now,
2403 }),
2404 ];
2405
2406 for event in events {
2407 let json = serde_json::to_string(&event).expect("serialize");
2408 assert!(
2409 json.contains(&format!("\"type\":\"{}\"", event.event_type())),
2410 "got {json}"
2411 );
2412 assert!(json.contains("\"workflow_name\":\"deploy\""), "got {json}");
2413 let back: Event = serde_json::from_str(&json).expect("deserialize");
2414 assert_eq!(back.event_type(), event.event_type());
2415 assert_eq!(back.user_id(), Some(user_id));
2416 assert_eq!(back.run_id(), None);
2417 assert_eq!(back.step_id(), None);
2418 assert!(Event::ALL.contains(&back.event_type()));
2419 }
2420 }
2421
2422 #[test]
2423 fn workflow_paused_without_user_id_deserializes() {
2424 let raw =
2425 r#"{"type":"workflow_paused","workflow_name":"deploy","at":"2026-01-01T00:00:00Z"}"#;
2426 let event: Event = serde_json::from_str(raw).expect("deserialize");
2427 assert_eq!(event.event_type(), Event::WORKFLOW_PAUSED);
2428 assert_eq!(event.user_id(), None);
2429 }
2430}