Skip to main content

zeroclaw_runtime/tools/
cron_list.rs

1use 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}