1use thiserror::Error;
19use uuid::Uuid;
20
21use crate::{oda::OdaSlot, phase::OperationalPhase};
22
23#[derive(Debug, Error)]
25pub enum CatapultError {
26 #[error("franchise not found: {0}")]
27 FranchiseNotFound(Uuid),
28 #[error("newco not found: {0}")]
29 NewcoNotFound(Uuid),
30 #[error("invalid phase transition: {from:?} -> {to:?}")]
31 InvalidPhaseTransition {
32 from: OperationalPhase,
33 to: OperationalPhase,
34 },
35 #[error("roster incomplete for phase {phase:?}: need {needed}, have {have}")]
36 RosterIncomplete {
37 phase: OperationalPhase,
38 needed: usize,
39 have: usize,
40 },
41 #[error("agent slot already filled: {0}")]
42 SlotAlreadyFilled(OdaSlot),
43 #[error("agent slot empty: {0}")]
44 SlotEmpty(OdaSlot),
45 #[error("budget exceeded: spent={spent_cents} limit={limit_cents}")]
46 BudgetExceeded { spent_cents: u64, limit_cents: u64 },
47 #[error("heartbeat timeout: agent {agent_did} last seen {elapsed_ms}ms ago")]
48 HeartbeatTimeout { agent_did: String, elapsed_ms: u64 },
49 #[error("goal not found: {0}")]
50 GoalNotFound(Uuid),
51 #[error("duplicate goal: {0}")]
52 DuplicateGoal(Uuid),
53 #[error("franchise already exists: {0}")]
54 FranchiseAlreadyExists(Uuid),
55 #[error("newco already exists: {0}")]
56 NewcoAlreadyExists(Uuid),
57 #[error("invalid catapult agent: {reason}")]
58 InvalidAgent { reason: String },
59 #[error("invalid budget policy: {reason}")]
60 InvalidBudgetPolicy { reason: String },
61 #[error("invalid cost event: {reason}")]
62 InvalidCostEvent { reason: String },
63 #[error("invalid goal: {reason}")]
64 InvalidGoal { reason: String },
65 #[error("invalid heartbeat record: {reason}")]
66 InvalidHeartbeat { reason: String },
67 #[error("invalid franchise blueprint: {reason}")]
68 InvalidFranchiseBlueprint { reason: String },
69 #[error("invalid newco: {reason}")]
70 InvalidNewco { reason: String },
71 #[error("invalid franchise receipt: {reason}")]
72 InvalidReceipt { reason: String },
73 #[error("franchise receipt serialization failed: {reason}")]
74 ReceiptSerializationFailed { reason: String },
75 #[error("franchise receipt chain broken at index {index}")]
76 ReceiptChainBroken { index: usize },
77}
78
79pub type Result<T> = std::result::Result<T, CatapultError>;
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 #[test]
86 fn all_display() {
87 let es: Vec<CatapultError> = vec![
88 CatapultError::FranchiseNotFound(Uuid::nil()),
89 CatapultError::NewcoNotFound(Uuid::nil()),
90 CatapultError::InvalidPhaseTransition {
91 from: OperationalPhase::Assessment,
92 to: OperationalPhase::Execution,
93 },
94 CatapultError::RosterIncomplete {
95 phase: OperationalPhase::Execution,
96 needed: 12,
97 have: 2,
98 },
99 CatapultError::SlotAlreadyFilled(OdaSlot::VentureCommander),
100 CatapultError::SlotEmpty(OdaSlot::VentureCommander),
101 CatapultError::BudgetExceeded {
102 spent_cents: 100,
103 limit_cents: 50,
104 },
105 CatapultError::HeartbeatTimeout {
106 agent_did: "did:exo:test".into(),
107 elapsed_ms: 600_000,
108 },
109 CatapultError::GoalNotFound(Uuid::nil()),
110 CatapultError::DuplicateGoal(Uuid::nil()),
111 CatapultError::FranchiseAlreadyExists(Uuid::nil()),
112 CatapultError::NewcoAlreadyExists(Uuid::nil()),
113 CatapultError::InvalidAgent {
114 reason: "bad agent".into(),
115 },
116 CatapultError::InvalidBudgetPolicy {
117 reason: "bad policy".into(),
118 },
119 CatapultError::InvalidCostEvent {
120 reason: "bad cost".into(),
121 },
122 CatapultError::InvalidGoal {
123 reason: "bad goal".into(),
124 },
125 CatapultError::InvalidHeartbeat {
126 reason: "bad heartbeat".into(),
127 },
128 CatapultError::InvalidFranchiseBlueprint {
129 reason: "bad blueprint".into(),
130 },
131 CatapultError::InvalidNewco {
132 reason: "bad newco".into(),
133 },
134 CatapultError::InvalidReceipt {
135 reason: "bad receipt".into(),
136 },
137 CatapultError::ReceiptSerializationFailed {
138 reason: "bad cbor".into(),
139 },
140 CatapultError::ReceiptChainBroken { index: 3 },
141 ];
142 for e in &es {
143 assert!(!e.to_string().is_empty());
144 }
145 }
146}