Skip to main content

made_core/events/
task_failed.rs

1//! [`TaskFailedEvent`] — a task failed during deliberation or execution.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::DomainError;
6use crate::events::envelope::EventEnvelope;
7use crate::value_objects::{Specialty, TaskId};
8
9const MAX_REASON_LEN: usize = 4096;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct TaskFailedEvent {
13    #[serde(flatten)]
14    envelope: EventEnvelope,
15    task_id: TaskId,
16    specialty: Specialty,
17    error_kind: String,
18    error_reason: String,
19}
20
21impl TaskFailedEvent {
22    pub fn new(
23        envelope: EventEnvelope,
24        task_id: TaskId,
25        specialty: Specialty,
26        error_kind: impl Into<String>,
27        error_reason: impl Into<String>,
28    ) -> Result<Self, DomainError> {
29        let kind = error_kind.into();
30        if kind.trim().is_empty() {
31            return Err(DomainError::EmptyField {
32                field: "task_failed.error_kind",
33            });
34        }
35        let reason = error_reason.into();
36        if reason.len() > MAX_REASON_LEN {
37            return Err(DomainError::FieldTooLong {
38                field: "task_failed.error_reason",
39                actual: reason.len(),
40                max: MAX_REASON_LEN,
41            });
42        }
43        Ok(Self {
44            envelope,
45            task_id,
46            specialty,
47            error_kind: kind,
48            error_reason: reason,
49        })
50    }
51
52    #[must_use]
53    pub fn envelope(&self) -> &EventEnvelope {
54        &self.envelope
55    }
56    #[must_use]
57    pub fn task_id(&self) -> &TaskId {
58        &self.task_id
59    }
60    #[must_use]
61    pub fn specialty(&self) -> &Specialty {
62        &self.specialty
63    }
64    #[must_use]
65    pub fn error_kind(&self) -> &str {
66        &self.error_kind
67    }
68    #[must_use]
69    pub fn error_reason(&self) -> &str {
70        &self.error_reason
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::value_objects::EventId;
78    use time::macros::datetime;
79
80    fn env() -> EventEnvelope {
81        EventEnvelope::new(
82            EventId::new("e").unwrap(),
83            datetime!(2026-04-15 12:00:00 UTC),
84            "s",
85            None,
86        )
87        .unwrap()
88    }
89
90    #[test]
91    fn empty_kind_is_rejected() {
92        let err = TaskFailedEvent::new(
93            env(),
94            TaskId::new("t").unwrap(),
95            Specialty::new("s").unwrap(),
96            "   ",
97            "reason",
98        )
99        .unwrap_err();
100        assert!(matches!(
101            err,
102            DomainError::EmptyField {
103                field: "task_failed.error_kind"
104            }
105        ));
106    }
107
108    #[test]
109    fn overlong_reason_is_rejected() {
110        let err = TaskFailedEvent::new(
111            env(),
112            TaskId::new("t").unwrap(),
113            Specialty::new("s").unwrap(),
114            "kind",
115            "x".repeat(MAX_REASON_LEN + 1),
116        )
117        .unwrap_err();
118        assert!(matches!(err, DomainError::FieldTooLong { .. }));
119    }
120
121    #[test]
122    fn well_formed_event_keeps_fields() {
123        let ev = TaskFailedEvent::new(
124            env(),
125            TaskId::new("t").unwrap(),
126            Specialty::new("s").unwrap(),
127            "validator.timeout",
128            "deadline exceeded",
129        )
130        .unwrap();
131        assert_eq!(ev.error_kind(), "validator.timeout");
132        assert_eq!(ev.error_reason(), "deadline exceeded");
133    }
134}