zeroclaw_runtime/tools/
cron_list.rs1use super::cron_common::cron_job_output;
2use crate::cron;
3use async_trait::async_trait;
4use serde_json::json;
5use std::sync::Arc;
6use zeroclaw_api::tool::{Tool, ToolResult};
7use zeroclaw_config::schema::Config;
8
9pub struct CronListTool {
10 config: Arc<Config>,
11}
12
13impl CronListTool {
14 pub fn new(config: Arc<Config>) -> Self {
15 Self { config }
16 }
17}
18
19#[async_trait]
20impl Tool for CronListTool {
21 fn name(&self) -> &str {
22 "cron_list"
23 }
24
25 fn description(&self) -> &str {
26 "List all scheduled cron jobs"
27 }
28
29 fn parameters_schema(&self) -> serde_json::Value {
30 json!({
31 "type": "object",
32 "properties": {},
33 "additionalProperties": false
34 })
35 }
36
37 async fn execute(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> {
38 if !self.config.scheduler.enabled {
39 return Ok(ToolResult {
40 success: false,
41 output: String::new(),
42 error: Some("cron is disabled by config (scheduler.enabled=false)".to_string()),
43 });
44 }
45
46 match cron::list_jobs(&self.config) {
47 Ok(jobs) => Ok(ToolResult {
48 success: true,
49 output: serde_json::to_string_pretty(
50 &jobs
51 .iter()
52 .map(cron_job_output)
53 .collect::<serde_json::Result<Vec<_>>>()?,
54 )?,
55 error: None,
56 }),
57 Err(e) => Ok(ToolResult {
58 success: false,
59 output: String::new(),
60 error: Some(e.to_string()),
61 }),
62 }
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use tempfile::TempDir;
70 use zeroclaw_config::schema::Config;
71
72 const TEST_AGENT: &str = "test-agent";
73
74 async fn test_config(tmp: &TempDir) -> Arc<Config> {
75 let mut config = Config {
76 data_dir: tmp.path().join("data"),
77 config_path: tmp.path().join("config.toml"),
78 ..Config::default()
79 };
80 seed_test_agent(&mut config);
81 tokio::fs::create_dir_all(&config.data_dir).await.unwrap();
82 Arc::new(config)
83 }
84
85 fn seed_test_agent(config: &mut Config) {
86 config
87 .risk_profiles
88 .entry(TEST_AGENT.to_string())
89 .or_default();
90 config
91 .runtime_profiles
92 .entry(TEST_AGENT.to_string())
93 .or_default();
94 config
95 .providers
96 .models
97 .ensure("openrouter", TEST_AGENT)
98 .expect("known family");
99 config.agents.entry(TEST_AGENT.to_string()).or_insert(
100 zeroclaw_config::schema::AliasedAgentConfig {
101 model_provider: format!("openrouter.{TEST_AGENT}").into(),
102 risk_profile: TEST_AGENT.to_string(),
103 runtime_profile: TEST_AGENT.to_string(),
104 ..Default::default()
105 },
106 );
107 }
108
109 #[tokio::test]
110 async fn returns_empty_list_when_no_jobs() {
111 let tmp = TempDir::new().unwrap();
112 let cfg = test_config(&tmp).await;
113 let tool = CronListTool::new(cfg);
114
115 let result = tool.execute(json!({})).await.unwrap();
116 assert!(result.success);
117 assert_eq!(result.output.trim(), "[]");
118 }
119
120 #[tokio::test]
121 async fn output_includes_timezone_confirmation_fields_for_cron_jobs() {
122 let tmp = TempDir::new().unwrap();
123 let cfg = test_config(&tmp).await;
124 cron::add_shell_job(
125 &cfg,
126 TEST_AGENT,
127 None,
128 cron::Schedule::Cron {
129 expr: "0 9 * * 1-5".into(),
130 tz: Some("America/New_York".into()),
131 },
132 "echo ok",
133 )
134 .unwrap();
135 let tool = CronListTool::new(cfg);
136
137 let result = tool.execute(json!({})).await.unwrap();
138
139 assert!(result.success, "{:?}", result.error);
140 let output: serde_json::Value = serde_json::from_str(&result.output).unwrap();
141 let job = &output[0];
142 assert_eq!(job["next_run"], job["next_run_utc"]);
143 assert_eq!(job["schedule_timezone"], "America/New_York");
144 assert_eq!(job["timezone_source"], "explicit");
145 assert!(
146 job["next_run_local"]
147 .as_str()
148 .is_some_and(|value| value.contains("T09:00:00")),
149 "next_run_local should display the next run in the explicit schedule timezone: {job}"
150 );
151 }
152
153 #[tokio::test]
154 async fn errors_when_cron_disabled() {
155 let tmp = TempDir::new().unwrap();
156 let mut cfg = (*test_config(&tmp).await).clone();
157 cfg.scheduler.enabled = false;
158 let tool = CronListTool::new(Arc::new(cfg));
159
160 let result = tool.execute(json!({})).await.unwrap();
161 assert!(!result.success);
162 assert!(
163 result
164 .error
165 .unwrap_or_default()
166 .contains("cron is disabled")
167 );
168 }
169}