1use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22
23pub(crate) const DEFAULT_CHECK_PROCESS_TAIL_LINES: usize = 50;
24
25fn default_read_file_mode() -> ReadFileMode {
26 ReadFileMode::Preview
27}
28
29#[derive(Debug, Deserialize, Serialize, JsonSchema)]
31pub struct ExecParams {
32 pub command: String,
34
35 #[serde(default)]
42 pub background: bool,
43
44 pub timeout_ms: Option<u64>,
46
47 pub log_path: Option<String>,
51}
52
53#[derive(Debug, Deserialize, Serialize, JsonSchema)]
55pub struct SudoExecParams {
56 pub command: String,
58
59 #[serde(default)]
66 pub background: bool,
67
68 pub timeout_ms: Option<u64>,
70
71 pub log_path: Option<String>,
75}
76
77#[derive(Debug, Deserialize, Serialize, JsonSchema)]
83pub struct CheckProcessParams {
84 pub job_id: String,
86
87 #[serde(default = "default_tail_lines")]
89 pub tail_lines: usize,
90}
91
92#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
94#[serde(rename_all = "lowercase")]
95pub enum ReadFileMode {
96 Preview,
98 Head,
100 Tail,
102 Full,
104}
105
106impl ReadFileMode {
107 pub const fn as_str(self) -> &'static str {
108 match self {
109 Self::Preview => "preview",
110 Self::Head => "head",
111 Self::Tail => "tail",
112 Self::Full => "full",
113 }
114 }
115}
116
117#[derive(Debug, Deserialize, Serialize, JsonSchema)]
118pub struct ReadFileParams {
119 pub remote_path: String,
121
122 #[serde(default = "default_read_file_mode")]
124 pub mode: ReadFileMode,
125
126 pub lines: Option<usize>,
128
129 pub timeout_ms: Option<u64>,
131}
132
133#[derive(Debug, Deserialize, Serialize, JsonSchema)]
135#[serde(deny_unknown_fields)]
136pub struct WriteFileParams {
137 pub remote_path: String,
139
140 pub new_content: String,
142
143 pub expected_sha256: Option<String>,
145
146 pub read_ticket: Option<String>,
148
149 pub dry_run: Option<bool>,
151
152 pub timeout_ms: Option<u64>,
154}
155
156#[derive(Debug, Deserialize, Serialize, JsonSchema)]
158#[serde(deny_unknown_fields)]
159pub struct ReplaceInFileParams {
160 pub remote_path: String,
162
163 pub old_text: String,
165
166 pub new_text: String,
168
169 pub scope_text: Option<String>,
171
172 pub replace_all: Option<bool>,
174
175 pub match_index: Option<usize>,
177
178 pub dry_run: Option<bool>,
180
181 pub expected_sha256: Option<String>,
183
184 pub timeout_ms: Option<u64>,
186}
187
188fn default_tail_lines() -> usize {
189 DEFAULT_CHECK_PROCESS_TAIL_LINES
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_exec_params_deserialize() {
198 let json = r#"{"command": "echo hello"}"#;
199 let params: ExecParams = serde_json::from_str(json).unwrap();
200 assert_eq!(params.command, "echo hello");
201 assert!(!params.background);
202 assert!(params.timeout_ms.is_none());
203 assert!(params.log_path.is_none());
204 }
205
206 #[test]
207 fn test_exec_params_deserialize_background() {
208 let json = r#"{"command": "sleep 10", "background": true, "timeout_ms": 1000, "log_path": "/tmp/x.log"}"#;
209 let params: ExecParams = serde_json::from_str(json).unwrap();
210 assert_eq!(params.command, "sleep 10");
211 assert!(params.background);
212 assert_eq!(params.timeout_ms, Some(1000));
213 assert_eq!(params.log_path.as_deref(), Some("/tmp/x.log"));
214 }
215
216 #[test]
217 fn test_sudo_exec_params_deserialize() {
218 let json = r#"{"command": "apt update"}"#;
219 let params: SudoExecParams = serde_json::from_str(json).unwrap();
220 assert_eq!(params.command, "apt update");
221 assert!(!params.background);
222 assert!(params.timeout_ms.is_none());
223 assert!(params.log_path.is_none());
224 }
225
226 #[test]
227 fn test_check_process_params_deserialize() {
228 let json = r#"{"job_id": "job-123"}"#;
229 let params: CheckProcessParams = serde_json::from_str(json).unwrap();
230 assert_eq!(params.job_id, "job-123");
231 assert_eq!(params.tail_lines, 50);
232 }
233
234 #[test]
235 fn test_check_process_params_with_tail_lines() {
236 let json = r#"{"job_id": "job-123", "tail_lines": 100}"#;
237 let params: CheckProcessParams = serde_json::from_str(json).unwrap();
238 assert_eq!(params.job_id, "job-123");
239 assert_eq!(params.tail_lines, 100);
240 }
241
242 #[test]
243 fn test_read_file_params_deserialize() {
244 let json = r#"{"remote_path": "/etc/hosts"}"#;
245 let params: ReadFileParams = serde_json::from_str(json).unwrap();
246 assert_eq!(params.remote_path, "/etc/hosts");
247 assert_eq!(params.mode, ReadFileMode::Preview);
248 assert_eq!(params.lines, None);
249 assert!(params.timeout_ms.is_none());
250 }
251
252 #[test]
253 fn test_read_file_params_deserialize_with_timeout() {
254 let json = r#"{"remote_path": "/etc/hosts", "timeout_ms": 2500}"#;
255 let params: ReadFileParams = serde_json::from_str(json).unwrap();
256 assert_eq!(params.remote_path, "/etc/hosts");
257 assert_eq!(params.mode, ReadFileMode::Preview);
258 assert_eq!(params.lines, None);
259 assert_eq!(params.timeout_ms, Some(2500));
260 }
261
262 #[test]
263 fn test_read_file_params_deserialize_with_mode_and_lines() {
264 let json = r#"{"remote_path":"/etc/hosts","mode":"tail","lines":120}"#;
265 let params: ReadFileParams = serde_json::from_str(json).unwrap();
266 assert_eq!(params.remote_path, "/etc/hosts");
267 assert_eq!(params.mode, ReadFileMode::Tail);
268 assert_eq!(params.lines, Some(120));
269 assert!(params.timeout_ms.is_none());
270 }
271
272 #[test]
273 fn test_read_file_mode_serialization_is_lowercase() {
274 let value = serde_json::to_value(ReadFileMode::Full).unwrap();
275 assert_eq!(value, serde_json::json!("full"));
276 }
277
278 #[test]
279 fn test_write_file_params_deserialize() {
280 let json = r#"{"remote_path":"/etc/hosts","new_content":"127.0.0.1 localhost\n"}"#;
281 let params: WriteFileParams = serde_json::from_str(json).unwrap();
282 assert_eq!(params.remote_path, "/etc/hosts");
283 assert_eq!(params.new_content, "127.0.0.1 localhost\n");
284 assert!(params.expected_sha256.is_none());
285 assert!(params.read_ticket.is_none());
286 assert!(params.dry_run.is_none());
287 assert!(params.timeout_ms.is_none());
288 }
289
290 #[test]
291 fn test_write_file_params_deserialize_with_expected_hash_and_timeout() {
292 let json = r#"{"remote_path":"/etc/hosts","new_content":"x","expected_sha256":"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff","timeout_ms":4000}"#;
293 let params: WriteFileParams = serde_json::from_str(json).unwrap();
294 assert_eq!(params.remote_path, "/etc/hosts");
295 assert_eq!(params.new_content, "x");
296 assert_eq!(
297 params.expected_sha256.as_deref(),
298 Some("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
299 );
300 assert!(params.read_ticket.is_none());
301 assert!(params.dry_run.is_none());
302 assert_eq!(params.timeout_ms, Some(4000));
303 }
304
305 #[test]
306 fn test_replace_in_file_params_deserialize_defaults_replace_all() {
307 let json = r#"{"remote_path":"/etc/hosts","old_text":"127.0.0.1","new_text":"127.0.0.2"}"#;
308 let params: ReplaceInFileParams = serde_json::from_str(json).unwrap();
309 assert_eq!(params.remote_path, "/etc/hosts");
310 assert_eq!(params.old_text, "127.0.0.1");
311 assert_eq!(params.new_text, "127.0.0.2");
312 assert!(params.scope_text.is_none());
313 assert!(params.replace_all.is_none());
314 assert!(params.match_index.is_none());
315 assert!(params.dry_run.is_none());
316 assert!(params.expected_sha256.is_none());
317 assert!(params.timeout_ms.is_none());
318 }
319
320 #[test]
321 fn test_replace_in_file_params_deserialize_replace_all_true() {
322 let json = r#"{"remote_path":"/etc/hosts","old_text":"x","new_text":"y","scope_text":"block","replace_all":true,"match_index":3,"dry_run":true,"timeout_ms":2000}"#;
323 let params: ReplaceInFileParams = serde_json::from_str(json).unwrap();
324 assert_eq!(params.remote_path, "/etc/hosts");
325 assert_eq!(params.old_text, "x");
326 assert_eq!(params.new_text, "y");
327 assert_eq!(params.scope_text.as_deref(), Some("block"));
328 assert_eq!(params.replace_all, Some(true));
329 assert_eq!(params.match_index, Some(3));
330 assert_eq!(params.dry_run, Some(true));
331 assert_eq!(params.timeout_ms, Some(2000));
332 }
333
334 #[test]
335 fn test_write_file_params_reject_unknown_fields() {
336 let json = r#"{"remote_path":"/etc/hosts","new_content":"x","old_text":"y"}"#;
337 let err = serde_json::from_str::<WriteFileParams>(json).unwrap_err();
338 assert!(err.to_string().contains("unknown field `old_text`"));
339 }
340
341 #[test]
342 fn test_replace_in_file_params_reject_unknown_fields() {
343 let json =
344 r#"{"remote_path":"/etc/hosts","old_text":"x","new_text":"y","read_ticket":"rt1.x"}"#;
345 let err = serde_json::from_str::<ReplaceInFileParams>(json).unwrap_err();
346 assert!(err.to_string().contains("unknown field `read_ticket`"));
347 }
348}