1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use super::Staleness;
5
6#[derive(Serialize, Deserialize, Debug, Clone)]
7pub struct Command {
8 pub id: String,
9 pub version: String,
10 pub request_id: String,
11 pub job_id: Option<String>,
12 pub shell: Shell,
13 pub script: String,
14 pub timeout_secs: u64,
15 pub jitter_secs: Option<u64>,
16 #[serde(default)]
20 pub run_as: RunAs,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub cwd: Option<String>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub deadline_at: Option<DateTime<Utc>>,
37 #[serde(default)]
44 pub staleness: Staleness,
45}
46
47#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
48#[serde(rename_all = "lowercase")]
49pub enum Shell {
50 Powershell,
51 Cmd,
52}
53
54#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
70#[serde(rename_all = "snake_case")]
71pub enum RunAs {
72 #[default]
75 System,
76 User,
81 SystemGui,
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 fn sample_command() -> Command {
92 Command {
93 id: "echo-test".into(),
94 version: "1.0.0".into(),
95 request_id: "req-1".into(),
96 job_id: Some("dep-1".into()),
97 shell: Shell::Powershell,
98 script: "echo hi".into(),
99 timeout_secs: 30,
100 jitter_secs: Some(5),
101 run_as: RunAs::System,
102 cwd: None,
103 deadline_at: None,
104 staleness: Staleness::Cached,
105 }
106 }
107
108 #[test]
109 fn shell_serialises_lowercase() {
110 let json = serde_json::to_string(&Shell::Powershell).unwrap();
111 assert_eq!(json, "\"powershell\"");
112 let json = serde_json::to_string(&Shell::Cmd).unwrap();
113 assert_eq!(json, "\"cmd\"");
114 }
115
116 #[test]
117 fn run_as_serialises_snake_case() {
118 for (mode, expected) in [
119 (RunAs::System, "\"system\""),
120 (RunAs::User, "\"user\""),
121 (RunAs::SystemGui, "\"system_gui\""),
122 ] {
123 let json = serde_json::to_string(&mode).unwrap();
124 assert_eq!(json, expected, "serialise {mode:?}");
125 let back: RunAs = serde_json::from_str(expected).unwrap();
126 assert_eq!(back, mode, "round-trip {expected}");
127 }
128 }
129
130 #[test]
131 fn run_as_defaults_to_system() {
132 assert_eq!(RunAs::default(), RunAs::System);
133 }
134
135 #[test]
136 fn command_round_trips_through_json() {
137 let orig = sample_command();
138 let json = serde_json::to_string(&orig).expect("encode");
139 let decoded: Command = serde_json::from_str(&json).expect("decode");
140 assert_eq!(decoded.id, orig.id);
141 assert_eq!(decoded.version, orig.version);
142 assert_eq!(decoded.request_id, orig.request_id);
143 assert_eq!(decoded.job_id, orig.job_id);
144 assert_eq!(decoded.shell, orig.shell);
145 assert_eq!(decoded.script, orig.script);
146 assert_eq!(decoded.timeout_secs, orig.timeout_secs);
147 assert_eq!(decoded.jitter_secs, orig.jitter_secs);
148 assert_eq!(decoded.run_as, orig.run_as);
149 }
150
151 #[test]
152 fn command_round_trips_each_run_as_variant() {
153 for mode in [RunAs::System, RunAs::User, RunAs::SystemGui] {
154 let cmd = Command {
155 run_as: mode,
156 ..sample_command()
157 };
158 let json = serde_json::to_string(&cmd).unwrap();
159 let back: Command = serde_json::from_str(&json).unwrap();
160 assert_eq!(back.run_as, mode);
161 }
162 }
163
164 #[test]
165 fn command_accepts_missing_optional_fields() {
166 let json = r#"{
167 "id": "x",
168 "version": "1.0.0",
169 "request_id": "r",
170 "shell": "cmd",
171 "script": "echo",
172 "timeout_secs": 5
173 }"#;
174 let cmd: Command = serde_json::from_str(json).expect("decode");
175 assert!(cmd.job_id.is_none());
176 assert!(cmd.jitter_secs.is_none());
177 assert_eq!(cmd.shell, Shell::Cmd);
178 assert_eq!(cmd.run_as, RunAs::System);
180 assert!(cmd.cwd.is_none());
182 assert!(cmd.deadline_at.is_none());
184 }
185
186 #[test]
187 fn command_deadline_at_round_trips() {
188 use chrono::TimeZone;
189 let deadline = Utc.with_ymd_and_hms(2026, 5, 18, 9, 30, 0).unwrap();
190 let cmd = Command {
191 deadline_at: Some(deadline),
192 ..sample_command()
193 };
194 let json = serde_json::to_string(&cmd).unwrap();
195 let back: Command = serde_json::from_str(&json).unwrap();
196 assert_eq!(back.deadline_at, Some(deadline));
197 }
198}