cellstate_core/
delegation.rs

1//! Delegation typestate for compile-time safety of delegation lifecycle.
2//!
3//! Uses the typestate pattern to make invalid state transitions uncompilable.
4//!
5//! # State Transition Diagram
6//!
7//! ```text
8//! create() → Pending ──┬── accept() ──→ Accepted ── start() → InProgress ──┬── complete() → Completed
9//!                      ├── reject() ──→ Rejected (terminal)                └── fail() → Failed (terminal)
10//!                      └── timeout() ─→ Failed (terminal)
11//! ```
12
13use crate::{
14    AgentId, ArtifactId, DelegationId, EnumParseError, NoteId, ScopeId, TenantId, Timestamp,
15    TrajectoryId,
16};
17use serde::{Deserialize, Serialize};
18use std::fmt;
19use std::marker::PhantomData;
20use std::str::FromStr;
21
22// ============================================================================
23// DELEGATION STATUS ENUM (replaces String)
24// ============================================================================
25
26/// Status of a delegation operation.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
30pub enum DelegationStatus {
31    /// Delegation is pending acceptance
32    Pending,
33    /// Delegation was accepted but not yet started
34    Accepted,
35    /// Delegation work is in progress
36    InProgress,
37    /// Delegation was completed successfully
38    Completed,
39    /// Delegation was rejected
40    Rejected,
41    /// Delegation failed (timeout, error, etc.)
42    Failed,
43}
44
45impl DelegationStatus {
46    /// Convert to database string representation.
47    pub fn as_db_str(&self) -> &'static str {
48        match self {
49            DelegationStatus::Pending => "pending",
50            DelegationStatus::Accepted => "accepted",
51            DelegationStatus::InProgress => "in_progress",
52            DelegationStatus::Completed => "completed",
53            DelegationStatus::Rejected => "rejected",
54            DelegationStatus::Failed => "failed",
55        }
56    }
57
58    /// Parse from database string representation.
59    pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
60        match s.to_lowercase().as_str() {
61            "pending" => Ok(DelegationStatus::Pending),
62            "accepted" => Ok(DelegationStatus::Accepted),
63            "inprogress" | "in_progress" | "in-progress" => Ok(DelegationStatus::InProgress),
64            "completed" | "complete" => Ok(DelegationStatus::Completed),
65            "rejected" => Ok(DelegationStatus::Rejected),
66            "failed" | "failure" => Ok(DelegationStatus::Failed),
67            _ => Err(EnumParseError {
68                enum_name: "DelegationStatus",
69                input: s.to_string(),
70            }),
71        }
72    }
73
74    /// Check if this is a terminal state (no further transitions possible).
75    pub fn is_terminal(&self) -> bool {
76        matches!(
77            self,
78            DelegationStatus::Completed | DelegationStatus::Rejected | DelegationStatus::Failed
79        )
80    }
81}
82
83impl fmt::Display for DelegationStatus {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        write!(f, "{}", self.as_db_str())
86    }
87}
88
89impl FromStr for DelegationStatus {
90    type Err = EnumParseError;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        Self::from_db_str(s)
94    }
95}
96
97// ============================================================================
98// DELEGATION RESULT STATUS (replaces String in result)
99// ============================================================================
100
101/// Status of a delegation result.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
105pub enum DelegationResultStatus {
106    /// Task completed successfully
107    Success,
108    /// Task partially completed
109    Partial,
110    /// Task failed
111    Failure,
112}
113
114impl DelegationResultStatus {
115    /// Convert to database string representation.
116    pub fn as_db_str(&self) -> &'static str {
117        match self {
118            DelegationResultStatus::Success => "success",
119            DelegationResultStatus::Partial => "partial",
120            DelegationResultStatus::Failure => "failure",
121        }
122    }
123
124    /// Parse from database string representation.
125    pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
126        match s.to_lowercase().as_str() {
127            "success" => Ok(DelegationResultStatus::Success),
128            "partial" => Ok(DelegationResultStatus::Partial),
129            "failure" | "failed" => Ok(DelegationResultStatus::Failure),
130            _ => Err(EnumParseError {
131                enum_name: "DelegationResultStatus",
132                input: s.to_string(),
133            }),
134        }
135    }
136}
137
138impl fmt::Display for DelegationResultStatus {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(f, "{}", self.as_db_str())
141    }
142}
143
144impl FromStr for DelegationResultStatus {
145    type Err = EnumParseError;
146
147    fn from_str(s: &str) -> Result<Self, Self::Err> {
148        Self::from_db_str(s)
149    }
150}
151
152// ============================================================================
153// DELEGATION RESULT
154// ============================================================================
155
156/// Result of a completed delegation.
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
159pub struct DelegationResult {
160    /// Status of the result
161    pub status: DelegationResultStatus,
162    /// Artifacts produced by the task
163    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
164    pub produced_artifacts: Vec<ArtifactId>,
165    /// Notes produced by the task
166    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
167    pub produced_notes: Vec<NoteId>,
168    /// Summary of what was accomplished
169    pub summary: String,
170    /// Error message (if failed)
171    pub error: Option<String>,
172}
173
174impl DelegationResult {
175    /// Create a successful result.
176    pub fn success(summary: &str, artifacts: Vec<ArtifactId>) -> Self {
177        Self {
178            status: DelegationResultStatus::Success,
179            produced_artifacts: artifacts,
180            produced_notes: Vec::new(),
181            summary: summary.to_string(),
182            error: None,
183        }
184    }
185
186    /// Create a partial result.
187    pub fn partial(summary: &str, artifacts: Vec<ArtifactId>) -> Self {
188        Self {
189            status: DelegationResultStatus::Partial,
190            produced_artifacts: artifacts,
191            produced_notes: Vec::new(),
192            summary: summary.to_string(),
193            error: None,
194        }
195    }
196
197    /// Create a failure result.
198    pub fn failure(error: &str) -> Self {
199        Self {
200            status: DelegationResultStatus::Failure,
201            produced_artifacts: Vec::new(),
202            produced_notes: Vec::new(),
203            summary: String::new(),
204            error: Some(error.to_string()),
205        }
206    }
207}
208
209// ============================================================================
210// DELEGATION DATA (internal storage, state-independent)
211// ============================================================================
212
213/// Internal data storage for a delegation, independent of typestate.
214/// This is what gets persisted to the database.
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
217pub struct DelegationRecord {
218    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
219    pub delegation_id: DelegationId,
220    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
221    pub tenant_id: TenantId,
222    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
223    pub from_agent_id: AgentId,
224    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
225    pub to_agent_id: AgentId,
226    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
227    pub trajectory_id: TrajectoryId,
228    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
229    pub scope_id: ScopeId,
230    pub task_description: String,
231    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
232    pub created_at: Timestamp,
233    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
234    pub accepted_at: Option<Timestamp>,
235    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
236    pub started_at: Option<Timestamp>,
237    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
238    pub completed_at: Option<Timestamp>,
239    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
240    pub expected_completion: Option<Timestamp>,
241    pub result: Option<DelegationResult>,
242    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
243    pub context: Option<serde_json::Value>,
244    /// Rejection reason (only set if rejected)
245    pub rejection_reason: Option<String>,
246    /// Failure reason (only set if failed)
247    pub failure_reason: Option<String>,
248}
249
250// ============================================================================
251// TYPESTATE MARKERS
252// ============================================================================
253
254/// Marker trait for delegation states.
255pub trait DelegationState: private::Sealed + Send + Sync {}
256
257/// Delegation is pending acceptance.
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub struct Pending;
260impl DelegationState for Pending {}
261
262/// Delegation was accepted but not yet started.
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub struct DelegationAccepted;
265impl DelegationState for DelegationAccepted {}
266
267/// Delegation work is in progress.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub struct InProgress;
270impl DelegationState for InProgress {}
271
272/// Delegation was completed successfully (terminal).
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct DelegationCompleted;
275impl DelegationState for DelegationCompleted {}
276
277/// Delegation was rejected (terminal).
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub struct DelegationRejected;
280impl DelegationState for DelegationRejected {}
281
282/// Delegation failed (terminal).
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub struct DelegationFailed;
285impl DelegationState for DelegationFailed {}
286
287mod private {
288    pub trait Sealed {}
289    impl Sealed for super::Pending {}
290    impl Sealed for super::DelegationAccepted {}
291    impl Sealed for super::InProgress {}
292    impl Sealed for super::DelegationCompleted {}
293    impl Sealed for super::DelegationRejected {}
294    impl Sealed for super::DelegationFailed {}
295}
296
297// ============================================================================
298// DELEGATION TYPESTATE WRAPPER
299// ============================================================================
300
301/// A delegation with compile-time state tracking.
302///
303/// The type parameter `S` indicates the current state of the delegation.
304/// Methods are only available in appropriate states:
305/// - `Delegation<Pending>`: Can be accepted, rejected, or timeout
306/// - `Delegation<DelegationAccepted>`: Can be started or fail
307/// - `Delegation<InProgress>`: Can be completed or fail
308/// - `Delegation<DelegationCompleted>`: Terminal, has result
309/// - `Delegation<DelegationRejected>`: Terminal
310/// - `Delegation<DelegationFailed>`: Terminal, has failure reason
311#[derive(Debug, Clone)]
312pub struct Delegation<S: DelegationState> {
313    data: DelegationRecord,
314    _state: PhantomData<S>,
315}
316
317impl<S: DelegationState> Delegation<S> {
318    /// Access the underlying delegation data (read-only).
319    pub fn data(&self) -> &DelegationRecord {
320        &self.data
321    }
322
323    /// Get the delegation ID.
324    pub fn delegation_id(&self) -> DelegationId {
325        self.data.delegation_id
326    }
327
328    /// Get the tenant ID.
329    pub fn tenant_id(&self) -> TenantId {
330        self.data.tenant_id
331    }
332
333    /// Get the delegating agent ID.
334    pub fn from_agent_id(&self) -> AgentId {
335        self.data.from_agent_id
336    }
337
338    /// Get the delegate agent ID.
339    pub fn to_agent_id(&self) -> AgentId {
340        self.data.to_agent_id
341    }
342
343    /// Get the trajectory ID.
344    pub fn trajectory_id(&self) -> TrajectoryId {
345        self.data.trajectory_id
346    }
347
348    /// Get the scope ID.
349    pub fn scope_id(&self) -> ScopeId {
350        self.data.scope_id
351    }
352
353    /// Get the task description.
354    pub fn task_description(&self) -> &str {
355        &self.data.task_description
356    }
357
358    /// Get when the delegation was created.
359    pub fn created_at(&self) -> Timestamp {
360        self.data.created_at
361    }
362
363    /// Get the expected completion time.
364    pub fn expected_completion(&self) -> Option<Timestamp> {
365        self.data.expected_completion
366    }
367
368    /// Get the context.
369    pub fn context(&self) -> Option<&serde_json::Value> {
370        self.data.context.as_ref()
371    }
372
373    /// Consume and return the underlying data (for serialization).
374    pub fn into_data(self) -> DelegationRecord {
375        self.data
376    }
377}
378
379impl Delegation<Pending> {
380    /// Create a new pending delegation.
381    pub fn new(data: DelegationRecord) -> Self {
382        Delegation {
383            data,
384            _state: PhantomData,
385        }
386    }
387
388    /// Accept the delegation.
389    ///
390    /// Transitions to `Delegation<DelegationAccepted>`.
391    /// Consumes the current delegation.
392    pub fn accept(mut self, accepted_at: Timestamp) -> Delegation<DelegationAccepted> {
393        self.data.accepted_at = Some(accepted_at);
394        Delegation {
395            data: self.data,
396            _state: PhantomData,
397        }
398    }
399
400    /// Reject the delegation.
401    ///
402    /// Transitions to `Delegation<DelegationRejected>` (terminal state).
403    /// Consumes the current delegation.
404    pub fn reject(mut self, reason: String) -> Delegation<DelegationRejected> {
405        self.data.rejection_reason = Some(reason);
406        Delegation {
407            data: self.data,
408            _state: PhantomData,
409        }
410    }
411
412    /// Mark the delegation as failed (e.g., timeout).
413    ///
414    /// Transitions to `Delegation<DelegationFailed>` (terminal state).
415    /// Consumes the current delegation.
416    pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
417        self.data.failure_reason = Some(reason);
418        Delegation {
419            data: self.data,
420            _state: PhantomData,
421        }
422    }
423}
424
425impl Delegation<DelegationAccepted> {
426    /// Get when the delegation was accepted.
427    pub fn accepted_at(&self) -> Timestamp {
428        self.data
429            .accepted_at
430            .expect("Accepted delegation must have accepted_at")
431    }
432
433    /// Start working on the delegation.
434    ///
435    /// Transitions to `Delegation<InProgress>`.
436    /// Consumes the current delegation.
437    pub fn start(mut self, started_at: Timestamp) -> Delegation<InProgress> {
438        self.data.started_at = Some(started_at);
439        Delegation {
440            data: self.data,
441            _state: PhantomData,
442        }
443    }
444
445    /// Mark the delegation as failed before starting.
446    ///
447    /// Transitions to `Delegation<DelegationFailed>` (terminal state).
448    /// Consumes the current delegation.
449    pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
450        self.data.failure_reason = Some(reason);
451        Delegation {
452            data: self.data,
453            _state: PhantomData,
454        }
455    }
456}
457
458impl Delegation<InProgress> {
459    /// Get when the delegation was accepted.
460    pub fn accepted_at(&self) -> Timestamp {
461        self.data
462            .accepted_at
463            .expect("In-progress delegation must have accepted_at")
464    }
465
466    /// Get when work started.
467    pub fn started_at(&self) -> Timestamp {
468        self.data
469            .started_at
470            .expect("In-progress delegation must have started_at")
471    }
472
473    /// Complete the delegation with a result.
474    ///
475    /// Transitions to `Delegation<DelegationCompleted>` (terminal state).
476    /// Consumes the current delegation.
477    pub fn complete(
478        mut self,
479        completed_at: Timestamp,
480        result: DelegationResult,
481    ) -> Delegation<DelegationCompleted> {
482        self.data.completed_at = Some(completed_at);
483        self.data.result = Some(result);
484        Delegation {
485            data: self.data,
486            _state: PhantomData,
487        }
488    }
489
490    /// Mark the delegation as failed during execution.
491    ///
492    /// Transitions to `Delegation<DelegationFailed>` (terminal state).
493    /// Consumes the current delegation.
494    pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
495        self.data.failure_reason = Some(reason);
496        Delegation {
497            data: self.data,
498            _state: PhantomData,
499        }
500    }
501}
502
503impl Delegation<DelegationCompleted> {
504    /// Get when the delegation was accepted.
505    pub fn accepted_at(&self) -> Timestamp {
506        self.data
507            .accepted_at
508            .expect("Completed delegation must have accepted_at")
509    }
510
511    /// Get when work started.
512    pub fn started_at(&self) -> Timestamp {
513        self.data
514            .started_at
515            .expect("Completed delegation must have started_at")
516    }
517
518    /// Get when the delegation was completed.
519    pub fn completed_at(&self) -> Timestamp {
520        self.data
521            .completed_at
522            .expect("Completed delegation must have completed_at")
523    }
524
525    /// Get the delegation result.
526    pub fn result(&self) -> &DelegationResult {
527        self.data
528            .result
529            .as_ref()
530            .expect("Completed delegation must have result")
531    }
532}
533
534impl Delegation<DelegationRejected> {
535    /// Get the rejection reason.
536    pub fn rejection_reason(&self) -> &str {
537        self.data
538            .rejection_reason
539            .as_deref()
540            .unwrap_or("No reason provided")
541    }
542}
543
544impl Delegation<DelegationFailed> {
545    /// Get the failure reason.
546    pub fn failure_reason(&self) -> &str {
547        self.data
548            .failure_reason
549            .as_deref()
550            .unwrap_or("No reason provided")
551    }
552}
553
554// ============================================================================
555// DATABASE BOUNDARY: STORED DELEGATION
556// ============================================================================
557
558/// A delegation as stored in the database (status-agnostic).
559///
560/// When loading from the database, we don't know the state at compile time.
561/// Use the `into_*` methods to validate and convert to a typed delegation.
562#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
563pub struct StoredDelegation {
564    pub data: DelegationRecord,
565    pub status: DelegationStatus,
566}
567
568/// Enum representing all possible runtime states of a delegation.
569/// Use this when you need to handle delegations loaded from the database.
570#[derive(Debug, Clone)]
571pub enum LoadedDelegation {
572    Pending(Delegation<Pending>),
573    Accepted(Delegation<DelegationAccepted>),
574    InProgress(Delegation<InProgress>),
575    Completed(Delegation<DelegationCompleted>),
576    Rejected(Delegation<DelegationRejected>),
577    Failed(Delegation<DelegationFailed>),
578}
579
580impl StoredDelegation {
581    /// Convert to a typed delegation based on the stored status.
582    pub fn into_typed(self) -> LoadedDelegation {
583        match self.status {
584            DelegationStatus::Pending => LoadedDelegation::Pending(Delegation {
585                data: self.data,
586                _state: PhantomData,
587            }),
588            DelegationStatus::Accepted => LoadedDelegation::Accepted(Delegation {
589                data: self.data,
590                _state: PhantomData,
591            }),
592            DelegationStatus::InProgress => LoadedDelegation::InProgress(Delegation {
593                data: self.data,
594                _state: PhantomData,
595            }),
596            DelegationStatus::Completed => LoadedDelegation::Completed(Delegation {
597                data: self.data,
598                _state: PhantomData,
599            }),
600            DelegationStatus::Rejected => LoadedDelegation::Rejected(Delegation {
601                data: self.data,
602                _state: PhantomData,
603            }),
604            DelegationStatus::Failed => LoadedDelegation::Failed(Delegation {
605                data: self.data,
606                _state: PhantomData,
607            }),
608        }
609    }
610
611    /// Try to convert to a pending delegation.
612    pub fn into_pending(self) -> Result<Delegation<Pending>, DelegationStateError> {
613        if self.status != DelegationStatus::Pending {
614            return Err(DelegationStateError::WrongState {
615                delegation_id: self.data.delegation_id,
616                expected: DelegationStatus::Pending,
617                actual: self.status,
618            });
619        }
620        Ok(Delegation {
621            data: self.data,
622            _state: PhantomData,
623        })
624    }
625
626    /// Try to convert to an accepted delegation.
627    pub fn into_accepted(self) -> Result<Delegation<DelegationAccepted>, DelegationStateError> {
628        if self.status != DelegationStatus::Accepted {
629            return Err(DelegationStateError::WrongState {
630                delegation_id: self.data.delegation_id,
631                expected: DelegationStatus::Accepted,
632                actual: self.status,
633            });
634        }
635        Ok(Delegation {
636            data: self.data,
637            _state: PhantomData,
638        })
639    }
640
641    /// Try to convert to an in-progress delegation.
642    pub fn into_in_progress(self) -> Result<Delegation<InProgress>, DelegationStateError> {
643        if self.status != DelegationStatus::InProgress {
644            return Err(DelegationStateError::WrongState {
645                delegation_id: self.data.delegation_id,
646                expected: DelegationStatus::InProgress,
647                actual: self.status,
648            });
649        }
650        Ok(Delegation {
651            data: self.data,
652            _state: PhantomData,
653        })
654    }
655
656    /// Get the underlying data without state validation.
657    pub fn data(&self) -> &DelegationRecord {
658        &self.data
659    }
660
661    /// Get the current status.
662    pub fn status(&self) -> DelegationStatus {
663        self.status
664    }
665}
666
667/// Errors when transitioning delegation states.
668#[derive(Debug, Clone, PartialEq, Eq)]
669pub enum DelegationStateError {
670    /// Delegation is not in the expected state.
671    WrongState {
672        delegation_id: DelegationId,
673        expected: DelegationStatus,
674        actual: DelegationStatus,
675    },
676}
677
678impl fmt::Display for DelegationStateError {
679    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680        match self {
681            DelegationStateError::WrongState {
682                delegation_id,
683                expected,
684                actual,
685            } => {
686                write!(
687                    f,
688                    "Delegation {} is in state {} but expected {}",
689                    delegation_id, actual, expected
690                )
691            }
692        }
693    }
694}
695
696impl std::error::Error for DelegationStateError {}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701    use crate::EntityIdType;
702    use chrono::Utc;
703
704    fn make_delegation_data() -> DelegationRecord {
705        let now = Utc::now();
706        DelegationRecord {
707            delegation_id: DelegationId::now_v7(),
708            tenant_id: TenantId::now_v7(),
709            from_agent_id: AgentId::now_v7(),
710            to_agent_id: AgentId::now_v7(),
711            trajectory_id: TrajectoryId::now_v7(),
712            scope_id: ScopeId::now_v7(),
713            task_description: "Analyze codebase".to_string(),
714            created_at: now,
715            accepted_at: None,
716            started_at: None,
717            completed_at: None,
718            expected_completion: Some(now + chrono::Duration::hours(1)),
719            result: None,
720            context: None,
721            rejection_reason: None,
722            failure_reason: None,
723        }
724    }
725
726    #[test]
727    fn test_delegation_status_roundtrip() {
728        for status in [
729            DelegationStatus::Pending,
730            DelegationStatus::Accepted,
731            DelegationStatus::InProgress,
732            DelegationStatus::Completed,
733            DelegationStatus::Rejected,
734            DelegationStatus::Failed,
735        ] {
736            let db_str = status.as_db_str();
737            let parsed = DelegationStatus::from_db_str(db_str)
738                .expect("DelegationStatus roundtrip should succeed");
739            assert_eq!(status, parsed);
740        }
741    }
742
743    #[test]
744    fn test_delegation_happy_path() {
745        let now = Utc::now();
746        let data = make_delegation_data();
747        let delegation = Delegation::<Pending>::new(data);
748
749        let accepted = delegation.accept(now);
750        assert_eq!(accepted.accepted_at(), now);
751
752        let in_progress = accepted.start(now);
753        assert_eq!(in_progress.started_at(), now);
754
755        let result = DelegationResult::success("Done", vec![]);
756        let completed = in_progress.complete(now, result);
757        assert_eq!(completed.result().status, DelegationResultStatus::Success);
758    }
759
760    #[test]
761    fn test_delegation_result_constructors() {
762        let success = DelegationResult::success("Done", vec![]);
763        assert_eq!(success.status, DelegationResultStatus::Success);
764        assert!(success.error.is_none());
765
766        let failure = DelegationResult::failure("Oops");
767        assert_eq!(failure.status, DelegationResultStatus::Failure);
768        assert_eq!(failure.error, Some("Oops".to_string()));
769    }
770
771    #[test]
772    fn test_delegation_reject() {
773        let data = make_delegation_data();
774        let delegation = Delegation::<Pending>::new(data);
775
776        let rejected = delegation.reject("Not available".to_string());
777        assert_eq!(rejected.rejection_reason(), "Not available");
778    }
779
780    #[test]
781    fn test_delegation_fail() {
782        let now = Utc::now();
783        let data = make_delegation_data();
784        let delegation = Delegation::<Pending>::new(data);
785
786        let accepted = delegation.accept(now);
787        let in_progress = accepted.start(now);
788        let failed = in_progress.fail("Timeout".to_string());
789        assert_eq!(failed.failure_reason(), "Timeout");
790    }
791
792    #[test]
793    fn test_stored_delegation_conversion() {
794        let data = make_delegation_data();
795        let stored = StoredDelegation {
796            data: data.clone(),
797            status: DelegationStatus::Pending,
798        };
799
800        let pending = stored
801            .into_pending()
802            .expect("pending delegation should convert successfully");
803        assert_eq!(pending.delegation_id(), data.delegation_id);
804    }
805
806    #[test]
807    fn test_stored_delegation_wrong_state() {
808        let data = make_delegation_data();
809        let stored = StoredDelegation {
810            data,
811            status: DelegationStatus::Accepted,
812        };
813
814        assert!(matches!(
815            stored.into_pending(),
816            Err(DelegationStateError::WrongState { .. })
817        ));
818    }
819}