nika_engine/runtime/builtin/
sleep.rs1use super::BuiltinTool;
25use crate::error::NikaError;
26use serde::{Deserialize, Serialize};
27use std::future::Future;
28use std::pin::Pin;
29use std::time::Duration;
30
31pub const MAX_SLEEP_DURATION: Duration = Duration::from_secs(5 * 60);
36
37#[derive(Debug, Clone, Deserialize)]
39struct SleepParams {
40 duration: String,
42}
43
44#[derive(Debug, Clone, Serialize)]
46struct SleepResponse {
47 slept_for_ms: u64,
49}
50
51pub struct SleepTool;
55
56impl BuiltinTool for SleepTool {
57 fn name(&self) -> &'static str {
58 "sleep"
59 }
60
61 fn description(&self) -> &'static str {
62 "Pause execution for the specified duration"
63 }
64
65 fn parameters_schema(&self) -> serde_json::Value {
66 serde_json::json!({
68 "type": "object",
69 "properties": {
70 "duration": {
71 "type": "string",
72 "description": "Duration to sleep in humantime format (e.g., '1s', '500ms', '1m30s')"
73 }
74 },
75 "required": ["duration"],
76 "additionalProperties": false
77 })
78 }
79
80 fn call<'a>(
81 &'a self,
82 args: String,
83 ) -> Pin<Box<dyn Future<Output = Result<String, NikaError>> + Send + 'a>> {
84 Box::pin(async move {
85 let params: SleepParams =
87 serde_json::from_str(&args).map_err(|e| NikaError::BuiltinInvalidParams {
88 tool: "nika:sleep".into(),
89 reason: format!("Invalid JSON parameters: {}", e),
90 })?;
91
92 let duration = humantime::parse_duration(¶ms.duration).map_err(|e| {
94 NikaError::BuiltinInvalidParams {
95 tool: "nika:sleep".into(),
96 reason: format!("Invalid duration '{}': {}", params.duration, e),
97 }
98 })?;
99
100 if duration > MAX_SLEEP_DURATION {
102 return Err(NikaError::BuiltinInvalidParams {
103 tool: "nika:sleep".into(),
104 reason: format!(
105 "Sleep duration '{}' exceeds maximum allowed {} seconds. \
106 This limit prevents workflows from being blocked indefinitely.",
107 params.duration,
108 MAX_SLEEP_DURATION.as_secs()
109 ),
110 });
111 }
112
113 tokio::time::sleep(duration).await;
115
116 let response = SleepResponse {
118 slept_for_ms: duration.as_millis() as u64,
119 };
120
121 serde_json::to_string(&response).map_err(|e| NikaError::BuiltinToolError {
122 tool: "nika:sleep".into(),
123 reason: format!("Failed to serialize response: {}", e),
124 })
125 })
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_sleep_tool_name() {
135 let tool = SleepTool;
136 assert_eq!(tool.name(), "sleep");
137 }
138
139 #[test]
140 fn test_sleep_tool_description() {
141 let tool = SleepTool;
142 assert!(tool.description().contains("Pause"));
143 }
144
145 #[test]
146 fn test_sleep_tool_schema() {
147 let tool = SleepTool;
148 let schema = tool.parameters_schema();
149 assert_eq!(schema["type"], "object");
150 assert!(schema["properties"]["duration"].is_object());
151 assert!(schema["required"]
152 .as_array()
153 .unwrap()
154 .contains(&serde_json::json!("duration")));
155 }
156
157 #[tokio::test]
158 async fn test_sleep_executes() {
159 let tool = SleepTool;
160 let start = std::time::Instant::now();
161
162 let result = tool.call(r#"{"duration": "10ms"}"#.to_string()).await;
163
164 assert!(result.is_ok());
165 let elapsed = start.elapsed();
166 assert!(elapsed.as_millis() >= 10);
168
169 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
170 assert_eq!(response["slept_for_ms"], 10);
171 }
172
173 #[tokio::test]
174 async fn test_sleep_parses_seconds() {
175 let tool = SleepTool;
176 let result = tool.call(r#"{"duration": "1ms"}"#.to_string()).await;
177
178 assert!(result.is_ok());
179 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
180 assert_eq!(response["slept_for_ms"], 1);
181 }
182
183 #[tokio::test]
184 async fn test_sleep_parses_complex_duration() {
185 let duration = humantime::parse_duration("1s500ms");
187 assert!(duration.is_ok());
188 assert_eq!(duration.unwrap().as_millis(), 1500);
189 }
190
191 #[tokio::test]
192 async fn test_sleep_invalid_duration() {
193 let tool = SleepTool;
194 let result = tool
195 .call(r#"{"duration": "not-a-duration"}"#.to_string())
196 .await;
197
198 assert!(result.is_err());
199 let err = result.unwrap_err();
200 assert!(err.to_string().contains("Invalid duration"));
201 }
202
203 #[tokio::test]
204 async fn test_sleep_invalid_json() {
205 let tool = SleepTool;
206 let result = tool.call("not json".to_string()).await;
207
208 assert!(result.is_err());
209 let err = result.unwrap_err();
210 assert!(err.to_string().contains("Invalid JSON parameters"));
211 }
212
213 #[tokio::test]
214 async fn test_sleep_missing_duration() {
215 let tool = SleepTool;
216 let result = tool.call(r#"{}"#.to_string()).await;
217
218 assert!(result.is_err());
219 let err = result.unwrap_err();
220 assert!(err.to_string().contains("Invalid JSON parameters"));
221 }
222
223 #[test]
228 fn test_max_sleep_duration_is_5_minutes() {
229 assert_eq!(MAX_SLEEP_DURATION.as_secs(), 300);
230 assert_eq!(MAX_SLEEP_DURATION, std::time::Duration::from_secs(5 * 60));
231 }
232
233 #[tokio::test]
234 async fn test_sleep_rejects_excessive_duration_hours() {
235 let tool = SleepTool;
236 let result = tool.call(r#"{"duration": "1000h"}"#.to_string()).await;
238
239 assert!(result.is_err());
240 let err = result.unwrap_err();
241 let err_str = err.to_string();
242 assert!(err_str.contains("exceeds maximum"));
243 assert!(err_str.contains("300"));
244 }
245
246 #[tokio::test]
247 async fn test_sleep_rejects_6_minutes() {
248 let tool = SleepTool;
249 let result = tool.call(r#"{"duration": "6m"}"#.to_string()).await;
251
252 assert!(result.is_err());
253 let err = result.unwrap_err();
254 assert!(err.to_string().contains("exceeds maximum"));
255 }
256
257 #[tokio::test]
258 async fn test_sleep_accepts_5_minutes() {
259 let _tool = SleepTool;
260 let duration = humantime::parse_duration("5m").unwrap();
263 assert!(duration <= MAX_SLEEP_DURATION);
264 }
265
266 #[tokio::test]
267 async fn test_sleep_accepts_4_minutes() {
268 let _tool = SleepTool;
269 let duration = humantime::parse_duration("4m").unwrap();
271 assert!(duration < MAX_SLEEP_DURATION);
272 }
273
274 #[tokio::test]
275 async fn test_sleep_accepts_just_under_limit() {
276 let _tool = SleepTool;
277 let duration = humantime::parse_duration("299s").unwrap();
279 assert!(duration < MAX_SLEEP_DURATION);
280 }
281
282 #[tokio::test]
283 async fn test_sleep_rejects_just_over_limit() {
284 let tool = SleepTool;
285 let result = tool.call(r#"{"duration": "301s"}"#.to_string()).await;
287
288 assert!(result.is_err());
289 let err = result.unwrap_err();
290 assert!(err.to_string().contains("exceeds maximum"));
291 }
292}