use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub(crate) const DEFAULT_CHECK_PROCESS_TAIL_LINES: usize = 50;
fn default_read_file_mode() -> ReadFileMode {
ReadFileMode::Preview
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ExecParams {
pub command: String,
#[serde(default)]
pub background: bool,
pub timeout_ms: Option<u64>,
pub log_path: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SudoExecParams {
pub command: String,
#[serde(default)]
pub background: bool,
pub timeout_ms: Option<u64>,
pub log_path: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CheckProcessParams {
pub job_id: String,
#[serde(default = "default_tail_lines")]
pub tail_lines: usize,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ReadFileMode {
Preview,
Head,
Tail,
Full,
}
impl ReadFileMode {
pub const fn as_str(self) -> &'static str {
match self {
Self::Preview => "preview",
Self::Head => "head",
Self::Tail => "tail",
Self::Full => "full",
}
}
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadFileParams {
pub remote_path: String,
#[serde(default = "default_read_file_mode")]
pub mode: ReadFileMode,
pub lines: Option<usize>,
pub timeout_ms: Option<u64>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WriteFileParams {
pub remote_path: String,
pub new_content: String,
pub expected_sha256: Option<String>,
pub read_ticket: Option<String>,
pub dry_run: Option<bool>,
pub timeout_ms: Option<u64>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReplaceInFileParams {
pub remote_path: String,
pub old_text: String,
pub new_text: String,
pub scope_text: Option<String>,
pub replace_all: Option<bool>,
pub match_index: Option<usize>,
pub dry_run: Option<bool>,
pub expected_sha256: Option<String>,
pub timeout_ms: Option<u64>,
}
fn default_tail_lines() -> usize {
DEFAULT_CHECK_PROCESS_TAIL_LINES
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exec_params_deserialize() {
let json = r#"{"command": "echo hello"}"#;
let params: ExecParams = serde_json::from_str(json).unwrap();
assert_eq!(params.command, "echo hello");
assert!(!params.background);
assert!(params.timeout_ms.is_none());
assert!(params.log_path.is_none());
}
#[test]
fn test_exec_params_deserialize_background() {
let json = r#"{"command": "sleep 10", "background": true, "timeout_ms": 1000, "log_path": "/tmp/x.log"}"#;
let params: ExecParams = serde_json::from_str(json).unwrap();
assert_eq!(params.command, "sleep 10");
assert!(params.background);
assert_eq!(params.timeout_ms, Some(1000));
assert_eq!(params.log_path.as_deref(), Some("/tmp/x.log"));
}
#[test]
fn test_sudo_exec_params_deserialize() {
let json = r#"{"command": "apt update"}"#;
let params: SudoExecParams = serde_json::from_str(json).unwrap();
assert_eq!(params.command, "apt update");
assert!(!params.background);
assert!(params.timeout_ms.is_none());
assert!(params.log_path.is_none());
}
#[test]
fn test_check_process_params_deserialize() {
let json = r#"{"job_id": "job-123"}"#;
let params: CheckProcessParams = serde_json::from_str(json).unwrap();
assert_eq!(params.job_id, "job-123");
assert_eq!(params.tail_lines, 50);
}
#[test]
fn test_check_process_params_with_tail_lines() {
let json = r#"{"job_id": "job-123", "tail_lines": 100}"#;
let params: CheckProcessParams = serde_json::from_str(json).unwrap();
assert_eq!(params.job_id, "job-123");
assert_eq!(params.tail_lines, 100);
}
#[test]
fn test_read_file_params_deserialize() {
let json = r#"{"remote_path": "/etc/hosts"}"#;
let params: ReadFileParams = serde_json::from_str(json).unwrap();
assert_eq!(params.remote_path, "/etc/hosts");
assert_eq!(params.mode, ReadFileMode::Preview);
assert_eq!(params.lines, None);
assert!(params.timeout_ms.is_none());
}
#[test]
fn test_read_file_params_deserialize_with_timeout() {
let json = r#"{"remote_path": "/etc/hosts", "timeout_ms": 2500}"#;
let params: ReadFileParams = serde_json::from_str(json).unwrap();
assert_eq!(params.remote_path, "/etc/hosts");
assert_eq!(params.mode, ReadFileMode::Preview);
assert_eq!(params.lines, None);
assert_eq!(params.timeout_ms, Some(2500));
}
#[test]
fn test_read_file_params_deserialize_with_mode_and_lines() {
let json = r#"{"remote_path":"/etc/hosts","mode":"tail","lines":120}"#;
let params: ReadFileParams = serde_json::from_str(json).unwrap();
assert_eq!(params.remote_path, "/etc/hosts");
assert_eq!(params.mode, ReadFileMode::Tail);
assert_eq!(params.lines, Some(120));
assert!(params.timeout_ms.is_none());
}
#[test]
fn test_read_file_mode_serialization_is_lowercase() {
let value = serde_json::to_value(ReadFileMode::Full).unwrap();
assert_eq!(value, serde_json::json!("full"));
}
#[test]
fn test_write_file_params_deserialize() {
let json = r#"{"remote_path":"/etc/hosts","new_content":"127.0.0.1 localhost\n"}"#;
let params: WriteFileParams = serde_json::from_str(json).unwrap();
assert_eq!(params.remote_path, "/etc/hosts");
assert_eq!(params.new_content, "127.0.0.1 localhost\n");
assert!(params.expected_sha256.is_none());
assert!(params.read_ticket.is_none());
assert!(params.dry_run.is_none());
assert!(params.timeout_ms.is_none());
}
#[test]
fn test_write_file_params_deserialize_with_expected_hash_and_timeout() {
let json = r#"{"remote_path":"/etc/hosts","new_content":"x","expected_sha256":"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff","timeout_ms":4000}"#;
let params: WriteFileParams = serde_json::from_str(json).unwrap();
assert_eq!(params.remote_path, "/etc/hosts");
assert_eq!(params.new_content, "x");
assert_eq!(
params.expected_sha256.as_deref(),
Some("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
);
assert!(params.read_ticket.is_none());
assert!(params.dry_run.is_none());
assert_eq!(params.timeout_ms, Some(4000));
}
#[test]
fn test_replace_in_file_params_deserialize_defaults_replace_all() {
let json = r#"{"remote_path":"/etc/hosts","old_text":"127.0.0.1","new_text":"127.0.0.2"}"#;
let params: ReplaceInFileParams = serde_json::from_str(json).unwrap();
assert_eq!(params.remote_path, "/etc/hosts");
assert_eq!(params.old_text, "127.0.0.1");
assert_eq!(params.new_text, "127.0.0.2");
assert!(params.scope_text.is_none());
assert!(params.replace_all.is_none());
assert!(params.match_index.is_none());
assert!(params.dry_run.is_none());
assert!(params.expected_sha256.is_none());
assert!(params.timeout_ms.is_none());
}
#[test]
fn test_replace_in_file_params_deserialize_replace_all_true() {
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}"#;
let params: ReplaceInFileParams = serde_json::from_str(json).unwrap();
assert_eq!(params.remote_path, "/etc/hosts");
assert_eq!(params.old_text, "x");
assert_eq!(params.new_text, "y");
assert_eq!(params.scope_text.as_deref(), Some("block"));
assert_eq!(params.replace_all, Some(true));
assert_eq!(params.match_index, Some(3));
assert_eq!(params.dry_run, Some(true));
assert_eq!(params.timeout_ms, Some(2000));
}
#[test]
fn test_write_file_params_reject_unknown_fields() {
let json = r#"{"remote_path":"/etc/hosts","new_content":"x","old_text":"y"}"#;
let err = serde_json::from_str::<WriteFileParams>(json).unwrap_err();
assert!(err.to_string().contains("unknown field `old_text`"));
}
#[test]
fn test_replace_in_file_params_reject_unknown_fields() {
let json =
r#"{"remote_path":"/etc/hosts","old_text":"x","new_text":"y","read_ticket":"rt1.x"}"#;
let err = serde_json::from_str::<ReplaceInFileParams>(json).unwrap_err();
assert!(err.to_string().contains("unknown field `read_ticket`"));
}
}