1use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21
22pub(crate) const DEFAULT_CHECK_PROCESS_TAIL_LINES: usize = 50;
23
24fn default_read_file_mode() -> ReadFileMode {
25 ReadFileMode::Preview
26}
27
28#[derive(Debug, Deserialize, Serialize, JsonSchema)]
30pub struct ExecParams {
31 pub command: String,
33
34 #[serde(default)]
41 pub background: bool,
42
43 pub timeout_ms: Option<u64>,
45
46 pub log_path: Option<String>,
50}
51
52#[derive(Debug, Deserialize, Serialize, JsonSchema)]
54pub struct SudoExecParams {
55 pub command: String,
57
58 #[serde(default)]
65 pub background: bool,
66
67 pub timeout_ms: Option<u64>,
69
70 pub log_path: Option<String>,
74}
75
76#[derive(Debug, Deserialize, Serialize, JsonSchema)]
82pub struct CheckProcessParams {
83 pub job_id: String,
85
86 #[serde(default = "default_tail_lines")]
88 pub tail_lines: usize,
89}
90
91#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
93#[serde(rename_all = "lowercase")]
94pub enum ReadFileMode {
95 Preview,
97 Head,
99 Tail,
101 Full,
103}
104
105impl ReadFileMode {
106 pub const fn as_str(self) -> &'static str {
107 match self {
108 Self::Preview => "preview",
109 Self::Head => "head",
110 Self::Tail => "tail",
111 Self::Full => "full",
112 }
113 }
114}
115
116#[derive(Debug, Deserialize, Serialize, JsonSchema)]
117pub struct ReadFileParams {
118 pub remote_path: String,
120
121 #[serde(default = "default_read_file_mode")]
123 pub mode: ReadFileMode,
124
125 pub lines: Option<usize>,
127
128 pub timeout_ms: Option<u64>,
130}
131
132#[derive(Debug, Deserialize, Serialize, JsonSchema)]
134#[serde(deny_unknown_fields)]
135pub struct ApplyPatchParams {
136 pub patch: String,
138}
139
140fn default_tail_lines() -> usize {
141 DEFAULT_CHECK_PROCESS_TAIL_LINES
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_exec_params_deserialize() {
150 let json = r#"{"command": "echo hello"}"#;
151 let params: ExecParams = serde_json::from_str(json).unwrap();
152 assert_eq!(params.command, "echo hello");
153 assert!(!params.background);
154 assert!(params.timeout_ms.is_none());
155 assert!(params.log_path.is_none());
156 }
157
158 #[test]
159 fn test_exec_params_deserialize_background() {
160 let json = r#"{"command": "sleep 10", "background": true, "timeout_ms": 1000, "log_path": "/tmp/x.log"}"#;
161 let params: ExecParams = serde_json::from_str(json).unwrap();
162 assert_eq!(params.command, "sleep 10");
163 assert!(params.background);
164 assert_eq!(params.timeout_ms, Some(1000));
165 assert_eq!(params.log_path.as_deref(), Some("/tmp/x.log"));
166 }
167
168 #[test]
169 fn test_sudo_exec_params_deserialize() {
170 let json = r#"{"command": "apt update"}"#;
171 let params: SudoExecParams = serde_json::from_str(json).unwrap();
172 assert_eq!(params.command, "apt update");
173 assert!(!params.background);
174 assert!(params.timeout_ms.is_none());
175 assert!(params.log_path.is_none());
176 }
177
178 #[test]
179 fn test_check_process_params_deserialize() {
180 let json = r#"{"job_id": "job-123"}"#;
181 let params: CheckProcessParams = serde_json::from_str(json).unwrap();
182 assert_eq!(params.job_id, "job-123");
183 assert_eq!(params.tail_lines, 50);
184 }
185
186 #[test]
187 fn test_check_process_params_with_tail_lines() {
188 let json = r#"{"job_id": "job-123", "tail_lines": 100}"#;
189 let params: CheckProcessParams = serde_json::from_str(json).unwrap();
190 assert_eq!(params.job_id, "job-123");
191 assert_eq!(params.tail_lines, 100);
192 }
193
194 #[test]
195 fn test_read_file_params_deserialize() {
196 let json = r#"{"remote_path": "/etc/hosts"}"#;
197 let params: ReadFileParams = serde_json::from_str(json).unwrap();
198 assert_eq!(params.remote_path, "/etc/hosts");
199 assert_eq!(params.mode, ReadFileMode::Preview);
200 assert_eq!(params.lines, None);
201 assert!(params.timeout_ms.is_none());
202 }
203
204 #[test]
205 fn test_read_file_params_deserialize_with_timeout() {
206 let json = r#"{"remote_path": "/etc/hosts", "timeout_ms": 2500}"#;
207 let params: ReadFileParams = serde_json::from_str(json).unwrap();
208 assert_eq!(params.remote_path, "/etc/hosts");
209 assert_eq!(params.mode, ReadFileMode::Preview);
210 assert_eq!(params.lines, None);
211 assert_eq!(params.timeout_ms, Some(2500));
212 }
213
214 #[test]
215 fn test_read_file_params_deserialize_with_mode_and_lines() {
216 let json = r#"{"remote_path":"/etc/hosts","mode":"tail","lines":120}"#;
217 let params: ReadFileParams = serde_json::from_str(json).unwrap();
218 assert_eq!(params.remote_path, "/etc/hosts");
219 assert_eq!(params.mode, ReadFileMode::Tail);
220 assert_eq!(params.lines, Some(120));
221 assert!(params.timeout_ms.is_none());
222 }
223
224 #[test]
225 fn test_read_file_mode_serialization_is_lowercase() {
226 let value = serde_json::to_value(ReadFileMode::Full).unwrap();
227 assert_eq!(value, serde_json::json!("full"));
228 }
229
230 #[test]
231 fn test_apply_patch_params_deserialize_and_reject_unknown_fields() {
232 let json = r#"{"patch":"*** Begin Patch\n*** Delete File: /tmp/old\n*** End Patch"}"#;
233 let params: ApplyPatchParams = serde_json::from_str(json).unwrap();
234 assert!(params.patch.contains("*** Delete File"));
235
236 let err =
237 serde_json::from_str::<ApplyPatchParams>(r#"{"patch":"x","remote_path":"/tmp/x"}"#)
238 .unwrap_err();
239 assert!(err.to_string().contains("unknown field `remote_path`"));
240 }
241}