meerkat_runtime/
accept.rs1use meerkat_core::lifecycle::InputId;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7use crate::input_state::InputState;
8use crate::policy::PolicyDecision;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(tag = "reject_type", rename_all = "snake_case")]
13#[non_exhaustive]
14pub enum RejectReason {
15 NotReady {
17 state: String,
19 },
20 DurabilityViolation {
22 detail: String,
24 },
25 PeerHandlingModeInvalid {
27 detail: String,
29 },
30}
31
32impl fmt::Display for RejectReason {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::NotReady { state } => {
36 write!(f, "runtime not accepting input while in state: {state}")
37 }
38 Self::DurabilityViolation { detail } => write!(f, "{detail}"),
39 Self::PeerHandlingModeInvalid { detail } => write!(f, "{detail}"),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(tag = "outcome_type", rename_all = "snake_case")]
47#[non_exhaustive]
48#[allow(clippy::large_enum_variant)]
49pub enum AcceptOutcome {
50 Accepted {
52 input_id: InputId,
54 policy: PolicyDecision,
56 state: InputState,
58 },
59 Deduplicated {
61 input_id: InputId,
63 existing_id: InputId,
65 },
66 Rejected {
68 reason: RejectReason,
70 },
71}
72
73impl AcceptOutcome {
74 pub fn is_accepted(&self) -> bool {
76 matches!(self, Self::Accepted { .. })
77 }
78
79 pub fn is_deduplicated(&self) -> bool {
81 matches!(self, Self::Deduplicated { .. })
82 }
83
84 pub fn is_rejected(&self) -> bool {
86 matches!(self, Self::Rejected { .. })
87 }
88}
89
90#[cfg(test)]
91#[allow(clippy::unwrap_used)]
92mod tests {
93 use super::*;
94 use crate::identifiers::PolicyVersion;
95 use crate::policy::{
96 ApplyMode, ConsumePoint, DrainPolicy, QueueMode, RoutingDisposition, WakeMode,
97 };
98
99 #[test]
100 fn accepted_serde() {
101 let outcome = AcceptOutcome::Accepted {
102 input_id: InputId::new(),
103 policy: PolicyDecision {
104 apply_mode: ApplyMode::StageRunStart,
105 wake_mode: WakeMode::WakeIfIdle,
106 queue_mode: QueueMode::Fifo,
107 consume_point: ConsumePoint::OnRunComplete,
108 drain_policy: DrainPolicy::QueueNextTurn,
109 routing_disposition: RoutingDisposition::Queue,
110 record_transcript: true,
111 emit_operator_content: true,
112 policy_version: PolicyVersion(1),
113 },
114 state: InputState::new_accepted(InputId::new()),
115 };
116 let json = serde_json::to_value(&outcome).unwrap();
117 assert_eq!(json["outcome_type"], "accepted");
118 let parsed: AcceptOutcome = serde_json::from_value(json).unwrap();
119 assert!(parsed.is_accepted());
120 assert!(!parsed.is_deduplicated());
121 assert!(!parsed.is_rejected());
122 }
123
124 #[test]
125 fn deduplicated_serde() {
126 let outcome = AcceptOutcome::Deduplicated {
127 input_id: InputId::new(),
128 existing_id: InputId::new(),
129 };
130 let json = serde_json::to_value(&outcome).unwrap();
131 assert_eq!(json["outcome_type"], "deduplicated");
132 let parsed: AcceptOutcome = serde_json::from_value(json).unwrap();
133 assert!(parsed.is_deduplicated());
134 }
135
136 #[test]
137 fn rejected_serde() {
138 let outcome = AcceptOutcome::Rejected {
139 reason: RejectReason::DurabilityViolation {
140 detail: "durability violation".into(),
141 },
142 };
143 let json = serde_json::to_value(&outcome).unwrap();
144 assert_eq!(json["outcome_type"], "rejected");
145 assert_eq!(json["reason"]["reject_type"], "durability_violation");
146 let parsed: AcceptOutcome = serde_json::from_value(json).unwrap();
147 assert!(parsed.is_rejected());
148 }
149
150 #[test]
151 fn reject_reason_display() {
152 let not_ready = RejectReason::NotReady {
153 state: "Stopped".into(),
154 };
155 assert_eq!(
156 not_ready.to_string(),
157 "runtime not accepting input while in state: Stopped"
158 );
159
160 let durability = RejectReason::DurabilityViolation {
161 detail: "Derived durability forbidden for prompt".into(),
162 };
163 assert_eq!(
164 durability.to_string(),
165 "Derived durability forbidden for prompt"
166 );
167
168 let peer = RejectReason::PeerHandlingModeInvalid {
169 detail: "handling_mode is forbidden on ResponseProgress peer inputs".into(),
170 };
171 assert_eq!(
172 peer.to_string(),
173 "handling_mode is forbidden on ResponseProgress peer inputs"
174 );
175 }
176
177 #[test]
178 fn reject_reason_serde_round_trip() {
179 let reasons = vec![
180 RejectReason::NotReady {
181 state: "Destroyed".into(),
182 },
183 RejectReason::DurabilityViolation {
184 detail: "external derived".into(),
185 },
186 RejectReason::PeerHandlingModeInvalid {
187 detail: "forbidden".into(),
188 },
189 ];
190 for reason in reasons {
191 let json = serde_json::to_value(&reason).unwrap();
192 let parsed: RejectReason = serde_json::from_value(json).unwrap();
193 assert_eq!(parsed, reason);
194 }
195 }
196}