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 STATE MACHINE + WRAPPER
252// ============================================================================
253
254define_state_machine! {
255    /// Marker trait for delegation states.
256    pub trait DelegationState;
257    /// Delegation is pending acceptance.
258    Pending,
259    /// Delegation was accepted but not yet started.
260    DelegationAccepted,
261    /// Delegation work is in progress.
262    InProgress,
263    /// Delegation was completed successfully (terminal).
264    DelegationCompleted,
265    /// Delegation was rejected (terminal).
266    DelegationRejected,
267    /// Delegation failed (terminal).
268    DelegationFailed,
269}
270
271define_typestate_wrapper! {
272    /// A delegation with compile-time state tracking.
273    ///
274    /// The type parameter `S` indicates the current state of the delegation.
275    /// Methods are only available in appropriate states:
276    /// - `Delegation<Pending>`: Can be accepted, rejected, or timeout
277    /// - `Delegation<DelegationAccepted>`: Can be started or fail
278    /// - `Delegation<InProgress>`: Can be completed or fail
279    /// - `Delegation<DelegationCompleted>`: Terminal, has result
280    /// - `Delegation<DelegationRejected>`: Terminal
281    /// - `Delegation<DelegationFailed>`: Terminal, has failure reason
282    pub struct Delegation<DelegationState> wraps DelegationRecord;
283    initial_state: Pending;
284}
285
286impl<S: DelegationState> Delegation<S> {
287    /// Get the delegation ID.
288    pub fn delegation_id(&self) -> DelegationId {
289        self.data.delegation_id
290    }
291
292    /// Get the tenant ID.
293    pub fn tenant_id(&self) -> TenantId {
294        self.data.tenant_id
295    }
296
297    /// Get the delegating agent ID.
298    pub fn from_agent_id(&self) -> AgentId {
299        self.data.from_agent_id
300    }
301
302    /// Get the delegate agent ID.
303    pub fn to_agent_id(&self) -> AgentId {
304        self.data.to_agent_id
305    }
306
307    /// Get the trajectory ID.
308    pub fn trajectory_id(&self) -> TrajectoryId {
309        self.data.trajectory_id
310    }
311
312    /// Get the scope ID.
313    pub fn scope_id(&self) -> ScopeId {
314        self.data.scope_id
315    }
316
317    /// Get the task description.
318    pub fn task_description(&self) -> &str {
319        &self.data.task_description
320    }
321
322    /// Get when the delegation was created.
323    pub fn created_at(&self) -> Timestamp {
324        self.data.created_at
325    }
326
327    /// Get the expected completion time.
328    pub fn expected_completion(&self) -> Option<Timestamp> {
329        self.data.expected_completion
330    }
331
332    /// Get the context.
333    pub fn context(&self) -> Option<&serde_json::Value> {
334        self.data.context.as_ref()
335    }
336}
337
338impl Delegation<Pending> {
339    /// Accept the delegation.
340    ///
341    /// Transitions to `Delegation<DelegationAccepted>`.
342    /// Consumes the current delegation.
343    pub fn accept(mut self, accepted_at: Timestamp) -> Delegation<DelegationAccepted> {
344        self.data.accepted_at = Some(accepted_at);
345        Delegation {
346            data: self.data,
347            _state: PhantomData,
348        }
349    }
350
351    /// Reject the delegation.
352    ///
353    /// Transitions to `Delegation<DelegationRejected>` (terminal state).
354    /// Consumes the current delegation.
355    pub fn reject(mut self, reason: String) -> Delegation<DelegationRejected> {
356        self.data.rejection_reason = Some(reason);
357        Delegation {
358            data: self.data,
359            _state: PhantomData,
360        }
361    }
362
363    /// Mark the delegation as failed (e.g., timeout).
364    ///
365    /// Transitions to `Delegation<DelegationFailed>` (terminal state).
366    /// Consumes the current delegation.
367    pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
368        self.data.failure_reason = Some(reason);
369        Delegation {
370            data: self.data,
371            _state: PhantomData,
372        }
373    }
374}
375
376impl Delegation<DelegationAccepted> {
377    /// Get when the delegation was accepted.
378    pub fn accepted_at(&self) -> Timestamp {
379        self.data
380            .accepted_at
381            .expect("Accepted delegation must have accepted_at")
382    }
383
384    /// Start working on the delegation.
385    ///
386    /// Transitions to `Delegation<InProgress>`.
387    /// Consumes the current delegation.
388    pub fn start(mut self, started_at: Timestamp) -> Delegation<InProgress> {
389        self.data.started_at = Some(started_at);
390        Delegation {
391            data: self.data,
392            _state: PhantomData,
393        }
394    }
395
396    /// Mark the delegation as failed before starting.
397    ///
398    /// Transitions to `Delegation<DelegationFailed>` (terminal state).
399    /// Consumes the current delegation.
400    pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
401        self.data.failure_reason = Some(reason);
402        Delegation {
403            data: self.data,
404            _state: PhantomData,
405        }
406    }
407}
408
409impl Delegation<InProgress> {
410    /// Get when the delegation was accepted.
411    pub fn accepted_at(&self) -> Timestamp {
412        self.data
413            .accepted_at
414            .expect("In-progress delegation must have accepted_at")
415    }
416
417    /// Get when work started.
418    pub fn started_at(&self) -> Timestamp {
419        self.data
420            .started_at
421            .expect("In-progress delegation must have started_at")
422    }
423
424    /// Complete the delegation with a result.
425    ///
426    /// Transitions to `Delegation<DelegationCompleted>` (terminal state).
427    /// Consumes the current delegation.
428    pub fn complete(
429        mut self,
430        completed_at: Timestamp,
431        result: DelegationResult,
432    ) -> Delegation<DelegationCompleted> {
433        self.data.completed_at = Some(completed_at);
434        self.data.result = Some(result);
435        Delegation {
436            data: self.data,
437            _state: PhantomData,
438        }
439    }
440
441    /// Mark the delegation as failed during execution.
442    ///
443    /// Transitions to `Delegation<DelegationFailed>` (terminal state).
444    /// Consumes the current delegation.
445    pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
446        self.data.failure_reason = Some(reason);
447        Delegation {
448            data: self.data,
449            _state: PhantomData,
450        }
451    }
452}
453
454impl Delegation<DelegationCompleted> {
455    /// Get when the delegation was accepted.
456    pub fn accepted_at(&self) -> Timestamp {
457        self.data
458            .accepted_at
459            .expect("Completed delegation must have accepted_at")
460    }
461
462    /// Get when work started.
463    pub fn started_at(&self) -> Timestamp {
464        self.data
465            .started_at
466            .expect("Completed delegation must have started_at")
467    }
468
469    /// Get when the delegation was completed.
470    pub fn completed_at(&self) -> Timestamp {
471        self.data
472            .completed_at
473            .expect("Completed delegation must have completed_at")
474    }
475
476    /// Get the delegation result.
477    pub fn result(&self) -> &DelegationResult {
478        self.data
479            .result
480            .as_ref()
481            .expect("Completed delegation must have result")
482    }
483}
484
485impl Delegation<DelegationRejected> {
486    /// Get the rejection reason.
487    pub fn rejection_reason(&self) -> &str {
488        self.data
489            .rejection_reason
490            .as_deref()
491            .unwrap_or("No reason provided")
492    }
493}
494
495impl Delegation<DelegationFailed> {
496    /// Get the failure reason.
497    pub fn failure_reason(&self) -> &str {
498        self.data
499            .failure_reason
500            .as_deref()
501            .unwrap_or("No reason provided")
502    }
503}
504
505// ============================================================================
506// DATABASE BOUNDARY: STORED DELEGATION
507// ============================================================================
508
509/// A delegation as stored in the database (status-agnostic).
510///
511/// When loading from the database, we don't know the state at compile time.
512/// Use the `into_*` methods to validate and convert to a typed delegation.
513#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
514pub struct StoredDelegation {
515    pub data: DelegationRecord,
516    pub status: DelegationStatus,
517}
518
519/// Enum representing all possible runtime states of a delegation.
520/// Use this when you need to handle delegations loaded from the database.
521#[derive(Debug, Clone)]
522pub enum LoadedDelegation {
523    Pending(Delegation<Pending>),
524    Accepted(Delegation<DelegationAccepted>),
525    InProgress(Delegation<InProgress>),
526    Completed(Delegation<DelegationCompleted>),
527    Rejected(Delegation<DelegationRejected>),
528    Failed(Delegation<DelegationFailed>),
529}
530
531impl StoredDelegation {
532    /// Convert to a typed delegation based on the stored status.
533    pub fn into_typed(self) -> LoadedDelegation {
534        match self.status {
535            DelegationStatus::Pending => LoadedDelegation::Pending(Delegation {
536                data: self.data,
537                _state: PhantomData,
538            }),
539            DelegationStatus::Accepted => LoadedDelegation::Accepted(Delegation {
540                data: self.data,
541                _state: PhantomData,
542            }),
543            DelegationStatus::InProgress => LoadedDelegation::InProgress(Delegation {
544                data: self.data,
545                _state: PhantomData,
546            }),
547            DelegationStatus::Completed => LoadedDelegation::Completed(Delegation {
548                data: self.data,
549                _state: PhantomData,
550            }),
551            DelegationStatus::Rejected => LoadedDelegation::Rejected(Delegation {
552                data: self.data,
553                _state: PhantomData,
554            }),
555            DelegationStatus::Failed => LoadedDelegation::Failed(Delegation {
556                data: self.data,
557                _state: PhantomData,
558            }),
559        }
560    }
561
562    /// Try to convert to a pending delegation.
563    pub fn into_pending(self) -> Result<Delegation<Pending>, DelegationStateError> {
564        if self.status != DelegationStatus::Pending {
565            return Err(DelegationStateError::WrongState {
566                delegation_id: self.data.delegation_id,
567                expected: DelegationStatus::Pending,
568                actual: self.status,
569            });
570        }
571        Ok(Delegation {
572            data: self.data,
573            _state: PhantomData,
574        })
575    }
576
577    /// Try to convert to an accepted delegation.
578    pub fn into_accepted(self) -> Result<Delegation<DelegationAccepted>, DelegationStateError> {
579        if self.status != DelegationStatus::Accepted {
580            return Err(DelegationStateError::WrongState {
581                delegation_id: self.data.delegation_id,
582                expected: DelegationStatus::Accepted,
583                actual: self.status,
584            });
585        }
586        Ok(Delegation {
587            data: self.data,
588            _state: PhantomData,
589        })
590    }
591
592    /// Try to convert to an in-progress delegation.
593    pub fn into_in_progress(self) -> Result<Delegation<InProgress>, DelegationStateError> {
594        if self.status != DelegationStatus::InProgress {
595            return Err(DelegationStateError::WrongState {
596                delegation_id: self.data.delegation_id,
597                expected: DelegationStatus::InProgress,
598                actual: self.status,
599            });
600        }
601        Ok(Delegation {
602            data: self.data,
603            _state: PhantomData,
604        })
605    }
606
607    /// Get the underlying data without state validation.
608    pub fn data(&self) -> &DelegationRecord {
609        &self.data
610    }
611
612    /// Get the current status.
613    pub fn status(&self) -> DelegationStatus {
614        self.status
615    }
616}
617
618/// Errors when transitioning delegation states.
619#[derive(Debug, Clone, PartialEq, Eq)]
620pub enum DelegationStateError {
621    /// Delegation is not in the expected state.
622    WrongState {
623        delegation_id: DelegationId,
624        expected: DelegationStatus,
625        actual: DelegationStatus,
626    },
627}
628
629impl fmt::Display for DelegationStateError {
630    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
631        match self {
632            DelegationStateError::WrongState {
633                delegation_id,
634                expected,
635                actual,
636            } => {
637                write!(
638                    f,
639                    "Delegation {} is in state {} but expected {}",
640                    delegation_id, actual, expected
641                )
642            }
643        }
644    }
645}
646
647impl std::error::Error for DelegationStateError {}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652    use crate::EntityIdType;
653    use chrono::Utc;
654
655    fn make_delegation_data() -> DelegationRecord {
656        let now = Utc::now();
657        DelegationRecord {
658            delegation_id: DelegationId::now_v7(),
659            tenant_id: TenantId::now_v7(),
660            from_agent_id: AgentId::now_v7(),
661            to_agent_id: AgentId::now_v7(),
662            trajectory_id: TrajectoryId::now_v7(),
663            scope_id: ScopeId::now_v7(),
664            task_description: "Analyze codebase".to_string(),
665            created_at: now,
666            accepted_at: None,
667            started_at: None,
668            completed_at: None,
669            expected_completion: Some(now + chrono::Duration::hours(1)),
670            result: None,
671            context: None,
672            rejection_reason: None,
673            failure_reason: None,
674        }
675    }
676
677    #[test]
678    fn test_delegation_status_roundtrip() {
679        for status in [
680            DelegationStatus::Pending,
681            DelegationStatus::Accepted,
682            DelegationStatus::InProgress,
683            DelegationStatus::Completed,
684            DelegationStatus::Rejected,
685            DelegationStatus::Failed,
686        ] {
687            let db_str = status.as_db_str();
688            let parsed = DelegationStatus::from_db_str(db_str)
689                .expect("DelegationStatus roundtrip should succeed");
690            assert_eq!(status, parsed);
691        }
692    }
693
694    #[test]
695    fn test_delegation_happy_path() {
696        let now = Utc::now();
697        let data = make_delegation_data();
698        let delegation = Delegation::<Pending>::new(data);
699
700        let accepted = delegation.accept(now);
701        assert_eq!(accepted.accepted_at(), now);
702
703        let in_progress = accepted.start(now);
704        assert_eq!(in_progress.started_at(), now);
705
706        let result = DelegationResult::success("Done", vec![]);
707        let completed = in_progress.complete(now, result);
708        assert_eq!(completed.result().status, DelegationResultStatus::Success);
709    }
710
711    #[test]
712    fn test_delegation_result_constructors() {
713        let success = DelegationResult::success("Done", vec![]);
714        assert_eq!(success.status, DelegationResultStatus::Success);
715        assert!(success.error.is_none());
716
717        let failure = DelegationResult::failure("Oops");
718        assert_eq!(failure.status, DelegationResultStatus::Failure);
719        assert_eq!(failure.error, Some("Oops".to_string()));
720    }
721
722    #[test]
723    fn test_delegation_reject() {
724        let data = make_delegation_data();
725        let delegation = Delegation::<Pending>::new(data);
726
727        let rejected = delegation.reject("Not available".to_string());
728        assert_eq!(rejected.rejection_reason(), "Not available");
729    }
730
731    #[test]
732    fn test_delegation_fail() {
733        let now = Utc::now();
734        let data = make_delegation_data();
735        let delegation = Delegation::<Pending>::new(data);
736
737        let accepted = delegation.accept(now);
738        let in_progress = accepted.start(now);
739        let failed = in_progress.fail("Timeout".to_string());
740        assert_eq!(failed.failure_reason(), "Timeout");
741    }
742
743    #[test]
744    fn test_stored_delegation_conversion() {
745        let data = make_delegation_data();
746        let stored = StoredDelegation {
747            data: data.clone(),
748            status: DelegationStatus::Pending,
749        };
750
751        let pending = stored
752            .into_pending()
753            .expect("pending delegation should convert successfully");
754        assert_eq!(pending.delegation_id(), data.delegation_id);
755    }
756
757    #[test]
758    fn test_stored_delegation_wrong_state() {
759        let data = make_delegation_data();
760        let stored = StoredDelegation {
761            data,
762            status: DelegationStatus::Accepted,
763        };
764
765        assert!(matches!(
766            stored.into_pending(),
767            Err(DelegationStateError::WrongState { .. })
768        ));
769    }
770}