ipfrs_cli/
shell.rs

1//! Interactive shell (REPL) for IPFRS
2//!
3//! Provides a read-eval-print loop for interactive IPFRS operations
4
5use anyhow::{Context, Result};
6use rustyline::completion::{Completer, Pair};
7use rustyline::error::ReadlineError;
8use rustyline::highlight::Highlighter;
9use rustyline::hint::Hinter;
10use rustyline::validate::Validator;
11use rustyline::{Editor, Helper};
12use std::collections::HashMap;
13use std::path::PathBuf;
14use tracing::{debug, info};
15
16use crate::output::{error, print_header, success};
17
18/// Calculate Levenshtein distance between two strings
19#[allow(clippy::needless_range_loop)]
20fn levenshtein_distance(s1: &str, s2: &str) -> usize {
21    let len1 = s1.len();
22    let len2 = s2.len();
23    let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
24
25    for i in 0..=len1 {
26        matrix[i][0] = i;
27    }
28    for j in 0..=len2 {
29        matrix[0][j] = j;
30    }
31
32    for (i, c1) in s1.chars().enumerate() {
33        for (j, c2) in s2.chars().enumerate() {
34            let cost = if c1 == c2 { 0 } else { 1 };
35            matrix[i + 1][j + 1] = std::cmp::min(
36                std::cmp::min(matrix[i][j + 1] + 1, matrix[i + 1][j] + 1),
37                matrix[i][j] + cost,
38            );
39        }
40    }
41
42    matrix[len1][len2]
43}
44
45/// Command completer for tab completion
46#[derive(Debug, Clone)]
47struct CommandCompleter {
48    commands: Vec<String>,
49}
50
51impl CommandCompleter {
52    fn new() -> Self {
53        Self {
54            commands: vec![
55                // General commands
56                "help".to_string(),
57                "?".to_string(),
58                "h".to_string(),
59                "exit".to_string(),
60                "quit".to_string(),
61                "q".to_string(),
62                "bye".to_string(),
63                "clear".to_string(),
64                "cls".to_string(),
65                "clean".to_string(),
66                "version".to_string(),
67                "pwd".to_string(),
68                "info".to_string(),
69                // File operations
70                "add".to_string(),
71                "get".to_string(),
72                "cat".to_string(),
73                "ls".to_string(),
74                // Network commands
75                "id".to_string(),
76                "peers".to_string(),
77                "peer".to_string(),
78                "connect".to_string(),
79                "disconnect".to_string(),
80                // Statistics
81                "stats".to_string(),
82                "stat".to_string(),
83                // Advanced commands
84                "semantic".to_string(),
85                "search".to_string(),
86                "find".to_string(),
87                "logic".to_string(),
88                "infer".to_string(),
89                "query".to_string(),
90                "tensor".to_string(),
91                "model".to_string(),
92                "gradient".to_string(),
93                // Pin management
94                "pin".to_string(),
95                "unpin".to_string(),
96                // Alias management
97                "alias".to_string(),
98                "unalias".to_string(),
99                // Common aliases
100                "ll".to_string(),
101                "list".to_string(),
102                "show".to_string(),
103                "view".to_string(),
104                "download".to_string(),
105                "upload".to_string(),
106                "put".to_string(),
107                "connections".to_string(),
108                "nodes".to_string(),
109                "whoami".to_string(),
110                "status".to_string(),
111                "statistics".to_string(),
112                "logout".to_string(),
113                "leave".to_string(),
114            ],
115        }
116    }
117}
118
119impl Completer for CommandCompleter {
120    type Candidate = Pair;
121
122    fn complete(
123        &self,
124        line: &str,
125        pos: usize,
126        _ctx: &rustyline::Context<'_>,
127    ) -> rustyline::Result<(usize, Vec<Pair>)> {
128        let start = line[..pos]
129            .rfind(char::is_whitespace)
130            .map(|i| i + 1)
131            .unwrap_or(0);
132
133        let prefix = &line[start..pos];
134
135        let matches: Vec<Pair> = self
136            .commands
137            .iter()
138            .filter(|cmd| cmd.starts_with(prefix))
139            .map(|cmd| Pair {
140                display: cmd.clone(),
141                replacement: cmd.clone(),
142            })
143            .collect();
144
145        Ok((start, matches))
146    }
147}
148
149impl Hinter for CommandCompleter {
150    type Hint = String;
151
152    fn hint(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> Option<String> {
153        if pos < line.len() {
154            return None;
155        }
156
157        let parts: Vec<&str> = line.split_whitespace().collect();
158        if parts.is_empty() {
159            return None;
160        }
161
162        // Provide hints based on incomplete commands
163        match parts[0] {
164            "add" if parts.len() == 1 => Some(" <path>".to_string()),
165            "get" if parts.len() == 1 => Some(" <cid> [output]".to_string()),
166            "cat" if parts.len() == 1 => Some(" <cid>".to_string()),
167            "ls" if parts.len() == 1 => Some(" <cid>".to_string()),
168            "stats" if parts.len() == 1 => Some(" [storage|semantic|logic]".to_string()),
169            "semantic" if parts.len() == 1 => Some(" <search|stats> [args...]".to_string()),
170            "logic" if parts.len() == 1 => Some(" <infer|prove|kb-stats> [args...]".to_string()),
171            "search" | "find" if parts.len() == 1 => Some(" <query>".to_string()),
172            "infer" | "query" if parts.len() == 1 => Some(" <goal>".to_string()),
173            _ => {
174                // Provide command completion hints
175                let prefix = parts[0];
176                self.commands
177                    .iter()
178                    .find(|cmd| cmd.starts_with(prefix) && cmd.len() > prefix.len())
179                    .map(|cmd| cmd[prefix.len()..].to_string())
180            }
181        }
182    }
183}
184
185impl Highlighter for CommandCompleter {}
186
187impl Validator for CommandCompleter {
188    fn validate(
189        &self,
190        ctx: &mut rustyline::validate::ValidationContext,
191    ) -> rustyline::Result<rustyline::validate::ValidationResult> {
192        let input = ctx.input();
193
194        // Check for line continuation (backslash at end)
195        if input.ends_with('\\') {
196            return Ok(rustyline::validate::ValidationResult::Incomplete);
197        }
198
199        // Check for unclosed quotes
200        let quote_count = input.chars().filter(|&c| c == '"').count();
201        if quote_count % 2 != 0 {
202            return Ok(rustyline::validate::ValidationResult::Incomplete);
203        }
204
205        // Check for unclosed parentheses (useful for logic queries)
206        let open_parens = input.chars().filter(|&c| c == '(').count();
207        let close_parens = input.chars().filter(|&c| c == ')').count();
208        if open_parens > close_parens {
209            return Ok(rustyline::validate::ValidationResult::Incomplete);
210        }
211
212        Ok(rustyline::validate::ValidationResult::Valid(None))
213    }
214}
215
216impl Helper for CommandCompleter {}
217
218/// Interactive shell configuration
219#[derive(Debug, Clone)]
220pub struct ShellConfig {
221    /// Data directory
222    pub data_dir: PathBuf,
223    /// History file path
224    pub history_file: PathBuf,
225    /// Prompt string
226    pub prompt: String,
227    /// User-defined command aliases (alias -> command)
228    pub aliases: HashMap<String, String>,
229}
230
231impl Default for ShellConfig {
232    fn default() -> Self {
233        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
234
235        // Initialize built-in aliases
236        let mut aliases = HashMap::new();
237
238        // Common shortcuts
239        aliases.insert("ll".to_string(), "ls".to_string());
240        aliases.insert("list".to_string(), "ls".to_string());
241        aliases.insert("show".to_string(), "cat".to_string());
242        aliases.insert("view".to_string(), "cat".to_string());
243        aliases.insert("download".to_string(), "get".to_string());
244        aliases.insert("upload".to_string(), "add".to_string());
245        aliases.insert("put".to_string(), "add".to_string());
246
247        // Network shortcuts
248        aliases.insert("connections".to_string(), "peers".to_string());
249        aliases.insert("nodes".to_string(), "peers".to_string());
250        aliases.insert("whoami".to_string(), "id".to_string());
251
252        // Statistics shortcuts
253        aliases.insert("status".to_string(), "stats".to_string());
254        aliases.insert("statistics".to_string(), "stats".to_string());
255
256        // Exit shortcuts (already handled in execute_command, but here for completeness)
257        aliases.insert("logout".to_string(), "exit".to_string());
258        aliases.insert("leave".to_string(), "exit".to_string());
259
260        Self {
261            data_dir: PathBuf::from(".ipfrs"),
262            history_file: home.join(".ipfrs_history"),
263            prompt: "ipfrs> ".to_string(),
264            aliases,
265        }
266    }
267}
268
269/// Interactive shell session
270pub struct Shell {
271    config: ShellConfig,
272    editor: Editor<CommandCompleter, rustyline::history::DefaultHistory>,
273}
274
275impl Shell {
276    /// Create a new interactive shell
277    pub fn new(config: ShellConfig) -> Result<Self> {
278        let mut editor = Editor::new().context("Failed to create line editor")?;
279
280        // Set up tab completion
281        editor.set_helper(Some(CommandCompleter::new()));
282
283        // Load history if it exists
284        if config.history_file.exists() {
285            if let Err(e) = editor.load_history(&config.history_file) {
286                debug!("Failed to load history: {}", e);
287            }
288        }
289
290        Ok(Self { config, editor })
291    }
292
293    /// Run the interactive shell
294    pub async fn run(&mut self) -> Result<()> {
295        print_header("IPFRS Interactive Shell");
296        println!("Type 'help' for available commands, 'exit' or Ctrl+D to quit");
297        println!("Use Tab for completion, Up/Down for history, Ctrl+R for search");
298        println!("Multi-line input: end line with \\ or leave quotes/parentheses unclosed\n");
299
300        loop {
301            match self.editor.readline(&self.config.prompt) {
302                Ok(line) => {
303                    let line = line.trim();
304                    if line.is_empty() {
305                        continue;
306                    }
307
308                    // Add to history
309                    let _ = self.editor.add_history_entry(line);
310
311                    // Parse and execute command
312                    match self.execute_command(line).await {
313                        Ok(should_continue) => {
314                            if !should_continue {
315                                break;
316                            }
317                        }
318                        Err(e) => {
319                            error(&format!("Error: {}", e));
320                        }
321                    }
322                }
323                Err(ReadlineError::Interrupted) => {
324                    println!("^C");
325                    continue;
326                }
327                Err(ReadlineError::Eof) => {
328                    println!("Goodbye!");
329                    break;
330                }
331                Err(err) => {
332                    error(&format!("Error reading line: {}", err));
333                    break;
334                }
335            }
336        }
337
338        // Save history
339        if let Err(e) = self.editor.save_history(&self.config.history_file) {
340            debug!("Failed to save history: {}", e);
341        }
342
343        Ok(())
344    }
345
346    /// Execute a shell command
347    /// Returns Ok(true) to continue, Ok(false) to exit
348    async fn execute_command(&mut self, line: &str) -> Result<bool> {
349        let parts: Vec<&str> = line.split_whitespace().collect();
350        if parts.is_empty() {
351            return Ok(true);
352        }
353
354        // Resolve alias
355        let command = if let Some(resolved) = self.config.aliases.get(parts[0]) {
356            resolved.as_str()
357        } else {
358            parts[0]
359        };
360
361        match command {
362            "help" | "?" | "h" => {
363                self.show_help();
364                Ok(true)
365            }
366            "exit" | "quit" | "q" | "bye" => {
367                println!("Goodbye!");
368                Ok(false)
369            }
370            "clear" | "cls" | "clean" => {
371                print!("\x1B[2J\x1B[1;1H");
372                Ok(true)
373            }
374            "version" => {
375                println!("IPFRS version {}", env!("CARGO_PKG_VERSION"));
376                Ok(true)
377            }
378            "pwd" => {
379                println!("{}", self.config.data_dir.display());
380                Ok(true)
381            }
382            "info" => {
383                self.show_info().await;
384                Ok(true)
385            }
386            "stats" | "stat" => {
387                self.show_stats(parts.get(1).copied()).await;
388                Ok(true)
389            }
390            "ls" => {
391                if parts.len() < 2 {
392                    error("Usage: ls <cid>");
393                } else {
394                    self.list_directory(parts[1]).await;
395                }
396                Ok(true)
397            }
398            "cat" => {
399                if parts.len() < 2 {
400                    error("Usage: cat <cid>");
401                } else {
402                    self.cat_file(parts[1]).await;
403                }
404                Ok(true)
405            }
406            "add" => {
407                if parts.len() < 2 {
408                    error("Usage: add <path>");
409                } else {
410                    self.add_file(parts[1]).await;
411                }
412                Ok(true)
413            }
414            "get" => {
415                if parts.len() < 2 {
416                    error("Usage: get <cid> [output_path]");
417                } else {
418                    let output = parts.get(2).copied();
419                    self.get_file(parts[1], output).await;
420                }
421                Ok(true)
422            }
423            "peers" | "peer" => {
424                self.list_peers().await;
425                Ok(true)
426            }
427            "id" => {
428                self.show_id().await;
429                Ok(true)
430            }
431            "semantic" | "search" | "find" => {
432                if parts.len() < 2 {
433                    error("Usage: semantic <search|stats> [args...] (or use: search/find <query>)");
434                } else {
435                    self.semantic_command(&parts[1..]).await;
436                }
437                Ok(true)
438            }
439            "logic" | "infer" | "query" => {
440                if parts.len() < 2 {
441                    error("Usage: logic <infer|prove|kb-stats> [args...] (or use: infer/query <goal>)");
442                } else {
443                    self.logic_command(&parts[1..]).await;
444                }
445                Ok(true)
446            }
447            "alias" => {
448                if parts.len() < 2 {
449                    // List all aliases
450                    self.list_aliases();
451                } else if parts.len() == 2 {
452                    // Show specific alias
453                    if let Some(resolved) = self.config.aliases.get(parts[1]) {
454                        println!("'{}' is aliased to '{}'", parts[1], resolved);
455                    } else {
456                        error(&format!("No alias found for '{}'", parts[1]));
457                    }
458                } else if parts.len() >= 3 {
459                    // Add new alias: alias <name> <command>
460                    let alias_name = parts[1].to_string();
461                    let alias_command = parts[2..].join(" ");
462                    self.config
463                        .aliases
464                        .insert(alias_name.clone(), alias_command.clone());
465                    success(&format!(
466                        "Alias '{}' -> '{}' added",
467                        alias_name, alias_command
468                    ));
469                }
470                Ok(true)
471            }
472            "unalias" => {
473                if parts.len() < 2 {
474                    error("Usage: unalias <alias_name>");
475                } else if self.config.aliases.remove(parts[1]).is_some() {
476                    success(&format!("Alias '{}' removed", parts[1]));
477                } else {
478                    error(&format!("No alias found for '{}'", parts[1]));
479                }
480                Ok(true)
481            }
482            _ => {
483                // Check if this might be a typo of a known command
484                let suggestion = self.suggest_command(parts[0]);
485                if let Some(suggested) = suggestion {
486                    error(&format!(
487                        "Unknown command: '{}'. Did you mean '{}'? Type 'help' for available commands.",
488                        parts[0], suggested
489                    ));
490                } else {
491                    error(&format!(
492                        "Unknown command: '{}'. Type 'help' for available commands.",
493                        parts[0]
494                    ));
495                }
496                Ok(true)
497            }
498        }
499    }
500
501    /// Suggest a command based on similarity to known commands
502    fn suggest_command(&self, input: &str) -> Option<String> {
503        let commands = vec![
504            "help", "exit", "quit", "clear", "version", "pwd", "info", "add", "get", "cat", "ls",
505            "peers", "id", "stats", "semantic", "search", "logic", "infer", "alias", "unalias",
506        ];
507
508        // Simple suggestion based on prefix matching or common typos
509        for cmd in &commands {
510            if cmd.starts_with(input) && cmd.len() > input.len() {
511                return Some(cmd.to_string());
512            }
513        }
514
515        // Check for common typos (single character difference)
516        for cmd in &commands {
517            if levenshtein_distance(input, cmd) == 1 {
518                return Some(cmd.to_string());
519            }
520        }
521
522        None
523    }
524
525    /// List all defined aliases
526    fn list_aliases(&self) {
527        if self.config.aliases.is_empty() {
528            println!("No aliases defined.");
529            return;
530        }
531
532        println!("\n{}", "=".repeat(60));
533        println!("Defined Aliases");
534        println!("{}\n", "=".repeat(60));
535
536        let mut aliases: Vec<_> = self.config.aliases.iter().collect();
537        aliases.sort_by(|a, b| a.0.cmp(b.0));
538
539        for (alias, command) in aliases {
540            println!("  {} -> {}", alias, command);
541        }
542        println!();
543    }
544
545    /// Show help message
546    fn show_help(&self) {
547        println!("\n{}", "=".repeat(60));
548        println!("IPFRS Interactive Shell - Available Commands");
549        println!("{}\n", "=".repeat(60));
550
551        println!("General:");
552        println!("  help, ?, h              Show this help message");
553        println!("  exit, quit, q, bye      Exit the shell");
554        println!("  clear, cls, clean       Clear the screen");
555        println!("  version                 Show version information");
556        println!("  info                    Show node information");
557        println!("  pwd                     Show current data directory");
558
559        println!("\nFile Operations:");
560        println!("  add <path>              Add a file to IPFRS");
561        println!("  get <cid> [output]      Get a file from IPFRS");
562        println!("  cat <cid>               Display file contents");
563        println!("  ls <cid>                List directory contents");
564
565        println!("\nNetwork:");
566        println!("  id                      Show peer ID and addresses");
567        println!("  peers, peer             List connected peers");
568
569        println!("\nStatistics:");
570        println!("  stats, stat             Show all statistics");
571        println!("  stats storage           Show storage statistics");
572        println!("  stats semantic          Show semantic search statistics");
573        println!("  stats logic             Show logic programming statistics");
574
575        println!("\nSemantic Search:");
576        println!("  semantic search <query> Search similar content");
577        println!("  search <query>          Alias for semantic search");
578        println!("  find <query>            Alias for semantic search");
579        println!("  semantic stats          Show semantic statistics");
580
581        println!("\nLogic Programming:");
582        println!("  logic infer <goal>      Run inference query");
583        println!("  infer <goal>            Alias for logic infer");
584        println!("  query <goal>            Alias for logic infer");
585        println!("  logic prove <goal>      Generate proof");
586        println!("  logic kb-stats          Show knowledge base statistics");
587
588        println!("\nAlias Management:");
589        println!("  alias                   List all aliases");
590        println!("  alias <name>            Show specific alias");
591        println!("  alias <name> <cmd>      Create new alias");
592        println!("  unalias <name>          Remove an alias");
593
594        println!("\nCommon Aliases:");
595        println!("  ll, list → ls           download → get");
596        println!("  show, view → cat        upload, put → add");
597        println!("  whoami → id             status → stats");
598        println!("  connections, nodes → peers");
599
600        println!("\nTips:");
601        println!("  • Use Tab for command completion");
602        println!("  • Use Up/Down arrows for command history");
603        println!("  • Create custom aliases with 'alias' command");
604        println!("  • Typos will suggest similar commands");
605
606        println!("\n{}", "=".repeat(60));
607    }
608
609    /// Show node information
610    #[allow(dead_code)]
611    async fn show_info(&self) {
612        println!("\nIPFRS Node Information:");
613        println!("  Data directory: {}", self.config.data_dir.display());
614        println!("  Status: Running");
615        success("Node is operational");
616    }
617
618    /// Show statistics
619    #[allow(dead_code)]
620    async fn show_stats(&self, category: Option<&str>) {
621        match category {
622            None => {
623                println!("\nNode Statistics:");
624                println!("  Storage: Available");
625                println!("  Semantic: Available");
626                println!("  Logic: Available");
627                info!("Use 'stats <category>' for detailed statistics");
628            }
629            Some("storage") => {
630                println!("\nStorage Statistics:");
631                println!("  Blocks: N/A (connect to daemon)");
632            }
633            Some("semantic") => {
634                println!("\nSemantic Statistics:");
635                println!("  Indexed vectors: N/A (connect to daemon)");
636            }
637            Some("logic") => {
638                println!("\nLogic Statistics:");
639                println!("  Facts: N/A (connect to daemon)");
640                println!("  Rules: N/A (connect to daemon)");
641            }
642            Some(cat) => {
643                error(&format!(
644                    "Unknown category: '{}'. Use storage, semantic, or logic.",
645                    cat
646                ));
647            }
648        }
649    }
650
651    /// List directory contents
652    #[allow(dead_code)]
653    async fn list_directory(&self, _cid: &str) {
654        info!("Directory listing not yet implemented in shell");
655        println!("Use 'ipfrs ls <cid>' from the command line");
656    }
657
658    /// Display file contents
659    #[allow(dead_code)]
660    async fn cat_file(&self, _cid: &str) {
661        info!("Cat command not yet implemented in shell");
662        println!("Use 'ipfrs cat <cid>' from the command line");
663    }
664
665    /// Add a file
666    #[allow(dead_code)]
667    async fn add_file(&self, _path: &str) {
668        info!("Add command not yet implemented in shell");
669        println!("Use 'ipfrs add <path>' from the command line");
670    }
671
672    /// Get a file
673    #[allow(dead_code)]
674    async fn get_file(&self, _cid: &str, _output: Option<&str>) {
675        info!("Get command not yet implemented in shell");
676        println!("Use 'ipfrs get <cid>' from the command line");
677    }
678
679    /// List peers
680    #[allow(dead_code)]
681    async fn list_peers(&self) {
682        info!("Peers command not yet implemented in shell");
683        println!("Use 'ipfrs swarm peers' from the command line");
684    }
685
686    /// Show peer ID
687    #[allow(dead_code)]
688    async fn show_id(&self) {
689        info!("ID command not yet implemented in shell");
690        println!("Use 'ipfrs id' from the command line");
691    }
692
693    /// Handle semantic commands
694    #[allow(dead_code)]
695    async fn semantic_command(&self, _args: &[&str]) {
696        info!("Semantic commands not yet implemented in shell");
697        println!("Use 'ipfrs semantic <command>' from the command line");
698    }
699
700    /// Handle logic commands
701    #[allow(dead_code)]
702    async fn logic_command(&self, _args: &[&str]) {
703        info!("Logic commands not yet implemented in shell");
704        println!("Use 'ipfrs logic <command>' from the command line");
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    #[test]
713    fn test_shell_config_default() {
714        let config = ShellConfig::default();
715        assert_eq!(config.prompt, "ipfrs> ");
716        assert_eq!(config.data_dir, PathBuf::from(".ipfrs"));
717    }
718
719    #[test]
720    fn test_shell_creation() {
721        let config = ShellConfig::default();
722        let result = Shell::new(config);
723        assert!(result.is_ok());
724    }
725
726    #[test]
727    fn test_command_completer() {
728        let completer = CommandCompleter::new();
729        assert!(!completer.commands.is_empty());
730        assert!(completer.commands.contains(&"help".to_string()));
731        assert!(completer.commands.contains(&"exit".to_string()));
732        assert!(completer.commands.contains(&"add".to_string()));
733    }
734
735    #[test]
736    fn test_command_completer_aliases() {
737        let completer = CommandCompleter::new();
738        // Test that aliases are included
739        assert!(completer.commands.contains(&"?".to_string()));
740        assert!(completer.commands.contains(&"q".to_string()));
741        assert!(completer.commands.contains(&"h".to_string()));
742        assert!(completer.commands.contains(&"search".to_string()));
743        assert!(completer.commands.contains(&"find".to_string()));
744    }
745
746    #[test]
747    fn test_command_hint_logic() {
748        let completer = CommandCompleter::new();
749
750        // Test hint logic for various commands
751        // Note: We can't easily test the hint() method due to rustyline's Context API,
752        // but we can verify the command list is properly set up
753        let commands = &completer.commands;
754
755        // Verify command coverage
756        assert!(commands.contains(&"add".to_string()));
757        assert!(commands.contains(&"get".to_string()));
758        assert!(commands.contains(&"cat".to_string()));
759        assert!(commands.contains(&"ls".to_string()));
760        assert!(commands.contains(&"stats".to_string()));
761        assert!(commands.contains(&"semantic".to_string()));
762        assert!(commands.contains(&"logic".to_string()));
763    }
764
765    #[test]
766    fn test_multiline_validation_logic() {
767        // Test the validation logic without using ValidationContext
768        // (which has a complex internal API)
769
770        // Test backslash detection
771        assert!("test \\".ends_with('\\'));
772        assert!(!"test".ends_with('\\'));
773
774        // Test quote counting
775        let quote_count_odd = "add \"file".chars().filter(|&c| c == '"').count();
776        let quote_count_even = "add \"file\"".chars().filter(|&c| c == '"').count();
777        assert_eq!(quote_count_odd % 2, 1); // Odd = unclosed
778        assert_eq!(quote_count_even % 2, 0); // Even = closed
779
780        // Test parentheses counting
781        let input1 = "logic (foo";
782        let open1 = input1.chars().filter(|&c| c == '(').count();
783        let close1 = input1.chars().filter(|&c| c == ')').count();
784        assert!(open1 > close1); // Unclosed
785
786        let input2 = "logic (foo)";
787        let open2 = input2.chars().filter(|&c| c == '(').count();
788        let close2 = input2.chars().filter(|&c| c == ')').count();
789        assert_eq!(open2, close2); // Balanced
790    }
791
792    #[test]
793    fn test_levenshtein_distance() {
794        assert_eq!(levenshtein_distance("", ""), 0);
795        assert_eq!(levenshtein_distance("cat", "cat"), 0);
796        assert_eq!(levenshtein_distance("cat", "bat"), 1);
797        assert_eq!(levenshtein_distance("cat", "ca"), 1);
798        assert_eq!(levenshtein_distance("cat", "cats"), 1);
799        assert_eq!(levenshtein_distance("help", "halp"), 1);
800        assert_eq!(levenshtein_distance("add", "dad"), 2); // 'a'->'d' and 'd'->'a'
801        assert_eq!(levenshtein_distance("exit", "exot"), 1); // 'i'->'o'
802        assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
803    }
804
805    #[test]
806    fn test_shell_config_aliases() {
807        let config = ShellConfig::default();
808
809        // Check that default aliases are set
810        assert!(config.aliases.contains_key("ll"));
811        assert_eq!(config.aliases.get("ll").unwrap(), "ls");
812
813        assert!(config.aliases.contains_key("whoami"));
814        assert_eq!(config.aliases.get("whoami").unwrap(), "id");
815
816        assert!(config.aliases.contains_key("upload"));
817        assert_eq!(config.aliases.get("upload").unwrap(), "add");
818
819        assert!(config.aliases.contains_key("download"));
820        assert_eq!(config.aliases.get("download").unwrap(), "get");
821    }
822
823    #[test]
824    fn test_command_completer_includes_aliases() {
825        let completer = CommandCompleter::new();
826
827        // Check that aliases are in the completion list
828        assert!(completer.commands.contains(&"ll".to_string()));
829        assert!(completer.commands.contains(&"whoami".to_string()));
830        assert!(completer.commands.contains(&"alias".to_string()));
831        assert!(completer.commands.contains(&"unalias".to_string()));
832        assert!(completer.commands.contains(&"upload".to_string()));
833        assert!(completer.commands.contains(&"download".to_string()));
834    }
835}