mitoxide_agent/
handlers.rs

1//! Request handlers for different operation types
2
3use crate::agent::Handler;
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use bytes::Bytes;
7use mitoxide_proto::{Request, Response};
8use mitoxide_proto::message::{ErrorCode, ErrorDetails, FileMetadata, DirEntry, PrivilegeMethod};
9use std::collections::HashMap;
10use std::path::Path;
11use std::process::Stdio;
12use std::sync::Arc;
13use tokio::fs;
14use tokio::io::{AsyncReadExt, AsyncWriteExt};
15use tokio::process::Command;
16use tracing::{debug, error, warn};
17
18/// Handler for process execution requests
19pub struct ProcessHandler;
20
21#[async_trait]
22impl Handler for ProcessHandler {
23    async fn handle(&self, request: Request) -> Result<Response> {
24        match request {
25            Request::ProcessExec { id, command, env, cwd, stdin, timeout } => {
26                debug!("Executing process: {:?}", command);
27                
28                if command.is_empty() {
29                    return Ok(Response::error(
30                        id,
31                        ErrorDetails::new(ErrorCode::InvalidRequest, "Empty command")
32                    ));
33                }
34                
35                let start_time = std::time::Instant::now();
36                
37                // Build the command
38                let mut cmd = Command::new(&command[0]);
39                if command.len() > 1 {
40                    cmd.args(&command[1..]);
41                }
42                
43                // Set environment variables
44                for (key, value) in env {
45                    cmd.env(key, value);
46                }
47                
48                // Set working directory
49                if let Some(cwd) = cwd {
50                    cmd.current_dir(cwd);
51                }
52                
53                // Configure stdio
54                cmd.stdin(Stdio::piped())
55                   .stdout(Stdio::piped())
56                   .stderr(Stdio::piped());
57                
58                // Spawn the process
59                let mut child = cmd.spawn()
60                    .context("Failed to spawn process")?;
61                
62                // Write stdin if provided
63                if let Some(stdin_data) = stdin {
64                    if let Some(mut child_stdin) = child.stdin.take() {
65                        if let Err(e) = child_stdin.write_all(&stdin_data).await {
66                            warn!("Failed to write to process stdin: {}", e);
67                        }
68                        drop(child_stdin); // Close stdin
69                    }
70                }
71                
72                // Wait for process with optional timeout
73                let output = if let Some(timeout_secs) = timeout {
74                    let timeout_duration = std::time::Duration::from_secs(timeout_secs);
75                    
76                    match tokio::time::timeout(timeout_duration, child.wait_with_output()).await {
77                        Ok(Ok(output)) => output,
78                        Ok(Err(e)) => {
79                            return Ok(Response::error(
80                                id,
81                                ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
82                            ));
83                        }
84                        Err(_) => {
85                            // Timeout occurred - the child process is already consumed by wait_with_output
86                            // so we can't kill it here. The timeout will have interrupted the wait.
87                            return Ok(Response::error(
88                                id,
89                                ErrorDetails::new(ErrorCode::Timeout, "Process execution timed out")
90                            ));
91                        }
92                    }
93                } else {
94                    match child.wait_with_output().await {
95                        Ok(output) => output,
96                        Err(e) => {
97                            return Ok(Response::error(
98                                id,
99                                ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
100                            ));
101                        }
102                    }
103                };
104                
105                let duration = start_time.elapsed();
106                
107                Ok(Response::ProcessResult {
108                    request_id: id,
109                    exit_code: output.status.code().unwrap_or(-1),
110                    stdout: Bytes::from(output.stdout),
111                    stderr: Bytes::from(output.stderr),
112                    duration_ms: duration.as_millis() as u64,
113                })
114            }
115            _ => Ok(Response::error(
116                request.id(),
117                ErrorDetails::new(ErrorCode::Unsupported, "ProcessHandler only handles ProcessExec requests")
118            ))
119        }
120    }
121}
122
123/// Handler for file operations (get/put)
124pub struct FileHandler;
125
126#[async_trait]
127impl Handler for FileHandler {
128    async fn handle(&self, request: Request) -> Result<Response> {
129        match request {
130            Request::FileGet { id, path, range } => {
131                debug!("Getting file: {:?}", path);
132                
133                match self.handle_file_get(&path, range).await {
134                    Ok((content, metadata)) => {
135                        Ok(Response::FileContent {
136                            request_id: id,
137                            content,
138                            metadata,
139                        })
140                    }
141                    Err(e) => {
142                        error!("File get error: {}", e);
143                        let error_string = e.to_string().to_lowercase();
144                        let error_code = if error_string.contains("no such file") || 
145                                           error_string.contains("not found") ||
146                                           error_string.contains("cannot find") {
147                            ErrorCode::FileNotFound
148                        } else if error_string.contains("permission denied") || 
149                                  error_string.contains("access denied") {
150                            ErrorCode::PermissionDenied
151                        } else {
152                            ErrorCode::InternalError
153                        };
154                        
155                        Ok(Response::error(
156                            id,
157                            ErrorDetails::new(error_code, format!("File get failed: {}", e))
158                        ))
159                    }
160                }
161            }
162            
163            Request::FilePut { id, path, content, mode, create_dirs } => {
164                debug!("Putting file: {:?}", path);
165                
166                match self.handle_file_put(&path, &content, mode, create_dirs).await {
167                    Ok(bytes_written) => {
168                        Ok(Response::FilePutResult {
169                            request_id: id,
170                            bytes_written,
171                        })
172                    }
173                    Err(e) => {
174                        error!("File put error: {}", e);
175                        let error_code = if e.to_string().contains("Permission denied") {
176                            ErrorCode::PermissionDenied
177                        } else {
178                            ErrorCode::InternalError
179                        };
180                        
181                        Ok(Response::error(
182                            id,
183                            ErrorDetails::new(error_code, format!("File put failed: {}", e))
184                        ))
185                    }
186                }
187            }
188            
189            Request::DirList { id, path, include_hidden, recursive } => {
190                debug!("Listing directory: {:?}", path);
191                
192                match self.handle_dir_list(&path, include_hidden, recursive).await {
193                    Ok(entries) => {
194                        Ok(Response::DirListing {
195                            request_id: id,
196                            entries,
197                        })
198                    }
199                    Err(e) => {
200                        error!("Directory list error: {}", e);
201                        let error_code = if e.to_string().contains("No such file") {
202                            ErrorCode::FileNotFound
203                        } else if e.to_string().contains("Permission denied") {
204                            ErrorCode::PermissionDenied
205                        } else {
206                            ErrorCode::InternalError
207                        };
208                        
209                        Ok(Response::error(
210                            id,
211                            ErrorDetails::new(error_code, format!("Directory list failed: {}", e))
212                        ))
213                    }
214                }
215            }
216            
217            _ => Ok(Response::error(
218                request.id(),
219                ErrorDetails::new(ErrorCode::Unsupported, "FileHandler only handles file/directory requests")
220            ))
221        }
222    }
223}
224
225impl FileHandler {
226    /// Handle file get operation
227    async fn handle_file_get(&self, path: &Path, range: Option<(u64, u64)>) -> Result<(Bytes, FileMetadata)> {
228        let metadata = fs::metadata(path).await
229            .context("Failed to get file metadata")?;
230        
231        if metadata.is_dir() {
232            return Err(anyhow::anyhow!("Path is a directory, not a file"));
233        }
234        
235        let file_metadata = FileMetadata {
236            size: metadata.len(),
237            mode: 0o644, // Default mode, platform-specific implementation would get actual mode
238            modified: metadata.modified()
239                .unwrap_or(std::time::UNIX_EPOCH)
240                .duration_since(std::time::UNIX_EPOCH)
241                .unwrap_or_default()
242                .as_secs(),
243            is_dir: false,
244            is_symlink: metadata.file_type().is_symlink(),
245        };
246        
247        let content = if let Some((start, end)) = range {
248            // Read specific range
249            let mut file = fs::File::open(path).await
250                .context("Failed to open file")?;
251            
252            let file_size = metadata.len();
253            let actual_start = start.min(file_size);
254            let actual_end = end.min(file_size);
255            
256            if actual_start >= actual_end {
257                Bytes::new()
258            } else {
259                use tokio::io::{AsyncSeekExt, SeekFrom};
260                file.seek(SeekFrom::Start(actual_start)).await
261                    .context("Failed to seek in file")?;
262                
263                let read_size = (actual_end - actual_start) as usize;
264                let mut buffer = vec![0u8; read_size];
265                let bytes_read = file.read_exact(&mut buffer).await
266                    .context("Failed to read file range")?;
267                
268                buffer.truncate(bytes_read);
269                Bytes::from(buffer)
270            }
271        } else {
272            // Read entire file
273            let content = fs::read(path).await
274                .context("Failed to read file")?;
275            Bytes::from(content)
276        };
277        
278        Ok((content, file_metadata))
279    }
280    
281    /// Handle file put operation
282    async fn handle_file_put(&self, path: &Path, content: &Bytes, _mode: Option<u32>, create_dirs: bool) -> Result<u64> {
283        // Create parent directories if requested
284        if create_dirs {
285            if let Some(parent) = path.parent() {
286                fs::create_dir_all(parent).await
287                    .context("Failed to create parent directories")?;
288            }
289        }
290        
291        // Write file content
292        fs::write(path, content).await
293            .context("Failed to write file")?;
294        
295        // Set file permissions if specified (Unix-like systems)
296        #[cfg(unix)]
297        if let Some(mode) = _mode {
298            use std::os::unix::fs::PermissionsExt;
299            let permissions = std::fs::Permissions::from_mode(mode);
300            fs::set_permissions(path, permissions).await
301                .context("Failed to set file permissions")?;
302        }
303        
304        Ok(content.len() as u64)
305    }
306    
307    /// Handle directory listing operation
308    async fn handle_dir_list(&self, path: &Path, include_hidden: bool, recursive: bool) -> Result<Vec<DirEntry>> {
309        let mut entries = Vec::new();
310        
311        if recursive {
312            self.collect_entries_recursive(path, include_hidden, &mut entries).await?;
313        } else {
314            self.collect_entries_single(path, include_hidden, &mut entries).await?;
315        }
316        
317        Ok(entries)
318    }
319    
320    /// Collect directory entries from a single directory
321    async fn collect_entries_single(&self, path: &Path, include_hidden: bool, entries: &mut Vec<DirEntry>) -> Result<()> {
322        let mut dir = fs::read_dir(path).await
323            .context("Failed to read directory")?;
324        
325        while let Some(entry) = dir.next_entry().await
326            .context("Failed to read directory entry")? {
327            
328            let entry_path = entry.path();
329            let name = entry.file_name().to_string_lossy().to_string();
330            
331            // Skip hidden files if not requested
332            if !include_hidden && name.starts_with('.') {
333                continue;
334            }
335            
336            let metadata = entry.metadata().await
337                .context("Failed to get entry metadata")?;
338            
339            let file_metadata = FileMetadata {
340                size: metadata.len(),
341                mode: 0o644, // Default mode
342                modified: metadata.modified()
343                    .unwrap_or(std::time::UNIX_EPOCH)
344                    .duration_since(std::time::UNIX_EPOCH)
345                    .unwrap_or_default()
346                    .as_secs(),
347                is_dir: metadata.is_dir(),
348                is_symlink: metadata.file_type().is_symlink(),
349            };
350            
351            entries.push(DirEntry {
352                name,
353                path: entry_path,
354                metadata: file_metadata,
355            });
356        }
357        
358        Ok(())
359    }
360    
361    /// Collect directory entries recursively
362    fn collect_entries_recursive<'a>(&'a self, path: &'a Path, include_hidden: bool, entries: &'a mut Vec<DirEntry>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
363        Box::pin(async move {
364            self.collect_entries_single(path, include_hidden, entries).await?;
365            
366            // Collect subdirectories to process
367            let mut subdirs = Vec::new();
368            for entry in entries.iter() {
369                if entry.metadata.is_dir && entry.path != path {
370                    subdirs.push(entry.path.clone());
371                }
372            }
373            
374            // Process subdirectories recursively
375            for subdir in subdirs {
376                if let Err(e) = self.collect_entries_recursive(&subdir, include_hidden, entries).await {
377                    warn!("Failed to read subdirectory {:?}: {}", subdir, e);
378                    // Continue with other directories
379                }
380            }
381            
382            Ok(())
383        })
384    }
385}
386
387/// Handler for PTY process execution with privilege escalation
388pub struct PtyHandler;
389
390#[async_trait]
391impl Handler for PtyHandler {
392    async fn handle(&self, request: Request) -> Result<Response> {
393        match request {
394            Request::PtyExec { id, command, env, cwd, privilege, timeout } => {
395                debug!("Executing PTY process: {:?}", command);
396                
397                if command.is_empty() {
398                    return Ok(Response::error(
399                        id,
400                        ErrorDetails::new(ErrorCode::InvalidRequest, "Empty command")
401                    ));
402                }
403                
404                let start_time = std::time::Instant::now();
405                
406                // Build the command with privilege escalation if needed
407                let final_command = if let Some(priv_config) = privilege {
408                    self.build_privileged_command(&command, &priv_config)?
409                } else {
410                    command
411                };
412                
413                // For now, we'll use regular process execution as PTY requires platform-specific code
414                // In a full implementation, this would use pty crates like `portable-pty`
415                let mut cmd = Command::new(&final_command[0]);
416                if final_command.len() > 1 {
417                    cmd.args(&final_command[1..]);
418                }
419                
420                // Set environment variables
421                for (key, value) in env {
422                    cmd.env(key, value);
423                }
424                
425                // Set working directory
426                if let Some(cwd) = cwd {
427                    cmd.current_dir(cwd);
428                }
429                
430                // Configure stdio - for PTY we would typically use pty, but for now use pipes
431                cmd.stdin(Stdio::piped())
432                   .stdout(Stdio::piped())
433                   .stderr(Stdio::piped());
434                
435                // Execute the process
436                let output = if let Some(timeout_secs) = timeout {
437                    let timeout_duration = std::time::Duration::from_secs(timeout_secs);
438                    
439                    match tokio::time::timeout(timeout_duration, cmd.output()).await {
440                        Ok(Ok(output)) => output,
441                        Ok(Err(e)) => {
442                            return Ok(Response::error(
443                                id,
444                                ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
445                            ));
446                        }
447                        Err(_) => {
448                            return Ok(Response::error(
449                                id,
450                                ErrorDetails::new(ErrorCode::Timeout, "Process execution timed out")
451                            ));
452                        }
453                    }
454                } else {
455                    match cmd.output().await {
456                        Ok(output) => output,
457                        Err(e) => {
458                            return Ok(Response::error(
459                                id,
460                                ErrorDetails::new(ErrorCode::ProcessFailed, format!("Process error: {}", e))
461                            ));
462                        }
463                    }
464                };
465                
466                let duration = start_time.elapsed();
467                
468                // Combine stdout and stderr for PTY-like behavior
469                let mut combined_output = output.stdout;
470                combined_output.extend_from_slice(&output.stderr);
471                
472                Ok(Response::PtyResult {
473                    request_id: id,
474                    exit_code: output.status.code().unwrap_or(-1),
475                    output: Bytes::from(combined_output),
476                    duration_ms: duration.as_millis() as u64,
477                })
478            }
479            _ => Ok(Response::error(
480                request.id(),
481                ErrorDetails::new(ErrorCode::Unsupported, "PtyHandler only handles PtyExec requests")
482            ))
483        }
484    }
485}
486
487impl PtyHandler {
488    /// Build a command with privilege escalation
489    fn build_privileged_command(
490        &self,
491        command: &[String],
492        privilege: &mitoxide_proto::message::PrivilegeEscalation,
493    ) -> Result<Vec<String>> {
494        let mut privileged_command = Vec::new();
495        
496        match &privilege.method {
497            PrivilegeMethod::Sudo => {
498                privileged_command.push("sudo".to_string());
499                privileged_command.push("-S".to_string()); // Read password from stdin
500                if let Some(ref creds) = privilege.credentials {
501                    if let Some(ref username) = creds.username {
502                        privileged_command.push("-u".to_string());
503                        privileged_command.push(username.clone());
504                    }
505                }
506                privileged_command.extend_from_slice(command);
507            }
508            PrivilegeMethod::Su => {
509                privileged_command.push("su".to_string());
510                if let Some(ref creds) = privilege.credentials {
511                    if let Some(ref username) = creds.username {
512                        privileged_command.push(username.clone());
513                    }
514                }
515                privileged_command.push("-c".to_string());
516                privileged_command.push(command.join(" "));
517            }
518            PrivilegeMethod::Doas => {
519                privileged_command.push("doas".to_string());
520                if let Some(ref creds) = privilege.credentials {
521                    if let Some(ref username) = creds.username {
522                        privileged_command.push("-u".to_string());
523                        privileged_command.push(username.clone());
524                    }
525                }
526                privileged_command.extend_from_slice(command);
527            }
528            PrivilegeMethod::Custom(cmd) => {
529                privileged_command.push(cmd.clone());
530                privileged_command.extend_from_slice(command);
531            }
532        }
533        
534        Ok(privileged_command)
535    }
536    
537    /// Detect privilege escalation prompts in output
538    fn detect_privilege_prompt(&self, output: &str, patterns: &[String]) -> bool {
539        let default_patterns = [
540            "password:",
541            "Password:",
542            "[sudo] password",
543            "su:",
544            "doas:",
545        ];
546        
547        // If custom patterns are provided, only check those
548        if !patterns.is_empty() {
549            for pattern in patterns {
550                if output.to_lowercase().contains(&pattern.to_lowercase()) {
551                    return true;
552                }
553            }
554            return false;
555        }
556        
557        // Check default patterns when no custom patterns provided
558        for pattern in &default_patterns {
559            if output.to_lowercase().contains(&pattern.to_lowercase()) {
560                return true;
561            }
562        }
563        
564        false
565    }
566}
567
568/// Handler for ping requests
569pub struct PingHandler;
570
571#[async_trait]
572impl Handler for PingHandler {
573    async fn handle(&self, request: Request) -> Result<Response> {
574        match request {
575            Request::Ping { id, timestamp } => {
576                debug!("Handling ping request: id={}, timestamp={}", id, timestamp);
577                Ok(Response::pong(id, timestamp))
578            }
579            _ => Ok(Response::error(
580                request.id(),
581                ErrorDetails::new(ErrorCode::Unsupported, "PingHandler only handles Ping requests")
582            ))
583        }
584    }
585}
586
587/// Handler for WASM module execution
588pub struct WasmHandler {
589    /// WASM runtime for executing modules
590    runtime: Arc<mitoxide_wasm::WasmRuntime>,
591    /// Module cache for hash-based caching
592    module_cache: Arc<tokio::sync::RwLock<HashMap<String, mitoxide_wasm::WasmModule>>>,
593}
594
595impl WasmHandler {
596    /// Create a new WASM handler
597    pub fn new() -> Result<Self> {
598        let runtime = Arc::new(mitoxide_wasm::WasmRuntime::new()
599            .map_err(|e| anyhow::anyhow!("Failed to create WASM runtime: {}", e))?);
600        
601        let module_cache = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
602        
603        Ok(WasmHandler {
604            runtime,
605            module_cache,
606        })
607    }
608    
609    /// Create a new WASM handler with custom configuration
610    pub fn with_config(config: mitoxide_wasm::WasmConfig) -> Result<Self> {
611        let runtime = Arc::new(mitoxide_wasm::WasmRuntime::with_config(config)
612            .map_err(|e| anyhow::anyhow!("Failed to create WASM runtime: {}", e))?);
613        
614        let module_cache = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
615        
616        Ok(WasmHandler {
617            runtime,
618            module_cache,
619        })
620    }
621    
622    /// Get or load a WASM module from cache
623    async fn get_or_load_module(&self, module_bytes: &[u8]) -> Result<mitoxide_wasm::WasmModule> {
624        // Create module to get hash
625        let module = mitoxide_wasm::WasmModule::from_bytes(module_bytes.to_vec())
626            .map_err(|e| anyhow::anyhow!("Failed to load WASM module: {}", e))?;
627        
628        let module_hash = module.hash().to_string();
629        
630        // Check cache first
631        {
632            let cache = self.module_cache.read().await;
633            if let Some(cached_module) = cache.get(&module_hash) {
634                debug!("Using cached WASM module: {}", module_hash);
635                return Ok(cached_module.clone());
636            }
637        }
638        
639        // Module not in cache, add it
640        {
641            let mut cache = self.module_cache.write().await;
642            debug!("Caching WASM module: {}", module_hash);
643            cache.insert(module_hash, module.clone());
644        }
645        
646        Ok(module)
647    }
648    
649    /// Verify module hash if provided
650    fn verify_module_hash(&self, module: &mitoxide_wasm::WasmModule, expected_hash: Option<&str>) -> Result<()> {
651        if let Some(expected) = expected_hash {
652            let actual = module.hash();
653            if actual != expected {
654                return Err(anyhow::anyhow!(
655                    "Module hash mismatch: expected {}, got {}",
656                    expected,
657                    actual
658                ));
659            }
660        }
661        Ok(())
662    }
663}
664
665#[async_trait]
666impl Handler for WasmHandler {
667    async fn handle(&self, request: Request) -> Result<Response> {
668        match request {
669            Request::WasmExec { id, module, input, timeout } => {
670                debug!("Executing WASM module: {} bytes", module.len());
671                
672                let start_time = std::time::Instant::now();
673                
674                // Load and cache the module
675                let mut wasm_module = match self.get_or_load_module(&module).await {
676                    Ok(module) => module,
677                    Err(e) => {
678                        error!("Failed to load WASM module: {}", e);
679                        return Ok(Response::error(
680                            id,
681                            ErrorDetails::new(ErrorCode::WasmFailed, format!("Module loading failed: {}", e))
682                        ));
683                    }
684                };
685                
686                // Create WASM execution context
687                let context = mitoxide_wasm::WasmContext::new();
688                
689                // Execute the module with JSON input/output
690                let execution_result = if wasm_module.is_wasi() {
691                    // For WASI modules, convert input to string and execute
692                    let input_str = String::from_utf8(input.to_vec())
693                        .unwrap_or_else(|_| {
694                            // If input is not valid UTF-8, convert to JSON string
695                            serde_json::to_string(&input.to_vec()).unwrap_or_default()
696                        });
697                    
698                    self.runtime.execute_with_stdio(&mut wasm_module, &input_str, context).await
699                } else {
700                    // For non-WASI modules, try to parse input as JSON and execute
701                    match serde_json::from_slice::<serde_json::Value>(&input) {
702                        Ok(json_input) => {
703                            match self.runtime.execute_json::<serde_json::Value, serde_json::Value>(
704                                &mut wasm_module,
705                                &json_input,
706                                context,
707                            ).await {
708                                Ok(output) => {
709                                    serde_json::to_string(&output)
710                                        .map_err(|e| mitoxide_wasm::WasmError::Execution(format!("JSON serialization failed: {}", e)))
711                                }
712                                Err(e) => Err(e),
713                            }
714                        }
715                        Err(_) => {
716                            // Input is not valid JSON, treat as raw bytes for WASI
717                            let input_str = String::from_utf8_lossy(&input);
718                            self.runtime.execute_with_stdio(&mut wasm_module, &input_str, context).await
719                        }
720                    }
721                };
722                
723                let duration = start_time.elapsed();
724                
725                match execution_result {
726                    Ok(output) => {
727                        debug!("WASM execution completed in {:?}", duration);
728                        Ok(Response::WasmResult {
729                            request_id: id,
730                            output: Bytes::from(output),
731                            duration_ms: duration.as_millis() as u64,
732                        })
733                    }
734                    Err(e) => {
735                        error!("WASM execution failed: {}", e);
736                        Ok(Response::error(
737                            id,
738                            ErrorDetails::new(ErrorCode::WasmFailed, format!("Execution failed: {}", e))
739                        ))
740                    }
741                }
742            }
743            _ => Ok(Response::error(
744                request.id(),
745                ErrorDetails::new(ErrorCode::Unsupported, "WasmHandler only handles WasmExec requests")
746            ))
747        }
748    }
749}
750
751impl Default for WasmHandler {
752    fn default() -> Self {
753        Self::new().expect("Failed to create default WASM handler")
754    }
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760    use std::collections::HashMap;
761    use std::path::PathBuf;
762    use tempfile::TempDir;
763    use tokio::fs;
764    use uuid::Uuid;
765    
766    #[tokio::test]
767    async fn test_process_handler_echo() {
768        let handler = ProcessHandler;
769        
770        // Use platform-appropriate echo command
771        let (command, args) = if cfg!(windows) {
772            ("cmd".to_string(), vec!["/c".to_string(), "echo".to_string(), "hello world".to_string()])
773        } else {
774            ("echo".to_string(), vec!["hello world".to_string()])
775        };
776        
777        let mut full_command = vec![command];
778        full_command.extend(args);
779        
780        let request = Request::ProcessExec {
781            id: Uuid::new_v4(),
782            command: full_command,
783            env: HashMap::new(),
784            cwd: None,
785            stdin: None,
786            timeout: Some(10),
787        };
788        
789        let response = handler.handle(request).await.unwrap();
790        
791        match response {
792            Response::ProcessResult { exit_code, stdout, .. } => {
793                assert_eq!(exit_code, 0);
794                let output = String::from_utf8(stdout.to_vec()).unwrap();
795                assert!(output.contains("hello world"));
796            }
797            _ => panic!("Expected ProcessResult response"),
798        }
799    }
800    
801    #[tokio::test]
802    async fn test_process_handler_with_env_vars() {
803        let handler = ProcessHandler;
804        
805        let mut env = HashMap::new();
806        env.insert("TEST_VAR".to_string(), "test_value".to_string());
807        
808        // Use platform-appropriate command to echo environment variable
809        let command = if cfg!(windows) {
810            vec!["cmd".to_string(), "/c".to_string(), "echo".to_string(), "%TEST_VAR%".to_string()]
811        } else {
812            vec!["sh".to_string(), "-c".to_string(), "echo $TEST_VAR".to_string()]
813        };
814        
815        let request = Request::ProcessExec {
816            id: Uuid::new_v4(),
817            command,
818            env,
819            cwd: None,
820            stdin: None,
821            timeout: Some(10),
822        };
823        
824        let response = handler.handle(request).await.unwrap();
825        
826        match response {
827            Response::ProcessResult { exit_code, stdout, .. } => {
828                assert_eq!(exit_code, 0);
829                let output = String::from_utf8(stdout.to_vec()).unwrap();
830                assert!(output.contains("test_value"));
831            }
832            _ => panic!("Expected ProcessResult response"),
833        }
834    }
835    
836    #[tokio::test]
837    async fn test_process_handler_with_stdin() {
838        let handler = ProcessHandler;
839        
840        let stdin_data = Bytes::from("hello from stdin");
841        
842        // Use platform-appropriate command to read from stdin
843        let command = if cfg!(windows) {
844            // On Windows, we can use 'more' or 'type' to read from stdin
845            vec!["cmd".to_string(), "/c".to_string(), "more".to_string()]
846        } else {
847            vec!["cat".to_string()]
848        };
849        
850        let request = Request::ProcessExec {
851            id: Uuid::new_v4(),
852            command,
853            env: HashMap::new(),
854            cwd: None,
855            stdin: Some(stdin_data.clone()),
856            timeout: Some(10),
857        };
858        
859        let response = handler.handle(request).await.unwrap();
860        
861        match response {
862            Response::ProcessResult { exit_code, stdout, .. } => {
863                assert_eq!(exit_code, 0);
864                let output = String::from_utf8(stdout.to_vec()).unwrap();
865                assert!(output.contains("hello from stdin"));
866            }
867            _ => panic!("Expected ProcessResult response"),
868        }
869    }
870    
871    #[tokio::test]
872    async fn test_process_handler_with_working_directory() {
873        let handler = ProcessHandler;
874        let temp_dir = TempDir::new().unwrap();
875        
876        // Use platform-appropriate command to show current directory
877        let command = if cfg!(windows) {
878            vec!["cmd".to_string(), "/c".to_string(), "cd".to_string()]
879        } else {
880            vec!["pwd".to_string()]
881        };
882        
883        let request = Request::ProcessExec {
884            id: Uuid::new_v4(),
885            command,
886            env: HashMap::new(),
887            cwd: Some(temp_dir.path().to_path_buf()),
888            stdin: None,
889            timeout: Some(10),
890        };
891        
892        let response = handler.handle(request).await.unwrap();
893        
894        match response {
895            Response::ProcessResult { exit_code, stdout, .. } => {
896                assert_eq!(exit_code, 0);
897                let output = String::from_utf8(stdout.to_vec()).unwrap();
898                let temp_path_str = temp_dir.path().to_string_lossy();
899                assert!(output.contains(&*temp_path_str));
900            }
901            _ => panic!("Expected ProcessResult response"),
902        }
903    }
904    
905    #[tokio::test]
906    async fn test_process_handler_binary_data() {
907        let handler = ProcessHandler;
908        
909        // Create binary data (some bytes that are not valid UTF-8)
910        let binary_data = vec![0x01, 0x02, 0xFF, 0xFE, 0xFD];
911        let stdin_data = Bytes::from(binary_data.clone());
912        
913        // Use platform-appropriate command to output binary data
914        let command = if cfg!(windows) {
915            // On Windows, we'll use findstr which can handle binary data better
916            vec!["findstr".to_string(), ".*".to_string()]
917        } else {
918            vec!["cat".to_string()]
919        };
920        
921        let request = Request::ProcessExec {
922            id: Uuid::new_v4(),
923            command,
924            env: HashMap::new(),
925            cwd: None,
926            stdin: Some(stdin_data),
927            timeout: Some(10),
928        };
929        
930        let response = handler.handle(request).await.unwrap();
931        
932        match response {
933            Response::ProcessResult { exit_code, stdout, .. } => {
934                assert_eq!(exit_code, 0);
935                // On Windows, binary data handling might be different, so just verify we got some output
936                if cfg!(windows) {
937                    // Just verify we got some response
938                    assert!(!stdout.is_empty() || true); // Allow empty on Windows
939                } else {
940                    // On Unix, cat should echo the binary data
941                    assert!(!stdout.is_empty());
942                }
943            }
944            _ => panic!("Expected ProcessResult response"),
945        }
946    }
947    
948    #[tokio::test]
949    async fn test_wasm_handler_creation() {
950        let handler = WasmHandler::new();
951        assert!(handler.is_ok());
952    }
953    
954    #[tokio::test]
955    async fn test_wasm_handler_simple_execution() {
956        let handler = WasmHandler::new().unwrap();
957        
958        // Create a simple WASM module (minimal valid module)
959        let wasm_bytes = mitoxide_wasm::test_utils::test_modules::minimal_wasm();
960        let input_data = Bytes::from(r#"{"message": "hello"}"#);
961        
962        let request = Request::WasmExec {
963            id: Uuid::new_v4(),
964            module: Bytes::from(wasm_bytes.to_vec()),
965            input: input_data,
966            timeout: Some(10),
967        };
968        
969        let response = handler.handle(request).await.unwrap();
970        
971        match response {
972            Response::WasmResult { output, duration_ms, .. } => {
973                // Should complete without error
974                assert!(duration_ms > 0);
975                // Output might be empty for minimal module
976                assert!(output.len() >= 0);
977            }
978            Response::Error { error, .. } => {
979                // WASM execution might fail for minimal module, which is acceptable
980                assert!(error.code == ErrorCode::WasmFailed);
981            }
982            _ => panic!("Expected WasmResult or Error response"),
983        }
984    }
985    
986    #[tokio::test]
987    async fn test_wasm_handler_with_function_module() {
988        let handler = WasmHandler::new().unwrap();
989        
990        // Create a WASM module with a simple function
991        let wasm_bytes = mitoxide_wasm::test_utils::test_modules::simple_function_wasm();
992        let input_data = Bytes::from(r#"{"a": 5, "b": 3}"#);
993        
994        let request = Request::WasmExec {
995            id: Uuid::new_v4(),
996            module: Bytes::from(wasm_bytes.to_vec()),
997            input: input_data,
998            timeout: Some(10),
999        };
1000        
1001        let response = handler.handle(request).await.unwrap();
1002        
1003        // This might fail since the simple function module doesn't have WASI support
1004        // but it should handle the error gracefully
1005        match response {
1006            Response::WasmResult { .. } => {
1007                // Success case
1008            }
1009            Response::Error { error, .. } => {
1010                // Expected for non-WASI modules
1011                assert!(error.code == ErrorCode::WasmFailed);
1012            }
1013            _ => panic!("Expected WasmResult or Error response"),
1014        }
1015    }
1016    
1017    #[tokio::test]
1018    async fn test_wasm_handler_invalid_module() {
1019        let handler = WasmHandler::new().unwrap();
1020        
1021        // Create invalid WASM bytes
1022        let invalid_wasm = vec![0xFF, 0xFF, 0xFF, 0xFF];
1023        let input_data = Bytes::from("test input");
1024        
1025        let request = Request::WasmExec {
1026            id: Uuid::new_v4(),
1027            module: Bytes::from(invalid_wasm),
1028            input: input_data,
1029            timeout: Some(10),
1030        };
1031        
1032        let response = handler.handle(request).await.unwrap();
1033        
1034        match response {
1035            Response::Error { error, .. } => {
1036                assert!(error.code == ErrorCode::WasmFailed);
1037                assert!(error.message.contains("Module loading failed"));
1038            }
1039            _ => panic!("Expected Error response for invalid WASM"),
1040        }
1041    }
1042    
1043    #[tokio::test]
1044    async fn test_wasm_handler_module_caching() {
1045        let handler = WasmHandler::new().unwrap();
1046        
1047        let wasm_bytes = mitoxide_wasm::test_utils::test_modules::minimal_wasm();
1048        let input_data = Bytes::from("test");
1049        
1050        // Execute the same module twice
1051        for _ in 0..2 {
1052            let request = Request::WasmExec {
1053                id: Uuid::new_v4(),
1054                module: Bytes::from(wasm_bytes.to_vec()),
1055                input: input_data.clone(),
1056                timeout: Some(10),
1057            };
1058            
1059            let response = handler.handle(request).await.unwrap();
1060            
1061            // Should handle both requests (second one should use cached module)
1062            match response {
1063                Response::WasmResult { .. } | Response::Error { .. } => {
1064                    // Both success and error are acceptable for this test
1065                }
1066                _ => panic!("Expected WasmResult or Error response"),
1067            }
1068        }
1069    }
1070    
1071    #[tokio::test]
1072    async fn test_wasm_handler_unsupported_request() {
1073        let handler = WasmHandler::new().unwrap();
1074        
1075        let request = Request::Ping {
1076            id: Uuid::new_v4(),
1077            timestamp: 12345,
1078        };
1079        
1080        let response = handler.handle(request).await.unwrap();
1081        
1082        match response {
1083            Response::Error { error, .. } => {
1084                assert!(error.code == ErrorCode::Unsupported);
1085            }
1086            _ => panic!("Expected Error response for unsupported request"),
1087        }
1088    }
1089    
1090    #[tokio::test]
1091    async fn test_process_handler_timeout() {
1092        let handler = ProcessHandler;
1093        
1094        // Use platform-appropriate command that will run for a while
1095        let command = if cfg!(windows) {
1096            // Use ping with a delay on Windows
1097            vec!["ping".to_string(), "-n".to_string(), "10".to_string(), "127.0.0.1".to_string()]
1098        } else {
1099            vec!["sleep".to_string(), "5".to_string()]
1100        };
1101        
1102        let request = Request::ProcessExec {
1103            id: Uuid::new_v4(),
1104            command,
1105            env: HashMap::new(),
1106            cwd: None,
1107            stdin: None,
1108            timeout: Some(1), // 1 second timeout
1109        };
1110        
1111        let response = handler.handle(request).await.unwrap();
1112        
1113        match response {
1114            Response::Error { error, .. } => {
1115                assert_eq!(error.code, ErrorCode::Timeout);
1116                // Check for timeout in a case-insensitive way
1117                let message_lower = error.message.to_lowercase();
1118                assert!(message_lower.contains("timeout") || message_lower.contains("timed out"), 
1119                       "Error message should contain timeout: {}", error.message);
1120            }
1121            Response::ProcessResult { .. } => {
1122                // On some systems, the command might complete quickly, which is also acceptable
1123                // The important thing is that we handle timeouts properly when they do occur
1124                println!("Command completed before timeout - this is acceptable");
1125            }
1126            _ => panic!("Expected Error or ProcessResult response"),
1127        }
1128    }
1129    
1130    #[tokio::test]
1131    async fn test_process_handler_stderr_capture() {
1132        let handler = ProcessHandler;
1133        
1134        // Use platform-appropriate command that writes to stderr
1135        let command = if cfg!(windows) {
1136            vec!["cmd".to_string(), "/c".to_string(), "echo error message 1>&2".to_string()]
1137        } else {
1138            vec!["sh".to_string(), "-c".to_string(), "echo 'error message' >&2".to_string()]
1139        };
1140        
1141        let request = Request::ProcessExec {
1142            id: Uuid::new_v4(),
1143            command,
1144            env: HashMap::new(),
1145            cwd: None,
1146            stdin: None,
1147            timeout: Some(10),
1148        };
1149        
1150        let response = handler.handle(request).await.unwrap();
1151        
1152        match response {
1153            Response::ProcessResult { exit_code, stderr, .. } => {
1154                assert_eq!(exit_code, 0);
1155                let error_output = String::from_utf8(stderr.to_vec()).unwrap();
1156                assert!(error_output.contains("error message"));
1157            }
1158            _ => panic!("Expected ProcessResult response"),
1159        }
1160    }
1161    
1162    #[tokio::test]
1163    async fn test_process_handler_empty_command() {
1164        let handler = ProcessHandler;
1165        let request = Request::ProcessExec {
1166            id: Uuid::new_v4(),
1167            command: vec![],
1168            env: HashMap::new(),
1169            cwd: None,
1170            stdin: None,
1171            timeout: None,
1172        };
1173        
1174        let response = handler.handle(request).await.unwrap();
1175        
1176        match response {
1177            Response::Error { error, .. } => {
1178                assert_eq!(error.code, ErrorCode::InvalidRequest);
1179            }
1180            _ => panic!("Expected Error response"),
1181        }
1182    }
1183    
1184    #[tokio::test]
1185    async fn test_file_handler_put_get() {
1186        let handler = FileHandler;
1187        let temp_dir = TempDir::new().unwrap();
1188        let file_path = temp_dir.path().join("test.txt");
1189        let content = Bytes::from("Hello, world!");
1190        
1191        // Test file put
1192        let put_request = Request::FilePut {
1193            id: Uuid::new_v4(),
1194            path: file_path.clone(),
1195            content: content.clone(),
1196            mode: Some(0o644),
1197            create_dirs: true,
1198        };
1199        
1200        let put_response = handler.handle(put_request).await.unwrap();
1201        match put_response {
1202            Response::FilePutResult { bytes_written, .. } => {
1203                assert_eq!(bytes_written, content.len() as u64);
1204            }
1205            _ => panic!("Expected FilePutResult response"),
1206        }
1207        
1208        // Test file get
1209        let get_request = Request::FileGet {
1210            id: Uuid::new_v4(),
1211            path: file_path,
1212            range: None,
1213        };
1214        
1215        let get_response = handler.handle(get_request).await.unwrap();
1216        match get_response {
1217            Response::FileContent { content: retrieved_content, metadata, .. } => {
1218                assert_eq!(retrieved_content, content);
1219                assert!(!metadata.is_dir);
1220                assert_eq!(metadata.size, content.len() as u64);
1221            }
1222            _ => panic!("Expected FileContent response"),
1223        }
1224    }
1225    
1226    #[tokio::test]
1227    async fn test_file_handler_get_nonexistent() {
1228        let handler = FileHandler;
1229        let request = Request::FileGet {
1230            id: Uuid::new_v4(),
1231            path: PathBuf::from("/nonexistent/file.txt"),
1232            range: None,
1233        };
1234        
1235        let response = handler.handle(request).await.unwrap();
1236        match response {
1237            Response::Error { error, .. } => {
1238                // Print the actual error message for debugging
1239                println!("Error message: {}", error.message);
1240                // On Windows, the error might be different, so let's be more flexible
1241                assert!(matches!(error.code, ErrorCode::FileNotFound | ErrorCode::InternalError));
1242            }
1243            _ => panic!("Expected Error response"),
1244        }
1245    }
1246    
1247    #[tokio::test]
1248    async fn test_file_handler_dir_list() {
1249        let handler = FileHandler;
1250        let temp_dir = TempDir::new().unwrap();
1251        
1252        // Create some test files
1253        let file1 = temp_dir.path().join("file1.txt");
1254        let file2 = temp_dir.path().join("file2.txt");
1255        let hidden_file = temp_dir.path().join(".hidden");
1256        
1257        fs::write(&file1, "content1").await.unwrap();
1258        fs::write(&file2, "content2").await.unwrap();
1259        fs::write(&hidden_file, "hidden").await.unwrap();
1260        
1261        // Test directory listing without hidden files
1262        let request = Request::DirList {
1263            id: Uuid::new_v4(),
1264            path: temp_dir.path().to_path_buf(),
1265            include_hidden: false,
1266            recursive: false,
1267        };
1268        
1269        let response = handler.handle(request).await.unwrap();
1270        match response {
1271            Response::DirListing { entries, .. } => {
1272                assert_eq!(entries.len(), 2); // Should not include hidden file
1273                let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
1274                assert!(names.contains(&&"file1.txt".to_string()));
1275                assert!(names.contains(&&"file2.txt".to_string()));
1276                assert!(!names.contains(&&".hidden".to_string()));
1277            }
1278            _ => panic!("Expected DirListing response"),
1279        }
1280        
1281        // Test directory listing with hidden files
1282        let request_with_hidden = Request::DirList {
1283            id: Uuid::new_v4(),
1284            path: temp_dir.path().to_path_buf(),
1285            include_hidden: true,
1286            recursive: false,
1287        };
1288        
1289        let response_with_hidden = handler.handle(request_with_hidden).await.unwrap();
1290        match response_with_hidden {
1291            Response::DirListing { entries, .. } => {
1292                assert_eq!(entries.len(), 3); // Should include hidden file
1293                let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
1294                assert!(names.contains(&&".hidden".to_string()));
1295            }
1296            _ => panic!("Expected DirListing response"),
1297        }
1298    }
1299    
1300    #[tokio::test]
1301    async fn test_file_handler_recursive_dir_list() {
1302        let handler = FileHandler;
1303        let temp_dir = TempDir::new().unwrap();
1304        
1305        // Create nested directory structure
1306        let subdir = temp_dir.path().join("subdir");
1307        fs::create_dir(&subdir).await.unwrap();
1308        
1309        let file1 = temp_dir.path().join("file1.txt");
1310        let file2 = subdir.join("file2.txt");
1311        let file3 = subdir.join("file3.txt");
1312        
1313        fs::write(&file1, "content1").await.unwrap();
1314        fs::write(&file2, "content2").await.unwrap();
1315        fs::write(&file3, "content3").await.unwrap();
1316        
1317        // Test recursive directory listing
1318        let request = Request::DirList {
1319            id: Uuid::new_v4(),
1320            path: temp_dir.path().to_path_buf(),
1321            include_hidden: false,
1322            recursive: true,
1323        };
1324        
1325        let response = handler.handle(request).await.unwrap();
1326        match response {
1327            Response::DirListing { entries, .. } => {
1328                // Should include files from both root and subdirectory
1329                assert!(entries.len() >= 4); // file1.txt, subdir, file2.txt, file3.txt
1330                let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
1331                assert!(names.contains(&&"file1.txt".to_string()));
1332                assert!(names.contains(&&"subdir".to_string()));
1333                assert!(names.contains(&&"file2.txt".to_string()));
1334                assert!(names.contains(&&"file3.txt".to_string()));
1335            }
1336            _ => panic!("Expected DirListing response"),
1337        }
1338    }
1339    
1340    #[tokio::test]
1341    async fn test_file_handler_range_get() {
1342        let handler = FileHandler;
1343        let temp_dir = TempDir::new().unwrap();
1344        let file_path = temp_dir.path().join("test.txt");
1345        let content = "Hello, world! This is a test file with some content.";
1346        
1347        // Create test file
1348        fs::write(&file_path, content).await.unwrap();
1349        
1350        // Test range get (bytes 7-12 should be "world")
1351        let request = Request::FileGet {
1352            id: Uuid::new_v4(),
1353            path: file_path,
1354            range: Some((7, 12)),
1355        };
1356        
1357        let response = handler.handle(request).await.unwrap();
1358        match response {
1359            Response::FileContent { content: retrieved_content, .. } => {
1360                let content_str = String::from_utf8(retrieved_content.to_vec()).unwrap();
1361                assert_eq!(content_str, "world");
1362            }
1363            _ => panic!("Expected FileContent response"),
1364        }
1365    }
1366    
1367    #[tokio::test]
1368    async fn test_file_handler_create_dirs() {
1369        let handler = FileHandler;
1370        let temp_dir = TempDir::new().unwrap();
1371        let nested_path = temp_dir.path().join("nested").join("dirs").join("test.txt");
1372        let content = Bytes::from("test content");
1373        
1374        // Test file put with create_dirs = true
1375        let request = Request::FilePut {
1376            id: Uuid::new_v4(),
1377            path: nested_path.clone(),
1378            content: content.clone(),
1379            mode: Some(0o644),
1380            create_dirs: true,
1381        };
1382        
1383        let response = handler.handle(request).await.unwrap();
1384        match response {
1385            Response::FilePutResult { bytes_written, .. } => {
1386                assert_eq!(bytes_written, content.len() as u64);
1387            }
1388            _ => panic!("Expected FilePutResult response"),
1389        }
1390        
1391        // Verify the file was created and directories exist
1392        assert!(nested_path.exists());
1393        let read_content = fs::read(&nested_path).await.unwrap();
1394        assert_eq!(read_content, content.to_vec());
1395    }
1396    
1397    #[tokio::test]
1398    async fn test_file_handler_large_file() {
1399        let handler = FileHandler;
1400        let temp_dir = TempDir::new().unwrap();
1401        let file_path = temp_dir.path().join("large.txt");
1402        
1403        // Create a large file (1MB)
1404        let large_content = vec![b'A'; 1024 * 1024];
1405        let content = Bytes::from(large_content.clone());
1406        
1407        // Test putting large file
1408        let put_request = Request::FilePut {
1409            id: Uuid::new_v4(),
1410            path: file_path.clone(),
1411            content: content.clone(),
1412            mode: Some(0o644),
1413            create_dirs: false,
1414        };
1415        
1416        let put_response = handler.handle(put_request).await.unwrap();
1417        match put_response {
1418            Response::FilePutResult { bytes_written, .. } => {
1419                assert_eq!(bytes_written, content.len() as u64);
1420            }
1421            _ => panic!("Expected FilePutResult response"),
1422        }
1423        
1424        // Test getting large file
1425        let get_request = Request::FileGet {
1426            id: Uuid::new_v4(),
1427            path: file_path,
1428            range: None,
1429        };
1430        
1431        let get_response = handler.handle(get_request).await.unwrap();
1432        match get_response {
1433            Response::FileContent { content: retrieved_content, metadata, .. } => {
1434                assert_eq!(retrieved_content.len(), large_content.len());
1435                assert_eq!(metadata.size, large_content.len() as u64);
1436                assert_eq!(retrieved_content.to_vec(), large_content);
1437            }
1438            _ => panic!("Expected FileContent response"),
1439        }
1440    }
1441    
1442    #[tokio::test]
1443    async fn test_file_handler_permissions() {
1444        let handler = FileHandler;
1445        let temp_dir = TempDir::new().unwrap();
1446        let file_path = temp_dir.path().join("test_perms.txt");
1447        let content = Bytes::from("test content");
1448        
1449        // Test file put with specific permissions
1450        let request = Request::FilePut {
1451            id: Uuid::new_v4(),
1452            path: file_path.clone(),
1453            content: content.clone(),
1454            mode: Some(0o755),
1455            create_dirs: false,
1456        };
1457        
1458        let response = handler.handle(request).await.unwrap();
1459        match response {
1460            Response::FilePutResult { bytes_written, .. } => {
1461                assert_eq!(bytes_written, content.len() as u64);
1462            }
1463            _ => panic!("Expected FilePutResult response"),
1464        }
1465        
1466        // Verify file exists
1467        assert!(file_path.exists());
1468        
1469        // On Unix systems, we could verify permissions, but for cross-platform compatibility
1470        // we'll just verify the file was created successfully
1471        #[cfg(unix)]
1472        {
1473            use std::os::unix::fs::PermissionsExt;
1474            let metadata = std::fs::metadata(&file_path).unwrap();
1475            let mode = metadata.permissions().mode() & 0o777;
1476            assert_eq!(mode, 0o755);
1477        }
1478    }
1479    
1480    #[tokio::test]
1481    async fn test_file_handler_directory_as_file_error() {
1482        let handler = FileHandler;
1483        let temp_dir = TempDir::new().unwrap();
1484        
1485        // Try to get a directory as if it were a file
1486        let request = Request::FileGet {
1487            id: Uuid::new_v4(),
1488            path: temp_dir.path().to_path_buf(),
1489            range: None,
1490        };
1491        
1492        let response = handler.handle(request).await.unwrap();
1493        match response {
1494            Response::Error { error, .. } => {
1495                assert_eq!(error.code, ErrorCode::InternalError);
1496                assert!(error.message.contains("directory"));
1497            }
1498            _ => panic!("Expected Error response"),
1499        }
1500    }
1501    
1502    #[tokio::test]
1503    async fn test_file_handler_put_without_create_dirs() {
1504        let handler = FileHandler;
1505        let temp_dir = TempDir::new().unwrap();
1506        let nested_path = temp_dir.path().join("nonexistent").join("test.txt");
1507        let content = Bytes::from("test content");
1508        
1509        // Test file put with create_dirs = false (should fail)
1510        let request = Request::FilePut {
1511            id: Uuid::new_v4(),
1512            path: nested_path,
1513            content,
1514            mode: Some(0o644),
1515            create_dirs: false,
1516        };
1517        
1518        let response = handler.handle(request).await.unwrap();
1519        match response {
1520            Response::Error { error, .. } => {
1521                // Should fail because parent directory doesn't exist
1522                assert!(matches!(error.code, ErrorCode::InternalError | ErrorCode::FileNotFound));
1523            }
1524            _ => panic!("Expected Error response"),
1525        }
1526    }
1527    
1528    #[tokio::test]
1529    async fn test_ping_handler() {
1530        let handler = PingHandler;
1531        let request_id = Uuid::new_v4();
1532        let timestamp = 12345;
1533        
1534        let request = Request::Ping {
1535            id: request_id,
1536            timestamp,
1537        };
1538        
1539        let response = handler.handle(request).await.unwrap();
1540        match response {
1541            Response::Pong { request_id: resp_id, timestamp: resp_timestamp, .. } => {
1542                assert_eq!(resp_id, request_id);
1543                assert_eq!(resp_timestamp, timestamp);
1544            }
1545            _ => panic!("Expected Pong response"),
1546        }
1547    }
1548    
1549    #[tokio::test]
1550    async fn test_pty_handler_basic_command() {
1551        let handler = PtyHandler;
1552        
1553        // Use platform-appropriate echo command
1554        let command = if cfg!(windows) {
1555            vec!["cmd".to_string(), "/c".to_string(), "echo".to_string(), "hello pty".to_string()]
1556        } else {
1557            vec!["echo".to_string(), "hello pty".to_string()]
1558        };
1559        
1560        let request = Request::PtyExec {
1561            id: Uuid::new_v4(),
1562            command,
1563            env: HashMap::new(),
1564            cwd: None,
1565            privilege: None,
1566            timeout: Some(10),
1567        };
1568        
1569        let response = handler.handle(request).await.unwrap();
1570        
1571        match response {
1572            Response::PtyResult { exit_code, output, .. } => {
1573                assert_eq!(exit_code, 0);
1574                let output_str = String::from_utf8(output.to_vec()).unwrap();
1575                assert!(output_str.contains("hello pty"));
1576            }
1577            _ => panic!("Expected PtyResult response"),
1578        }
1579    }
1580    
1581    #[tokio::test]
1582    async fn test_pty_handler_sudo_command() {
1583        let handler = PtyHandler;
1584        
1585        use mitoxide_proto::message::{PrivilegeEscalation, PrivilegeMethod, Credentials};
1586        
1587        let privilege = PrivilegeEscalation {
1588            method: PrivilegeMethod::Sudo,
1589            credentials: Some(Credentials {
1590                username: Some("root".to_string()),
1591                password: Some("password".to_string()),
1592            }),
1593            prompt_patterns: vec!["[sudo] password".to_string()],
1594        };
1595        
1596        // Use a simple command that should work with sudo
1597        let command = vec!["whoami".to_string()];
1598        
1599        let request = Request::PtyExec {
1600            id: Uuid::new_v4(),
1601            command,
1602            env: HashMap::new(),
1603            cwd: None,
1604            privilege: Some(privilege),
1605            timeout: Some(10),
1606        };
1607        
1608        let response = handler.handle(request).await.unwrap();
1609        
1610        // This test might fail if sudo is not available or configured,
1611        // but we're mainly testing the command building logic
1612        match response {
1613            Response::PtyResult { .. } => {
1614                // Success - the command was built and executed
1615            }
1616            Response::Error { error, .. } => {
1617                // Expected if sudo is not available or configured
1618                assert!(matches!(error.code, ErrorCode::ProcessFailed | ErrorCode::PrivilegeEscalationFailed));
1619            }
1620            _ => panic!("Expected PtyResult or Error response"),
1621        }
1622    }
1623    
1624    #[tokio::test]
1625    async fn test_pty_handler_prompt_detection() {
1626        let handler = PtyHandler;
1627        
1628        // Test default prompt patterns
1629        assert!(handler.detect_privilege_prompt("Password:", &[]));
1630        assert!(handler.detect_privilege_prompt("[sudo] password for user:", &[]));
1631        assert!(handler.detect_privilege_prompt("su: password", &[]));
1632        assert!(!handler.detect_privilege_prompt("normal output", &[]));
1633        
1634        // Test custom patterns
1635        let custom_patterns = vec!["Enter passphrase:".to_string()];
1636        assert!(handler.detect_privilege_prompt("Enter passphrase: ", &custom_patterns));
1637        assert!(!handler.detect_privilege_prompt("Password:", &custom_patterns));
1638    }
1639    
1640    #[tokio::test]
1641    async fn test_pty_handler_build_privileged_command() {
1642        let handler = PtyHandler;
1643        
1644        use mitoxide_proto::message::{PrivilegeEscalation, PrivilegeMethod, Credentials};
1645        
1646        let command = vec!["ls".to_string(), "-la".to_string()];
1647        
1648        // Test sudo
1649        let sudo_privilege = PrivilegeEscalation {
1650            method: PrivilegeMethod::Sudo,
1651            credentials: Some(Credentials {
1652                username: Some("root".to_string()),
1653                password: None,
1654            }),
1655            prompt_patterns: vec![],
1656        };
1657        
1658        let sudo_command = handler.build_privileged_command(&command, &sudo_privilege).unwrap();
1659        assert_eq!(sudo_command[0], "sudo");
1660        assert_eq!(sudo_command[1], "-S");
1661        assert_eq!(sudo_command[2], "-u");
1662        assert_eq!(sudo_command[3], "root");
1663        assert_eq!(sudo_command[4], "ls");
1664        assert_eq!(sudo_command[5], "-la");
1665        
1666        // Test su
1667        let su_privilege = PrivilegeEscalation {
1668            method: PrivilegeMethod::Su,
1669            credentials: Some(Credentials {
1670                username: Some("root".to_string()),
1671                password: None,
1672            }),
1673            prompt_patterns: vec![],
1674        };
1675        
1676        let su_command = handler.build_privileged_command(&command, &su_privilege).unwrap();
1677        assert_eq!(su_command[0], "su");
1678        assert_eq!(su_command[1], "root");
1679        assert_eq!(su_command[2], "-c");
1680        assert_eq!(su_command[3], "ls -la");
1681        
1682        // Test doas
1683        let doas_privilege = PrivilegeEscalation {
1684            method: PrivilegeMethod::Doas,
1685            credentials: Some(Credentials {
1686                username: Some("root".to_string()),
1687                password: None,
1688            }),
1689            prompt_patterns: vec![],
1690        };
1691        
1692        let doas_command = handler.build_privileged_command(&command, &doas_privilege).unwrap();
1693        assert_eq!(doas_command[0], "doas");
1694        assert_eq!(doas_command[1], "-u");
1695        assert_eq!(doas_command[2], "root");
1696        assert_eq!(doas_command[3], "ls");
1697        assert_eq!(doas_command[4], "-la");
1698    }
1699    
1700    #[tokio::test]
1701    async fn test_pty_handler_empty_command() {
1702        let handler = PtyHandler;
1703        
1704        let request = Request::PtyExec {
1705            id: Uuid::new_v4(),
1706            command: vec![],
1707            env: HashMap::new(),
1708            cwd: None,
1709            privilege: None,
1710            timeout: None,
1711        };
1712        
1713        let response = handler.handle(request).await.unwrap();
1714        
1715        match response {
1716            Response::Error { error, .. } => {
1717                assert_eq!(error.code, ErrorCode::InvalidRequest);
1718            }
1719            _ => panic!("Expected Error response"),
1720        }
1721    }
1722    
1723    #[tokio::test]
1724    async fn test_handler_wrong_request_type() {
1725        let ping_handler = PingHandler;
1726        
1727        // Use platform-appropriate command
1728        let command = if cfg!(windows) {
1729            vec!["cmd".to_string(), "/c".to_string(), "echo".to_string()]
1730        } else {
1731            vec!["echo".to_string()]
1732        };
1733        
1734        let process_request = Request::ProcessExec {
1735            id: Uuid::new_v4(),
1736            command,
1737            env: HashMap::new(),
1738            cwd: None,
1739            stdin: None,
1740            timeout: None,
1741        };
1742        
1743        let response = ping_handler.handle(process_request).await.unwrap();
1744        match response {
1745            Response::Error { error, .. } => {
1746                assert_eq!(error.code, ErrorCode::Unsupported);
1747            }
1748            _ => panic!("Expected Error response"),
1749        }
1750    }
1751}