deepseek/agent/builtin_tools/
cron_delete.rs1use std::sync::{Arc, Mutex};
4
5use async_trait::async_trait;
6use serde_json::{json, Value};
7
8use crate::agent::scheduler::{Scheduler, TaskId};
9use crate::agent::tool::{Tool, ToolDefinition};
10
11pub struct CronDeleteTool {
12 pub scheduler: Arc<Mutex<Scheduler>>,
13}
14
15impl CronDeleteTool {
16 pub fn new(scheduler: Arc<Mutex<Scheduler>>) -> Self {
17 Self { scheduler }
18 }
19}
20
21#[async_trait]
22impl Tool for CronDeleteTool {
23 fn name(&self) -> &str {
24 "CronDelete"
25 }
26
27 fn definition(&self) -> ToolDefinition {
28 ToolDefinition {
29 name: self.name().to_string(),
30 description: "Cancel a scheduled task by its 8-character `task_id` \
31 (as returned by CronCreate or CronList)."
32 .into(),
33 parameters: json!({
34 "type": "object",
35 "properties": {
36 "task_id": { "type": "string" }
37 },
38 "required": ["task_id"]
39 }),
40 }
41 }
42
43 async fn call_json(&self, args: Value) -> Result<String, String> {
44 let raw = args
45 .get("task_id")
46 .and_then(Value::as_str)
47 .ok_or_else(|| "CronDelete: missing string `task_id`".to_string())?;
48 let id = TaskId::from_raw(raw);
49
50 let mut sched = self
51 .scheduler
52 .lock()
53 .map_err(|_| "CronDelete: scheduler lock poisoned".to_string())?;
54
55 let deleted = sched.delete(&id);
56 Ok(serde_json::to_string(&json!({
57 "task_id": id.as_str(),
58 "deleted": deleted,
59 }))
60 .unwrap())
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::agent::scheduler::{CronExpr, Schedule, Scheduler};
68 use serde_json::Value;
69
70 fn fresh_sched() -> Arc<Mutex<Scheduler>> {
71 std::env::remove_var("CLAUDE_CODE_DISABLE_CRON");
72 std::env::remove_var("DEEPSEEK_LOOP_DISABLE_CRON");
73 Arc::new(Mutex::new(Scheduler::new("cron-delete-test")))
74 }
75
76 #[test]
77 fn definition_marks_task_id_required() {
78 let tool = CronDeleteTool::new(fresh_sched());
79 let def = tool.definition();
80 assert_eq!(def.name, "CronDelete");
81 let required = def.parameters.get("required").unwrap().as_array().unwrap();
82 assert!(required.iter().any(|v| v == "task_id"));
83 }
84
85 #[tokio::test]
86 async fn delete_returns_false_for_unknown_id() {
87 let tool = CronDeleteTool::new(fresh_sched());
88 let raw = tool
89 .call_json(json!({"task_id": "ZZZZZZZZ"}))
90 .await
91 .unwrap();
92 let v: Value = serde_json::from_str(&raw).unwrap();
93 assert_eq!(v["deleted"].as_bool().unwrap(), false);
94 assert_eq!(v["task_id"].as_str().unwrap(), "ZZZZZZZZ");
95 }
96
97 #[tokio::test]
98 async fn delete_returns_true_then_false_on_repeat() {
99 let sched = fresh_sched();
100 let id = {
101 let mut s = sched.lock().unwrap();
102 let cron = CronExpr::parse("*/5 * * * *").unwrap();
103 s.create(Schedule::Cron(Box::new(cron)), "x", true).unwrap()
104 };
105 let tool = CronDeleteTool::new(sched);
106 let first: Value = serde_json::from_str(
107 &tool
108 .call_json(json!({"task_id": id.as_str()}))
109 .await
110 .unwrap(),
111 )
112 .unwrap();
113 assert_eq!(first["deleted"].as_bool().unwrap(), true);
114
115 let second: Value = serde_json::from_str(
116 &tool
117 .call_json(json!({"task_id": id.as_str()}))
118 .await
119 .unwrap(),
120 )
121 .unwrap();
122 assert_eq!(second["deleted"].as_bool().unwrap(), false);
123 }
124
125 #[tokio::test]
126 async fn missing_task_id_returns_error() {
127 let tool = CronDeleteTool::new(fresh_sched());
128 let err = tool.call_json(json!({})).await.unwrap_err();
129 assert!(err.contains("missing"), "got: {err}");
130 }
131
132 #[tokio::test]
133 async fn non_string_task_id_returns_error() {
134 let tool = CronDeleteTool::new(fresh_sched());
135 let err = tool.call_json(json!({"task_id": 12345})).await.unwrap_err();
136 assert!(err.contains("missing"), "got: {err}");
137 }
138}