1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Serialize, Deserialize, Debug, Clone)]
5pub struct Command {
6 pub id: String,
7 pub version: String,
8 pub request_id: String,
9 pub job_id: Option<String>,
10 pub shell: Shell,
11 pub script: String,
12 pub timeout_secs: u64,
13 pub jitter_secs: Option<u64>,
14 #[serde(default)]
18 pub run_as: RunAs,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub cwd: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub deadline_at: Option<DateTime<Utc>>,
35}
36
37#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
38#[serde(rename_all = "lowercase")]
39pub enum Shell {
40 Powershell,
41 Cmd,
42}
43
44#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
60#[serde(rename_all = "snake_case")]
61pub enum RunAs {
62 #[default]
65 System,
66 User,
71 SystemGui,
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn sample_command() -> Command {
82 Command {
83 id: "echo-test".into(),
84 version: "1.0.0".into(),
85 request_id: "req-1".into(),
86 job_id: Some("dep-1".into()),
87 shell: Shell::Powershell,
88 script: "echo hi".into(),
89 timeout_secs: 30,
90 jitter_secs: Some(5),
91 run_as: RunAs::System,
92 cwd: None,
93 deadline_at: None,
94 }
95 }
96
97 #[test]
98 fn shell_serialises_lowercase() {
99 let json = serde_json::to_string(&Shell::Powershell).unwrap();
100 assert_eq!(json, "\"powershell\"");
101 let json = serde_json::to_string(&Shell::Cmd).unwrap();
102 assert_eq!(json, "\"cmd\"");
103 }
104
105 #[test]
106 fn run_as_serialises_snake_case() {
107 for (mode, expected) in [
108 (RunAs::System, "\"system\""),
109 (RunAs::User, "\"user\""),
110 (RunAs::SystemGui, "\"system_gui\""),
111 ] {
112 let json = serde_json::to_string(&mode).unwrap();
113 assert_eq!(json, expected, "serialise {mode:?}");
114 let back: RunAs = serde_json::from_str(expected).unwrap();
115 assert_eq!(back, mode, "round-trip {expected}");
116 }
117 }
118
119 #[test]
120 fn run_as_defaults_to_system() {
121 assert_eq!(RunAs::default(), RunAs::System);
122 }
123
124 #[test]
125 fn command_round_trips_through_json() {
126 let orig = sample_command();
127 let json = serde_json::to_string(&orig).expect("encode");
128 let decoded: Command = serde_json::from_str(&json).expect("decode");
129 assert_eq!(decoded.id, orig.id);
130 assert_eq!(decoded.version, orig.version);
131 assert_eq!(decoded.request_id, orig.request_id);
132 assert_eq!(decoded.job_id, orig.job_id);
133 assert_eq!(decoded.shell, orig.shell);
134 assert_eq!(decoded.script, orig.script);
135 assert_eq!(decoded.timeout_secs, orig.timeout_secs);
136 assert_eq!(decoded.jitter_secs, orig.jitter_secs);
137 assert_eq!(decoded.run_as, orig.run_as);
138 }
139
140 #[test]
141 fn command_round_trips_each_run_as_variant() {
142 for mode in [RunAs::System, RunAs::User, RunAs::SystemGui] {
143 let cmd = Command {
144 run_as: mode,
145 ..sample_command()
146 };
147 let json = serde_json::to_string(&cmd).unwrap();
148 let back: Command = serde_json::from_str(&json).unwrap();
149 assert_eq!(back.run_as, mode);
150 }
151 }
152
153 #[test]
154 fn command_accepts_missing_optional_fields() {
155 let json = r#"{
156 "id": "x",
157 "version": "1.0.0",
158 "request_id": "r",
159 "shell": "cmd",
160 "script": "echo",
161 "timeout_secs": 5
162 }"#;
163 let cmd: Command = serde_json::from_str(json).expect("decode");
164 assert!(cmd.job_id.is_none());
165 assert!(cmd.jitter_secs.is_none());
166 assert_eq!(cmd.shell, Shell::Cmd);
167 assert_eq!(cmd.run_as, RunAs::System);
169 assert!(cmd.cwd.is_none());
171 assert!(cmd.deadline_at.is_none());
173 }
174
175 #[test]
176 fn command_deadline_at_round_trips() {
177 use chrono::TimeZone;
178 let deadline = Utc.with_ymd_and_hms(2026, 5, 18, 9, 30, 0).unwrap();
179 let cmd = Command {
180 deadline_at: Some(deadline),
181 ..sample_command()
182 };
183 let json = serde_json::to_string(&cmd).unwrap();
184 let back: Command = serde_json::from_str(&json).unwrap();
185 assert_eq!(back.deadline_at, Some(deadline));
186 }
187}