1use crate::input::{Input, PeerConvention};
9
10#[derive(Debug, Clone, thiserror::Error)]
12#[non_exhaustive]
13pub enum PeerHandlingModeError {
14 #[error("handling_mode is forbidden on ResponseProgress peer inputs")]
16 ForbiddenForResponseProgress,
17}
18
19pub fn validate_peer_handling_mode(input: &Input) -> Result<(), PeerHandlingModeError> {
21 let Input::Peer(peer) = input else {
22 return Ok(());
23 };
24 if peer.handling_mode.is_none() {
25 return Ok(());
26 }
27 match &peer.convention {
28 Some(PeerConvention::ResponseProgress { .. }) => {
29 Err(PeerHandlingModeError::ForbiddenForResponseProgress)
30 }
31 _ => Ok(()),
32 }
33}
34
35#[cfg(test)]
36#[allow(clippy::unwrap_used)]
37mod tests {
38 use super::*;
39 use crate::input::*;
40 use chrono::Utc;
41 use meerkat_core::lifecycle::InputId;
42 use meerkat_core::types::HandlingMode;
43
44 fn make_header() -> InputHeader {
45 InputHeader {
46 id: InputId::new(),
47 timestamp: Utc::now(),
48 source: InputOrigin::Peer {
49 peer_id: "peer-1".into(),
50 display_identity: None,
51 runtime_id: None,
52 },
53 durability: InputDurability::Durable,
54 visibility: InputVisibility::default(),
55 idempotency_key: None,
56 supersession_key: None,
57 correlation_id: None,
58 }
59 }
60
61 #[test]
62 fn response_progress_with_handling_mode_rejected() {
63 let input = Input::Peer(PeerInput {
64 header: make_header(),
65 convention: Some(PeerConvention::ResponseProgress {
66 request_id: "r".into(),
67 phase: ResponseProgressPhase::InProgress,
68 }),
69 body: "working".into(),
70 payload: Some(serde_json::json!({"progress": "working"})),
71 blocks: None,
72 handling_mode: Some(HandlingMode::Queue),
73 });
74 let err = validate_peer_handling_mode(&input).unwrap_err();
75 assert!(matches!(
76 err,
77 PeerHandlingModeError::ForbiddenForResponseProgress
78 ));
79 }
80
81 #[test]
82 fn response_terminal_with_handling_mode_accepted() {
83 let input = Input::Peer(PeerInput {
84 header: make_header(),
85 convention: Some(PeerConvention::ResponseTerminal {
86 request_id: "r".into(),
87 status: ResponseTerminalStatus::Completed,
88 }),
89 body: "done".into(),
90 payload: Some(serde_json::json!({"ok": true})),
91 blocks: None,
92 handling_mode: Some(HandlingMode::Steer),
93 });
94 assert!(validate_peer_handling_mode(&input).is_ok());
95 }
96
97 #[test]
98 fn response_terminal_with_queue_handling_mode_accepted() {
99 let input = Input::Peer(PeerInput {
100 header: make_header(),
101 convention: Some(PeerConvention::ResponseTerminal {
102 request_id: "r".into(),
103 status: ResponseTerminalStatus::Completed,
104 }),
105 body: "done".into(),
106 payload: Some(serde_json::json!({"ok": true})),
107 blocks: None,
108 handling_mode: Some(HandlingMode::Queue),
109 });
110 assert!(validate_peer_handling_mode(&input).is_ok());
111 }
112
113 #[test]
114 fn message_with_handling_mode_accepted() {
115 let input = Input::Peer(PeerInput {
116 header: make_header(),
117 convention: Some(PeerConvention::Message),
118 body: "hi".into(),
119 payload: None,
120 blocks: None,
121 handling_mode: Some(HandlingMode::Queue),
122 });
123 assert!(validate_peer_handling_mode(&input).is_ok());
124 }
125
126 #[test]
127 fn request_with_handling_mode_accepted() {
128 let input = Input::Peer(PeerInput {
129 header: make_header(),
130 convention: Some(PeerConvention::Request {
131 request_id: "r".into(),
132 intent: "i".into(),
133 }),
134 body: "do it".into(),
135 payload: Some(serde_json::json!({"subject": "x"})),
136 blocks: None,
137 handling_mode: Some(HandlingMode::Steer),
138 });
139 assert!(validate_peer_handling_mode(&input).is_ok());
140 }
141
142 #[test]
143 fn no_convention_with_handling_mode_accepted() {
144 let input = Input::Peer(PeerInput {
145 header: make_header(),
146 convention: None,
147 body: "hi".into(),
148 payload: None,
149 blocks: None,
150 handling_mode: Some(HandlingMode::Queue),
151 });
152 assert!(validate_peer_handling_mode(&input).is_ok());
153 }
154
155 #[test]
156 fn peer_without_handling_mode_always_accepted() {
157 for convention in [
158 Some(PeerConvention::Message),
159 Some(PeerConvention::Request {
160 request_id: "r".into(),
161 intent: "i".into(),
162 }),
163 Some(PeerConvention::ResponseProgress {
164 request_id: "r".into(),
165 phase: ResponseProgressPhase::InProgress,
166 }),
167 Some(PeerConvention::ResponseTerminal {
168 request_id: "r".into(),
169 status: ResponseTerminalStatus::Completed,
170 }),
171 None,
172 ] {
173 let input = Input::Peer(PeerInput {
174 header: make_header(),
175 convention,
176 body: "hi".into(),
177 payload: None,
178 blocks: None,
179 handling_mode: None,
180 });
181 assert!(
182 validate_peer_handling_mode(&input).is_ok(),
183 "should accept peer without handling_mode"
184 );
185 }
186 }
187
188 #[test]
189 fn non_peer_input_always_accepted() {
190 let input = Input::Prompt(PromptInput {
191 header: InputHeader {
192 id: InputId::new(),
193 timestamp: Utc::now(),
194 source: InputOrigin::Operator,
195 durability: InputDurability::Durable,
196 visibility: InputVisibility::default(),
197 idempotency_key: None,
198 supersession_key: None,
199 correlation_id: None,
200 },
201 text: "hi".into(),
202 blocks: None,
203 typed_turn_appends: Vec::new(),
204 turn_metadata: None,
205 });
206 assert!(validate_peer_handling_mode(&input).is_ok());
207 }
208}