Skip to main content

neumann_shell/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Neumann Shell - Interactive CLI for Neumann database
3//!
4//! Provides a readline-based interface for executing queries against the
5//! Neumann unified query engine.
6
7pub mod cli;
8mod doctor;
9mod input;
10mod output;
11mod progress;
12mod style;
13mod wal;
14
15pub use input::NeumannHelper;
16pub use style::{Icons, Theme};
17pub use wal::{Wal, WalRecoveryMode, WalReplayError, WalReplayResult};
18
19use std::{
20    fmt::Write as _,
21    io::{BufRead, BufReader},
22    path::{Path, PathBuf},
23    sync::Arc,
24};
25
26use parking_lot::{Mutex, RwLock};
27use query_router::QueryRouter;
28use rustyline::{
29    error::ReadlineError,
30    history::{DefaultHistory, History},
31    Editor,
32};
33use tensor_chain::QueryExecutor;
34use tensor_checkpoint::{
35    format_confirmation_prompt, ConfirmationHandler, DestructiveOp, OperationPreview,
36};
37use tensor_store::TensorStore;
38
39/// Shell configuration options.
40#[derive(Debug, Clone)]
41pub struct ShellConfig {
42    /// Path to history file (None disables persistence).
43    pub history_file: Option<PathBuf>,
44    /// Maximum number of history entries to keep.
45    pub history_size: usize,
46    /// Prompt string displayed before each input.
47    pub prompt: String,
48    /// Color theme for output.
49    pub theme: Theme,
50    /// Disable colored output.
51    pub no_color: bool,
52    /// Skip boot sequence animation.
53    pub no_boot: bool,
54    /// Quiet mode: suppress non-essential output.
55    pub quiet: bool,
56}
57
58impl Default for ShellConfig {
59    fn default() -> Self {
60        Self {
61            history_file: dirs_home().map(|h| h.join(".neumann_history")),
62            history_size: 1000,
63            // Phosphor green prompt to match boot aesthetic
64            prompt: "\x1b[38;2;0;238;0mneumann>\x1b[0m ".to_string(),
65            theme: Theme::auto(),
66            no_color: false,
67            no_boot: false,
68            quiet: false,
69        }
70    }
71}
72
73/// Returns the user's home directory if available.
74fn dirs_home() -> Option<PathBuf> {
75    std::env::var_os("HOME").map(PathBuf::from)
76}
77
78/// Result of executing a shell command.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum CommandResult {
81    /// Query executed successfully with output.
82    Output(String),
83    /// Shell should exit.
84    Exit,
85    /// Help text to display.
86    Help(String),
87    /// Empty input (no-op).
88    Empty,
89    /// Error occurred.
90    Error(String),
91}
92
93/// Interactive shell for Neumann database.
94pub struct Shell {
95    router: Arc<RwLock<QueryRouter>>,
96    config: ShellConfig,
97    wal: Mutex<Option<Wal>>,
98    icons: &'static Icons,
99}
100
101/// Wrapper to implement `QueryExecutor` for `Arc<RwLock<QueryRouter>>`.
102struct RouterExecutor(Arc<RwLock<QueryRouter>>);
103
104impl QueryExecutor for RouterExecutor {
105    fn execute(&self, query: &str) -> std::result::Result<Vec<u8>, String> {
106        let router = self.0.read();
107        router.execute_for_cluster(query)
108    }
109}
110
111/// Interactive confirmation handler for destructive operations.
112struct ShellConfirmationHandler {
113    editor: Arc<Mutex<Editor<NeumannHelper, DefaultHistory>>>,
114}
115
116impl ShellConfirmationHandler {
117    const fn new(editor: Arc<Mutex<Editor<NeumannHelper, DefaultHistory>>>) -> Self {
118        Self { editor }
119    }
120}
121
122impl ConfirmationHandler for ShellConfirmationHandler {
123    fn confirm(&self, op: &DestructiveOp, preview: &OperationPreview) -> bool {
124        let prompt = format_confirmation_prompt(op, preview);
125        println!("\n{prompt}");
126
127        let mut editor = self.editor.lock();
128        editor
129            .readline("Type 'yes' to proceed: ")
130            .is_ok_and(|input| input.trim().eq_ignore_ascii_case("yes"))
131    }
132}
133
134impl Shell {
135    /// Creates a new shell with default configuration.
136    #[must_use]
137    pub fn new() -> Self {
138        Self {
139            router: Arc::new(RwLock::new(QueryRouter::new())),
140            config: ShellConfig::default(),
141            wal: Mutex::new(None),
142            icons: Icons::auto(),
143        }
144    }
145
146    /// Creates a new shell with custom configuration.
147    #[must_use]
148    pub fn with_config(mut config: ShellConfig) -> Self {
149        // Apply no_color setting
150        if config.no_color {
151            config.theme = Theme::plain();
152            config.prompt = "neumann> ".to_string();
153        }
154
155        let icons = if config.no_color {
156            Icons::plain()
157        } else {
158            Icons::auto()
159        };
160
161        Self {
162            router: Arc::new(RwLock::new(QueryRouter::new())),
163            config,
164            wal: Mutex::new(None),
165            icons,
166        }
167    }
168
169    /// Returns a clone of the router Arc for shared access.
170    #[must_use]
171    pub fn router_arc(&self) -> Arc<RwLock<QueryRouter>> {
172        Arc::clone(&self.router)
173    }
174
175    /// Returns a read guard to the query router for direct access.
176    pub fn router(&self) -> parking_lot::RwLockReadGuard<'_, QueryRouter> {
177        self.router.read()
178    }
179
180    /// Returns a write guard to the query router for mutable access.
181    pub fn router_mut(&self) -> parking_lot::RwLockWriteGuard<'_, QueryRouter> {
182        self.router.write()
183    }
184
185    /// Check if a command is a write operation that should be logged to WAL.
186    fn is_write_command(cmd: &str) -> bool {
187        let upper = cmd.to_uppercase();
188        let first_word = upper.split_whitespace().next().unwrap_or("");
189
190        match first_word {
191            "INSERT" | "UPDATE" | "DELETE" | "CREATE" | "DROP" | "CHECKPOINT" | "ROLLBACK" => true,
192            "NODE" => !upper.contains("NODE GET"),
193            "EDGE" => !upper.contains("EDGE GET"),
194            "EMBED" => upper.contains("EMBED STORE") || upper.contains("EMBED DELETE"),
195            "VAULT" => {
196                upper.contains("VAULT SET")
197                    || upper.contains("VAULT DELETE")
198                    || upper.contains("VAULT ROTATE")
199                    || upper.contains("VAULT GRANT")
200                    || upper.contains("VAULT REVOKE")
201            },
202            "CACHE" => upper.contains("CACHE CLEAR"),
203            "BLOB" => {
204                upper.contains("BLOB PUT")
205                    || upper.contains("BLOB DELETE")
206                    || upper.contains("BLOB LINK")
207                    || upper.contains("BLOB UNLINK")
208                    || upper.contains("BLOB TAG")
209                    || upper.contains("BLOB UNTAG")
210                    || upper.contains("BLOB GC")
211                    || upper.contains("BLOB REPAIR")
212                    || upper.contains("BLOB META SET")
213            },
214            "ENTITY" => !upper.contains("ENTITY GET"),
215            "GRAPH" => {
216                upper.contains("GRAPH BATCH")
217                    || upper.contains("CONSTRAINT CREATE")
218                    || upper.contains("CONSTRAINT DROP")
219                    || upper.contains("INDEX CREATE")
220                    || upper.contains("INDEX DROP")
221            },
222            "BEGIN" => upper.contains("BEGIN CHAIN"),
223            "COMMIT" => upper.contains("COMMIT CHAIN"),
224            _ => false,
225        }
226    }
227
228    /// Replay commands from a WAL file.
229    fn replay_wal(
230        &self,
231        wal_path: &Path,
232        mode: WalRecoveryMode,
233    ) -> Result<WalReplayResult, String> {
234        let file = std::fs::File::open(wal_path).map_err(|e| format!("Failed to open WAL: {e}"))?;
235        let reader = BufReader::new(file);
236
237        let mut replayed = 0;
238        let mut errors = Vec::new();
239
240        for (line_num, line) in reader.lines().enumerate() {
241            let line_number = line_num + 1;
242
243            let cmd = match line {
244                Ok(c) => c,
245                Err(e) => {
246                    let error = format!("Failed to read line: {e}");
247                    match mode {
248                        WalRecoveryMode::Strict => {
249                            return Err(format!(
250                                "WAL replay failed at line {line_number}: {error}"
251                            ));
252                        },
253                        WalRecoveryMode::Recover => {
254                            errors.push(WalReplayError::new(line_number, "<unreadable>", error));
255                            continue;
256                        },
257                    }
258                },
259            };
260
261            let cmd = cmd.trim();
262            if cmd.is_empty() {
263                continue;
264            }
265
266            let result = self.router.read().execute_parsed(cmd);
267            if let Err(e) = result {
268                match mode {
269                    WalRecoveryMode::Strict => {
270                        return Err(format!("WAL replay failed at line {line_number}: {e}"));
271                    },
272                    WalRecoveryMode::Recover => {
273                        errors.push(WalReplayError::new(line_number, cmd, e.to_string()));
274                        continue;
275                    },
276                }
277            }
278            replayed += 1;
279        }
280
281        Ok(WalReplayResult { replayed, errors })
282    }
283
284    /// Executes a single command and returns the result.
285    pub fn execute(&mut self, input: &str) -> CommandResult {
286        let trimmed = input.trim();
287
288        if trimmed.is_empty() {
289            return CommandResult::Empty;
290        }
291
292        // Handle built-in commands
293        let lower = trimmed.to_lowercase();
294        match lower.as_str() {
295            "exit" | "quit" | "\\q" => return CommandResult::Exit,
296            "help" | "\\h" | "\\?" => {
297                return CommandResult::Help(output::format_help(&self.config.theme))
298            },
299            "tables" | "\\dt" => return self.list_tables(),
300            "clear" | "\\c" => return CommandResult::Output("\x1B[2J\x1B[H".to_string()),
301            "wal status" => return self.handle_wal_status(),
302            "wal truncate" => return self.handle_wal_truncate(),
303            "doctor" => return self.handle_doctor(),
304            _ => {},
305        }
306
307        // Handle SAVE COMPRESSED command
308        if lower.starts_with("save compressed") {
309            return self.handle_save_compressed(trimmed);
310        }
311
312        // Handle SAVE command
313        if lower.starts_with("save ") {
314            return self.handle_save(trimmed);
315        }
316
317        // Handle LOAD command
318        if lower.starts_with("load ") {
319            return self.handle_load(trimmed);
320        }
321
322        // Handle VAULT INIT command
323        if lower == "vault init" {
324            return self.handle_vault_init();
325        }
326
327        // Handle VAULT IDENTITY command
328        if lower.starts_with("vault identity") {
329            return self.handle_vault_identity(trimmed);
330        }
331
332        // Handle CACHE INIT command
333        if lower == "cache init" {
334            return self.handle_cache_init();
335        }
336
337        // Handle CLUSTER CONNECT command
338        if lower.starts_with("cluster connect") {
339            return self.handle_cluster_connect(trimmed);
340        }
341
342        // Handle CLUSTER DISCONNECT command
343        if lower == "cluster disconnect" {
344            return self.handle_cluster_disconnect();
345        }
346
347        // Execute with optional spinner for long operations
348        let use_spinner = progress::needs_spinner(trimmed);
349        let spinner = if use_spinner {
350            Some(progress::operation_spinner(trimmed, &self.config.theme))
351        } else {
352            None
353        };
354
355        let query_result = self.router.read().execute_parsed(trimmed);
356
357        if let Some(ref s) = spinner {
358            s.finish_and_clear();
359        }
360
361        match query_result {
362            Ok(result) => {
363                // Log write commands to WAL after successful execution
364                if Self::is_write_command(trimmed) {
365                    let mut wal_guard = self.wal.lock();
366                    if let Some(ref mut wal) = *wal_guard {
367                        if let Err(e) = wal.append(trimmed) {
368                            return CommandResult::Error(format!(
369                                "Command succeeded but WAL write failed: {e}"
370                            ));
371                        }
372                    }
373                }
374                CommandResult::Output(output::format_result(
375                    &result,
376                    &self.config.theme,
377                    self.icons,
378                ))
379            },
380            Err(e) => {
381                let error_msg = format!(
382                    "{} Error: {e}",
383                    style::styled(self.icons.error, self.config.theme.error)
384                );
385                CommandResult::Error(error_msg)
386            },
387        }
388    }
389
390    /// Handles the SAVE command.
391    fn handle_save(&self, input: &str) -> CommandResult {
392        let Some(p) = Self::extract_path(input, "save") else {
393            return CommandResult::Error(
394                "Usage: SAVE 'path/to/file.bin' or SAVE path/to/file.bin".to_string(),
395            );
396        };
397
398        let spinner = progress::operation_spinner("SAVE", &self.config.theme);
399
400        let store = self.router.read().vector().store().clone();
401        if let Err(e) = store.save_snapshot(&p) {
402            spinner.finish_error(&format!("Failed to save: {e}"));
403            return CommandResult::Error(format!("Failed to save: {e}"));
404        }
405
406        // Truncate WAL after successful save
407        if let Some(ref mut wal) = *self.wal.lock() {
408            if let Err(e) = wal.truncate() {
409                spinner.finish_error("Saved but WAL truncate failed");
410                return CommandResult::Error(format!(
411                    "Saved snapshot but WAL truncate failed: {e}"
412                ));
413            }
414        }
415
416        spinner.finish_success(&format!("Saved to {p}"));
417        CommandResult::Output(format!(
418            "{} Saved snapshot to: {}",
419            style::styled(self.icons.success, self.config.theme.success),
420            style::styled(&p, self.config.theme.string)
421        ))
422    }
423
424    /// Handles the SAVE COMPRESSED command.
425    fn handle_save_compressed(&self, input: &str) -> CommandResult {
426        let Some(p) = Self::extract_path(input, "save compressed") else {
427            return CommandResult::Error("Usage: SAVE COMPRESSED 'path/to/file.bin'".to_string());
428        };
429
430        let spinner = progress::operation_spinner("SAVE COMPRESSED", &self.config.theme);
431
432        let store = self.router.read().vector().store().clone();
433        let dim = Self::detect_embedding_dimension(&store);
434        let config = tensor_compress::CompressionConfig::balanced(dim);
435
436        if let Err(e) = store.save_snapshot_compressed(&p, config) {
437            spinner.finish_error(&format!("Failed to save: {e}"));
438            return CommandResult::Error(format!("Failed to save compressed: {e}"));
439        }
440
441        // Truncate WAL after successful save
442        if let Some(ref mut wal) = *self.wal.lock() {
443            if let Err(e) = wal.truncate() {
444                spinner.finish_error("Saved but WAL truncate failed");
445                return CommandResult::Error(format!(
446                    "Saved snapshot but WAL truncate failed: {e}"
447                ));
448            }
449        }
450
451        spinner.finish_success(&format!("Saved compressed to {p}"));
452        CommandResult::Output(format!(
453            "{} Saved compressed snapshot to: {}",
454            style::styled(self.icons.success, self.config.theme.success),
455            style::styled(&p, self.config.theme.string)
456        ))
457    }
458
459    /// Handles the LOAD command.
460    fn handle_load(&self, input: &str) -> CommandResult {
461        let Some((p, recovery_mode)) = Self::extract_load_path_and_mode(input) else {
462            return CommandResult::Error(
463                "Usage: LOAD 'path/to/file.bin' or LOAD 'path' RECOVER".to_string(),
464            );
465        };
466
467        let spinner = progress::operation_spinner("LOAD", &self.config.theme);
468
469        // Try compressed format first, fall back to legacy
470        let result =
471            TensorStore::load_snapshot_compressed(&p).or_else(|_| TensorStore::load_snapshot(&p));
472
473        match result {
474            Ok(store) => {
475                *self.router.write() = QueryRouter::with_shared_store(store);
476
477                // Derive WAL path from snapshot path
478                let wal_path = Path::new(&p).with_extension("log");
479
480                // Replay WAL if it exists
481                let replay_msg = if wal_path.exists() {
482                    match self.replay_wal(&wal_path, recovery_mode) {
483                        Ok(result) => Self::format_wal_replay_result(&result, recovery_mode),
484                        Err(e) => {
485                            spinner.finish_error("WAL replay failed");
486                            let hint = if recovery_mode == WalRecoveryMode::Strict {
487                                "\nHint: Use 'LOAD path RECOVER' to skip corrupted entries"
488                            } else {
489                                ""
490                            };
491                            return CommandResult::Error(format!(
492                                "Loaded snapshot but WAL replay failed: {e}{hint}"
493                            ));
494                        },
495                    }
496                } else {
497                    String::new()
498                };
499
500                // Initialize WAL for new writes
501                match Wal::open_append(&wal_path) {
502                    Ok(wal) => {
503                        *self.wal.lock() = Some(wal);
504                        spinner.finish_success(&format!("Loaded from {p}"));
505                        CommandResult::Output(format!(
506                            "{} Loaded snapshot from: {}{replay_msg}",
507                            style::styled(self.icons.success, self.config.theme.success),
508                            style::styled(&p, self.config.theme.string)
509                        ))
510                    },
511                    Err(e) => {
512                        spinner.finish_error("Failed to initialize WAL");
513                        CommandResult::Error(format!(
514                            "Loaded snapshot but failed to initialize WAL: {e}"
515                        ))
516                    },
517                }
518            },
519            Err(e) => {
520                spinner.finish_error(&format!("Failed to load: {e}"));
521                CommandResult::Error(format!("Failed to load: {e}"))
522            },
523        }
524    }
525
526    /// Extracts path and recovery mode from LOAD command.
527    fn extract_load_path_and_mode(input: &str) -> Option<(String, WalRecoveryMode)> {
528        let rest = input
529            .strip_prefix("load")
530            .or_else(|| input.strip_prefix("LOAD"))?
531            .trim();
532
533        if rest.is_empty() {
534            return None;
535        }
536
537        let upper_rest = rest.to_uppercase();
538
539        if upper_rest == "RECOVER" {
540            return None;
541        }
542
543        let (path_part, mode) = if upper_rest.ends_with(" RECOVER") {
544            let path_end = rest.len() - " RECOVER".len();
545            (&rest[..path_end], WalRecoveryMode::Recover)
546        } else {
547            (rest, WalRecoveryMode::Strict)
548        };
549
550        let path_part = path_part.trim();
551        if path_part.is_empty() {
552            return None;
553        }
554
555        let path = if (path_part.starts_with('\'') && path_part.ends_with('\''))
556            || (path_part.starts_with('"') && path_part.ends_with('"'))
557        {
558            if path_part.len() > 2 {
559                path_part[1..path_part.len() - 1].to_string()
560            } else {
561                return None;
562            }
563        } else {
564            path_part.to_string()
565        };
566
567        Some((path, mode))
568    }
569
570    /// Formats the WAL replay result for display.
571    fn format_wal_replay_result(result: &WalReplayResult, mode: WalRecoveryMode) -> String {
572        let mut output = String::new();
573
574        if result.replayed > 0 {
575            let _ = write!(output, "\nReplayed {} commands from WAL", result.replayed);
576        }
577
578        if mode == WalRecoveryMode::Recover && !result.errors.is_empty() {
579            let _ = write!(
580                output,
581                "\nWarning: Skipped {} corrupted WAL entries:",
582                result.errors.len()
583            );
584
585            for error in result.errors.iter().take(5) {
586                let _ = write!(
587                    output,
588                    "\n  Line {}: {} ({})",
589                    error.line, error.command, error.error
590                );
591            }
592
593            if result.errors.len() > 5 {
594                let _ = write!(output, "\n  ... and {} more", result.errors.len() - 5);
595            }
596        }
597
598        output
599    }
600
601    /// Extracts the path from a SAVE or LOAD command.
602    fn extract_path(input: &str, command: &str) -> Option<String> {
603        let rest = input[command.len()..].trim();
604        if rest.is_empty() {
605            return None;
606        }
607
608        if (rest.starts_with('\'') && rest.ends_with('\''))
609            || (rest.starts_with('"') && rest.ends_with('"'))
610        {
611            if rest.len() > 2 {
612                return Some(rest[1..rest.len() - 1].to_string());
613            }
614            return None;
615        }
616
617        Some(rest.to_string())
618    }
619
620    /// Detect the most common embedding dimension from stored vectors.
621    fn detect_embedding_dimension(store: &TensorStore) -> usize {
622        use tensor_store::TensorValue;
623
624        let keys = store.scan("");
625        for key in keys.iter().take(100) {
626            if let Ok(tensor) = store.get(key) {
627                for field in tensor.keys() {
628                    match tensor.get(field) {
629                        Some(TensorValue::Vector(v)) => {
630                            return v.len();
631                        },
632                        Some(TensorValue::Sparse(s)) => {
633                            return s.dimension();
634                        },
635                        _ => {},
636                    }
637                }
638            }
639        }
640
641        tensor_compress::CompressionDefaults::STANDARD
642    }
643
644    /// Handles the WAL STATUS command.
645    fn handle_wal_status(&self) -> CommandResult {
646        self.wal.lock().as_ref().map_or_else(
647            || {
648                CommandResult::Output(
649                    "WAL not active (use LOAD to enable WAL for a snapshot)".to_string(),
650                )
651            },
652            |wal| {
653                let size = wal.size().unwrap_or(0);
654                CommandResult::Output(format!(
655                    "WAL enabled\n  Path: {}\n  Size: {} bytes",
656                    wal.path().display(),
657                    size
658                ))
659            },
660        )
661    }
662
663    /// Handles the WAL TRUNCATE command.
664    fn handle_wal_truncate(&self) -> CommandResult {
665        self.wal.lock().as_mut().map_or_else(
666            || {
667                CommandResult::Error(
668                    "WAL not active (use LOAD to enable WAL for a snapshot)".to_string(),
669                )
670            },
671            |wal| match wal.truncate() {
672                Ok(()) => CommandResult::Output("WAL truncated".to_string()),
673                Err(e) => CommandResult::Error(format!("Failed to truncate WAL: {e}")),
674            },
675        )
676    }
677
678    /// Handles the DOCTOR command for system diagnostics.
679    #[allow(clippy::significant_drop_tightening)]
680    fn handle_doctor(&self) -> CommandResult {
681        let wal_guard = self.wal.lock();
682        let wal_ref = wal_guard.as_ref();
683        let router = self.router.read();
684
685        let ctx = doctor::DiagnosticContext::new(&router, wal_ref);
686        let report = doctor::run_diagnostics(&ctx);
687        let output = doctor::output::format_report(&report, &self.config.theme, self.icons);
688
689        CommandResult::Output(output)
690    }
691
692    /// Initialize vault from environment variable.
693    fn handle_vault_init(&self) -> CommandResult {
694        match std::env::var("NEUMANN_VAULT_KEY") {
695            Ok(key) => {
696                let decoded = match base64::Engine::decode(
697                    &base64::engine::general_purpose::STANDARD,
698                    &key,
699                ) {
700                    Ok(d) => d,
701                    Err(e) => {
702                        return CommandResult::Error(format!(
703                            "Invalid base64 in NEUMANN_VAULT_KEY: {e}"
704                        ))
705                    },
706                };
707
708                let result = self.router.write().init_vault(&decoded);
709                match result {
710                    Ok(()) => CommandResult::Output(format!(
711                        "{} Vault initialized",
712                        style::styled(self.icons.success, self.config.theme.success)
713                    )),
714                    Err(e) => CommandResult::Error(format!("Failed to initialize vault: {e}")),
715                }
716            },
717            Err(_) => CommandResult::Error(
718                "Set NEUMANN_VAULT_KEY environment variable (base64 encoded 32-byte key)"
719                    .to_string(),
720            ),
721        }
722    }
723
724    /// Set current identity for vault access control.
725    fn handle_vault_identity(&self, input: &str) -> CommandResult {
726        let prefix_len = "vault identity".len();
727        let rest = input.get(prefix_len..).unwrap_or("").trim().to_string();
728
729        let identity = if rest.starts_with('\'') && rest.ends_with('\'') && rest.len() > 2 {
730            &rest[1..rest.len() - 1]
731        } else if !rest.is_empty() {
732            &rest
733        } else {
734            return CommandResult::Output(format!(
735                "Current identity: {}",
736                self.router.read().current_identity().unwrap_or("<none>")
737            ));
738        };
739
740        self.router.write().set_identity(identity);
741        CommandResult::Output(format!(
742            "{} Identity set to: {}",
743            style::styled(self.icons.success, self.config.theme.success),
744            style::styled(identity, self.config.theme.id)
745        ))
746    }
747
748    /// Initialize the cache with default configuration.
749    fn handle_cache_init(&self) -> CommandResult {
750        let result = self.router.write().init_cache_default();
751        match result {
752            Ok(()) => CommandResult::Output(format!(
753                "{} Cache initialized",
754                style::styled(self.icons.success, self.config.theme.success)
755            )),
756            Err(e) => CommandResult::Error(format!("Failed to initialize cache: {e}")),
757        }
758    }
759
760    /// Handles the CLUSTER CONNECT command.
761    fn handle_cluster_connect(&self, input: &str) -> CommandResult {
762        let args = input.trim();
763        let args = args
764            .strip_prefix("cluster connect")
765            .or_else(|| args.strip_prefix("CLUSTER CONNECT"))
766            .unwrap_or(args)
767            .trim();
768
769        if args.is_empty() {
770            return CommandResult::Error(
771                "Usage: CLUSTER CONNECT 'node_id@bind_addr' ['peer_id@peer_addr', ...]".to_string(),
772            );
773        }
774
775        let spinner = progress::operation_spinner("CLUSTER CONNECT", &self.config.theme);
776
777        // Parse quoted strings
778        let mut addresses: Vec<String> = Vec::new();
779        let mut current = String::new();
780        let mut in_quote = false;
781        let mut quote_char = '"';
782
783        for c in args.chars() {
784            match c {
785                '\'' | '"' if !in_quote => {
786                    in_quote = true;
787                    quote_char = c;
788                },
789                c if c == quote_char && in_quote => {
790                    in_quote = false;
791                    if !current.is_empty() {
792                        addresses.push(current.clone());
793                        current.clear();
794                    }
795                },
796                _ if in_quote => current.push(c),
797                ' ' | ',' => {},
798                _ => current.push(c),
799            }
800        }
801        if !current.is_empty() {
802            addresses.push(current);
803        }
804
805        if addresses.is_empty() {
806            spinner.finish_error("No addresses provided");
807            return CommandResult::Error("No addresses provided".to_string());
808        }
809
810        // Parse first address as local node
811        let local = &addresses[0];
812        let (node_id, bind_addr) = match Self::parse_node_address(local) {
813            Ok(parsed) => parsed,
814            Err(e) => {
815                spinner.finish_error(&format!("Invalid address: {e}"));
816                return CommandResult::Error(format!("Invalid local address: {e}"));
817            },
818        };
819
820        // Parse remaining addresses as peers
821        let mut peers: Vec<(String, std::net::SocketAddr)> = Vec::new();
822        for addr in &addresses[1..] {
823            match Self::parse_node_address(addr) {
824                Ok((peer_id, peer_addr)) => peers.push((peer_id, peer_addr)),
825                Err(e) => {
826                    spinner.finish_error(&format!("Invalid peer: {e}"));
827                    return CommandResult::Error(format!("Invalid peer address '{addr}': {e}"));
828                },
829            }
830        }
831
832        // Create executor wrapper
833        let executor: Arc<dyn QueryExecutor> = Arc::new(RouterExecutor(Arc::clone(&self.router)));
834
835        // Initialize cluster
836        let result = {
837            let mut router = self.router.write();
838            router.init_cluster_with_executor(&node_id, bind_addr, &peers, Some(executor))
839        };
840
841        match result {
842            Ok(()) => {
843                spinner.finish_success(&format!("Connected as {node_id}"));
844                CommandResult::Output(format!(
845                    "{} Cluster initialized: {} @ {} with {} peer(s)",
846                    style::styled(self.icons.success, self.config.theme.success),
847                    style::styled(&node_id, self.config.theme.id),
848                    style::styled(bind_addr, self.config.theme.muted),
849                    style::styled(peers.len(), self.config.theme.number)
850                ))
851            },
852            Err(e) => {
853                spinner.finish_error(&format!("Failed: {e}"));
854                CommandResult::Error(format!("Failed to connect to cluster: {e}"))
855            },
856        }
857    }
858
859    /// Parse a node address in the format `node_id@host:port`.
860    fn parse_node_address(s: &str) -> std::result::Result<(String, std::net::SocketAddr), String> {
861        let parts: Vec<&str> = s.splitn(2, '@').collect();
862        if parts.len() != 2 {
863            return Err("Expected format 'node_id@host:port'".to_string());
864        }
865
866        let node_id = parts[0].to_string();
867        let addr: std::net::SocketAddr = parts[1]
868            .parse()
869            .map_err(|e| format!("Invalid address '{}': {}", parts[1], e))?;
870
871        Ok((node_id, addr))
872    }
873
874    /// Handles the CLUSTER DISCONNECT command.
875    fn handle_cluster_disconnect(&self) -> CommandResult {
876        let is_active = self.router.read().is_cluster_active();
877        if !is_active {
878            return CommandResult::Error("Not connected to cluster".to_string());
879        }
880
881        let result = self.router.write().shutdown_cluster();
882        match result {
883            Ok(()) => CommandResult::Output(format!(
884                "{} Disconnected from cluster",
885                style::styled(self.icons.success, self.config.theme.success)
886            )),
887            Err(e) => CommandResult::Error(format!("Failed to disconnect: {e}")),
888        }
889    }
890
891    /// Lists all tables in the database.
892    fn list_tables(&self) -> CommandResult {
893        self.router
894            .read()
895            .execute_parsed("SHOW TABLES")
896            .map_or_else(
897                |_| CommandResult::Output("No tables found.".to_string()),
898                |result| {
899                    CommandResult::Output(output::format_result(
900                        &result,
901                        &self.config.theme,
902                        self.icons,
903                    ))
904                },
905            )
906    }
907
908    /// Returns the help text.
909    #[must_use]
910    pub fn help_text() -> String {
911        output::format_help(&Theme::auto())
912    }
913
914    /// Processes a command result and returns whether to continue the loop.
915    #[must_use]
916    pub fn process_result(result: &CommandResult) -> LoopAction {
917        match result {
918            CommandResult::Output(text) | CommandResult::Help(text) => {
919                println!("{text}");
920                LoopAction::Continue
921            },
922            CommandResult::Error(text) => {
923                eprintln!("{text}");
924                LoopAction::Continue
925            },
926            CommandResult::Exit => {
927                println!("{}", progress::goodbye_message(&Theme::auto()));
928                LoopAction::Exit
929            },
930            CommandResult::Empty => LoopAction::Continue,
931        }
932    }
933
934    /// Returns the shell version string.
935    #[must_use]
936    pub const fn version() -> &'static str {
937        env!("CARGO_PKG_VERSION")
938    }
939
940    /// Executes a single line of input and returns a result.
941    ///
942    /// This is the primary entry point for non-interactive execution.
943    ///
944    /// # Errors
945    ///
946    /// Returns an error string if the command fails.
947    pub fn execute_line(&mut self, line: &str) -> Result<String, String> {
948        match self.execute(line) {
949            CommandResult::Output(text) | CommandResult::Help(text) => Ok(text),
950            CommandResult::Empty | CommandResult::Exit => Ok(String::new()),
951            CommandResult::Error(e) => Err(e),
952        }
953    }
954
955    /// Runs the interactive shell loop.
956    ///
957    /// # Errors
958    ///
959    /// Returns an error if readline initialization fails.
960    pub fn run(&mut self) -> Result<(), ShellError> {
961        let helper = NeumannHelper::new(self.config.theme.clone());
962        let mut editor: Editor<NeumannHelper, DefaultHistory> =
963            Editor::new().map_err(|e| ShellError::Init(e.to_string()))?;
964        editor.set_helper(Some(helper));
965
966        let editor = Arc::new(Mutex::new(editor));
967
968        {
969            let mut ed = editor.lock();
970            if let Some(ref path) = self.config.history_file {
971                let _ = ed.load_history(path);
972            }
973            ed.history_mut()
974                .set_max_len(self.config.history_size)
975                .map_err(|e| ShellError::Init(e.to_string()))?;
976        }
977
978        // Set up confirmation handler if checkpoint is available
979        {
980            let router = self.router.read();
981            if router.has_checkpoint() {
982                let handler = Arc::new(ShellConfirmationHandler::new(Arc::clone(&editor)));
983                drop(router);
984                let router = self.router.write();
985                if let Err(e) = router.set_confirmation_handler(handler) {
986                    eprintln!("Warning: Failed to set confirmation handler: {e}");
987                }
988            }
989        }
990
991        // Show welcome banner
992        let banner = if progress::supports_full_banner() {
993            progress::welcome_banner(Self::version(), &self.config.theme)
994        } else {
995            progress::compact_banner(Self::version(), &self.config.theme)
996        };
997        println!("{banner}");
998
999        loop {
1000            let readline_result = {
1001                let mut ed = editor.lock();
1002                ed.readline(&self.config.prompt)
1003            };
1004
1005            match readline_result {
1006                Ok(line) => {
1007                    if !line.trim().is_empty() {
1008                        let mut ed = editor.lock();
1009                        let _ = ed.add_history_entry(line.trim());
1010                    }
1011                    if Self::process_result(&self.execute(&line)) == LoopAction::Exit {
1012                        break;
1013                    }
1014                },
1015                Err(ReadlineError::Interrupted) => println!("^C"),
1016                Err(ReadlineError::Eof) => {
1017                    println!("{}", progress::goodbye_message(&self.config.theme));
1018                    break;
1019                },
1020                Err(err) => {
1021                    eprintln!("Error: {err}");
1022                    break;
1023                },
1024            }
1025        }
1026
1027        if let Some(ref path) = self.config.history_file {
1028            let mut ed = editor.lock();
1029            let _ = ed.save_history(path);
1030        }
1031        Ok(())
1032    }
1033}
1034
1035/// Action to take after processing a command.
1036#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1037pub enum LoopAction {
1038    /// Continue the shell loop.
1039    Continue,
1040    /// Exit the shell.
1041    Exit,
1042}
1043
1044impl Default for Shell {
1045    fn default() -> Self {
1046        Self::new()
1047    }
1048}
1049
1050/// Errors that can occur in the shell.
1051#[derive(Debug, Clone, PartialEq, Eq)]
1052pub enum ShellError {
1053    /// Failed to initialize readline.
1054    Init(String),
1055}
1056
1057impl std::fmt::Display for ShellError {
1058    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1059        match self {
1060            Self::Init(msg) => write!(f, "Shell initialization failed: {msg}"),
1061        }
1062    }
1063}
1064
1065impl std::error::Error for ShellError {}
1066
1067#[cfg(test)]
1068mod tests {
1069    use super::*;
1070
1071    #[test]
1072    fn test_shell_creation() {
1073        let shell = Shell::new();
1074        assert!(!shell.config.prompt.is_empty());
1075    }
1076
1077    #[test]
1078    fn test_shell_with_config() {
1079        let config = ShellConfig {
1080            history_file: None,
1081            history_size: 500,
1082            prompt: "neumann> ".to_string(),
1083            theme: Theme::plain(),
1084            no_color: false,
1085            no_boot: false,
1086            quiet: false,
1087        };
1088        let shell = Shell::with_config(config);
1089        assert_eq!(shell.config.prompt, "neumann> ");
1090        assert_eq!(shell.config.history_size, 500);
1091    }
1092
1093    #[test]
1094    fn test_shell_with_no_color() {
1095        let config = ShellConfig {
1096            no_color: true,
1097            ..Default::default()
1098        };
1099        let shell = Shell::with_config(config);
1100        // When no_color is set, prompt should be plain
1101        assert_eq!(shell.config.prompt, "neumann> ");
1102    }
1103
1104    #[test]
1105    fn test_execute_line() {
1106        let mut shell = Shell::new();
1107        let result = shell.execute_line("SELECT 1");
1108        assert!(result.is_ok() || result.is_err()); // Just verify it runs
1109    }
1110
1111    #[test]
1112    fn test_execute_line_help() {
1113        let mut shell = Shell::new();
1114        let result = shell.execute_line("help");
1115        assert!(result.is_ok());
1116        if let Ok(text) = result {
1117            assert!(text.contains("Commands"));
1118        }
1119    }
1120
1121    #[test]
1122    fn test_execute_line_exit() {
1123        let mut shell = Shell::new();
1124        let result = shell.execute_line("exit");
1125        assert!(result.is_ok());
1126        assert_eq!(result.unwrap(), "");
1127    }
1128
1129    #[test]
1130    fn test_execute_line_empty() {
1131        let mut shell = Shell::new();
1132        let result = shell.execute_line("");
1133        assert!(result.is_ok());
1134        assert_eq!(result.unwrap(), "");
1135    }
1136
1137    #[test]
1138    fn test_empty_input() {
1139        let mut shell = Shell::new();
1140        assert_eq!(shell.execute(""), CommandResult::Empty);
1141        assert_eq!(shell.execute("   "), CommandResult::Empty);
1142        assert_eq!(shell.execute("\t\n"), CommandResult::Empty);
1143    }
1144
1145    #[test]
1146    fn test_exit_commands() {
1147        let mut shell = Shell::new();
1148        assert_eq!(shell.execute("exit"), CommandResult::Exit);
1149        assert_eq!(shell.execute("quit"), CommandResult::Exit);
1150        assert_eq!(shell.execute("\\q"), CommandResult::Exit);
1151        assert_eq!(shell.execute("EXIT"), CommandResult::Exit);
1152        assert_eq!(shell.execute("QUIT"), CommandResult::Exit);
1153    }
1154
1155    #[test]
1156    fn test_help_commands() {
1157        let mut shell = Shell::new();
1158        let result = shell.execute("help");
1159        assert!(matches!(result, CommandResult::Help(_)));
1160
1161        if let CommandResult::Help(text) = result {
1162            assert!(text.contains("Commands"));
1163            assert!(text.contains("SELECT"));
1164        }
1165    }
1166
1167    #[test]
1168    fn test_help_backslash() {
1169        let mut shell = Shell::new();
1170        let result = shell.execute("\\h");
1171        assert!(matches!(result, CommandResult::Help(_)));
1172    }
1173
1174    #[test]
1175    fn test_help_question_mark() {
1176        let mut shell = Shell::new();
1177        let result = shell.execute("\\?");
1178        assert!(matches!(result, CommandResult::Help(_)));
1179    }
1180
1181    #[test]
1182    fn test_clear_command() {
1183        let mut shell = Shell::new();
1184        let result = shell.execute("clear");
1185        assert!(matches!(result, CommandResult::Output(_)));
1186        if let CommandResult::Output(text) = result {
1187            assert!(text.contains("\x1B[2J"));
1188        }
1189    }
1190
1191    #[test]
1192    fn test_clear_backslash() {
1193        let mut shell = Shell::new();
1194        let result = shell.execute("\\c");
1195        assert!(matches!(result, CommandResult::Output(_)));
1196    }
1197
1198    #[test]
1199    fn test_tables_command() {
1200        let mut shell = Shell::new();
1201        let result = shell.execute("tables");
1202        // Should work even with no tables
1203        assert!(
1204            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
1205        );
1206    }
1207
1208    #[test]
1209    fn test_tables_backslash() {
1210        let mut shell = Shell::new();
1211        let result = shell.execute("\\dt");
1212        assert!(
1213            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
1214        );
1215    }
1216
1217    #[test]
1218    fn test_wal_status_not_active() {
1219        let shell = Shell::new();
1220        let result = shell.handle_wal_status();
1221        if let CommandResult::Output(msg) = result {
1222            assert!(msg.contains("not active"));
1223        } else {
1224            panic!("Expected Output");
1225        }
1226    }
1227
1228    #[test]
1229    fn test_wal_truncate_not_active() {
1230        let shell = Shell::new();
1231        let result = shell.handle_wal_truncate();
1232        assert!(matches!(result, CommandResult::Error(_)));
1233    }
1234
1235    #[test]
1236    fn test_wal_status_command() {
1237        let mut shell = Shell::new();
1238        let result = shell.execute("wal status");
1239        assert!(matches!(result, CommandResult::Output(_)));
1240    }
1241
1242    #[test]
1243    fn test_wal_truncate_command() {
1244        let mut shell = Shell::new();
1245        let result = shell.execute("wal truncate");
1246        assert!(matches!(result, CommandResult::Error(_)));
1247    }
1248
1249    #[test]
1250    fn test_execute_doctor_command() {
1251        let mut shell = Shell::new();
1252        let result = shell.execute("doctor");
1253        assert!(matches!(result, CommandResult::Output(_)));
1254    }
1255
1256    #[test]
1257    fn test_doctor_output_contains_storage() {
1258        let mut shell = Shell::new();
1259        if let CommandResult::Output(s) = shell.execute("doctor") {
1260            let lower = s.to_lowercase();
1261            assert!(lower.contains("storage"));
1262        }
1263    }
1264
1265    #[test]
1266    fn test_doctor_case_insensitive() {
1267        let mut shell = Shell::new();
1268        let result = shell.execute("DOCTOR");
1269        assert!(matches!(result, CommandResult::Output(_)));
1270    }
1271
1272    #[test]
1273    fn test_is_write_command() {
1274        assert!(Shell::is_write_command("INSERT INTO users VALUES (1)"));
1275        assert!(Shell::is_write_command("UPDATE users SET name = 'x'"));
1276        assert!(Shell::is_write_command("DELETE FROM users"));
1277        assert!(Shell::is_write_command("CREATE TABLE test (id INT)"));
1278        assert!(Shell::is_write_command("DROP TABLE test"));
1279        assert!(Shell::is_write_command("NODE CREATE person {}"));
1280        assert!(!Shell::is_write_command("SELECT * FROM users"));
1281        assert!(!Shell::is_write_command("NODE GET 1"));
1282        assert!(!Shell::is_write_command("SHOW TABLES"));
1283    }
1284
1285    #[test]
1286    fn test_is_write_command_checkpoint() {
1287        assert!(Shell::is_write_command("CHECKPOINT"));
1288        assert!(Shell::is_write_command("ROLLBACK"));
1289    }
1290
1291    #[test]
1292    fn test_is_write_command_edge() {
1293        assert!(Shell::is_write_command("EDGE CREATE knows 1 2"));
1294        assert!(!Shell::is_write_command("EDGE GET 1"));
1295    }
1296
1297    #[test]
1298    fn test_is_write_command_embed() {
1299        assert!(Shell::is_write_command("EMBED STORE 'key' [1,2,3]"));
1300        assert!(Shell::is_write_command("EMBED DELETE 'key'"));
1301        assert!(!Shell::is_write_command("EMBED GET 'key'"));
1302    }
1303
1304    #[test]
1305    fn test_is_write_command_vault() {
1306        assert!(Shell::is_write_command("VAULT SET 'key' 'value'"));
1307        assert!(Shell::is_write_command("VAULT DELETE 'key'"));
1308        assert!(Shell::is_write_command("VAULT ROTATE 'key'"));
1309        assert!(Shell::is_write_command("VAULT GRANT read 'key' TO 'user'"));
1310        assert!(Shell::is_write_command(
1311            "VAULT REVOKE read 'key' FROM 'user'"
1312        ));
1313        assert!(!Shell::is_write_command("VAULT GET 'key'"));
1314    }
1315
1316    #[test]
1317    fn test_is_write_command_cache() {
1318        assert!(Shell::is_write_command("CACHE CLEAR"));
1319        assert!(!Shell::is_write_command("CACHE GET 'key'"));
1320    }
1321
1322    #[test]
1323    fn test_is_write_command_blob() {
1324        assert!(Shell::is_write_command("BLOB PUT 'file.txt' content"));
1325        assert!(Shell::is_write_command("BLOB DELETE 'hash'"));
1326        assert!(Shell::is_write_command("BLOB LINK 'hash' 'artifact'"));
1327        assert!(Shell::is_write_command("BLOB UNLINK 'hash' 'artifact'"));
1328        assert!(Shell::is_write_command("BLOB TAG 'hash' 'tag'"));
1329        assert!(Shell::is_write_command("BLOB UNTAG 'hash' 'tag'"));
1330        assert!(Shell::is_write_command("BLOB GC"));
1331        assert!(Shell::is_write_command("BLOB REPAIR"));
1332        assert!(Shell::is_write_command("BLOB META SET 'hash' 'key' 'val'"));
1333        assert!(!Shell::is_write_command("BLOB GET 'hash'"));
1334    }
1335
1336    #[test]
1337    fn test_is_write_command_entity() {
1338        assert!(Shell::is_write_command("ENTITY CREATE type {}"));
1339        assert!(!Shell::is_write_command("ENTITY GET 1"));
1340    }
1341
1342    #[test]
1343    fn test_is_write_command_graph() {
1344        assert!(Shell::is_write_command("GRAPH BATCH CREATE"));
1345        assert!(Shell::is_write_command("GRAPH CONSTRAINT CREATE unique"));
1346        assert!(Shell::is_write_command("GRAPH CONSTRAINT DROP unique"));
1347        assert!(Shell::is_write_command("GRAPH INDEX CREATE idx"));
1348        assert!(Shell::is_write_command("GRAPH INDEX DROP idx"));
1349        assert!(!Shell::is_write_command("GRAPH ALGORITHM PAGERANK"));
1350    }
1351
1352    #[test]
1353    fn test_is_write_command_chain() {
1354        assert!(Shell::is_write_command("BEGIN CHAIN tx1"));
1355        assert!(Shell::is_write_command("COMMIT CHAIN"));
1356        assert!(!Shell::is_write_command("CHAIN HEIGHT"));
1357    }
1358
1359    #[test]
1360    fn test_is_write_command_other() {
1361        assert!(!Shell::is_write_command("SIMILAR [1,2,3] LIMIT 5"));
1362        assert!(!Shell::is_write_command("FIND pattern"));
1363    }
1364
1365    #[test]
1366    fn test_extract_path_quoted() {
1367        let path = Shell::extract_path("save 'test.bin'", "save");
1368        assert_eq!(path, Some("test.bin".to_string()));
1369    }
1370
1371    #[test]
1372    fn test_extract_path_double_quoted() {
1373        let path = Shell::extract_path("save \"test.bin\"", "save");
1374        assert_eq!(path, Some("test.bin".to_string()));
1375    }
1376
1377    #[test]
1378    fn test_extract_path_unquoted() {
1379        let path = Shell::extract_path("save test.bin", "save");
1380        assert_eq!(path, Some("test.bin".to_string()));
1381    }
1382
1383    #[test]
1384    fn test_extract_path_empty() {
1385        let path = Shell::extract_path("save", "save");
1386        assert_eq!(path, None);
1387    }
1388
1389    #[test]
1390    fn test_extract_path_empty_quotes() {
1391        let path = Shell::extract_path("save ''", "save");
1392        assert_eq!(path, None);
1393    }
1394
1395    #[test]
1396    fn test_extract_load_path_strict() {
1397        let result = Shell::extract_load_path_and_mode("LOAD 'data.bin'");
1398        assert_eq!(
1399            result,
1400            Some(("data.bin".to_string(), WalRecoveryMode::Strict))
1401        );
1402    }
1403
1404    #[test]
1405    fn test_extract_load_path_recover() {
1406        let result = Shell::extract_load_path_and_mode("LOAD 'data.bin' RECOVER");
1407        assert_eq!(
1408            result,
1409            Some(("data.bin".to_string(), WalRecoveryMode::Recover))
1410        );
1411    }
1412
1413    #[test]
1414    fn test_extract_load_path_lowercase() {
1415        let result = Shell::extract_load_path_and_mode("load data.bin");
1416        assert_eq!(
1417            result,
1418            Some(("data.bin".to_string(), WalRecoveryMode::Strict))
1419        );
1420    }
1421
1422    #[test]
1423    fn test_extract_load_path_lowercase_recover() {
1424        let result = Shell::extract_load_path_and_mode("load 'data.bin' recover");
1425        assert_eq!(
1426            result,
1427            Some(("data.bin".to_string(), WalRecoveryMode::Recover))
1428        );
1429    }
1430
1431    #[test]
1432    fn test_extract_load_path_empty() {
1433        let result = Shell::extract_load_path_and_mode("LOAD");
1434        assert_eq!(result, None);
1435    }
1436
1437    #[test]
1438    fn test_extract_load_path_only_recover() {
1439        let result = Shell::extract_load_path_and_mode("LOAD RECOVER");
1440        assert_eq!(result, None);
1441    }
1442
1443    #[test]
1444    fn test_extract_load_path_empty_after_recover() {
1445        let result = Shell::extract_load_path_and_mode("LOAD '' RECOVER");
1446        assert_eq!(result, None);
1447    }
1448
1449    #[test]
1450    fn test_parse_node_address_valid() {
1451        let result = Shell::parse_node_address("node1@127.0.0.1:8080");
1452        assert!(result.is_ok());
1453        let (id, addr) = result.unwrap();
1454        assert_eq!(id, "node1");
1455        assert_eq!(addr.port(), 8080);
1456    }
1457
1458    #[test]
1459    fn test_parse_node_address_invalid() {
1460        let result = Shell::parse_node_address("invalid");
1461        assert!(result.is_err());
1462    }
1463
1464    #[test]
1465    fn test_parse_node_address_invalid_port() {
1466        let result = Shell::parse_node_address("node1@127.0.0.1:invalid");
1467        assert!(result.is_err());
1468    }
1469
1470    #[test]
1471    fn test_parse_node_address_ipv6() {
1472        let result = Shell::parse_node_address("node1@[::1]:8080");
1473        assert!(result.is_ok());
1474        let (id, addr) = result.unwrap();
1475        assert_eq!(id, "node1");
1476        assert_eq!(addr.port(), 8080);
1477    }
1478
1479    #[test]
1480    fn test_version() {
1481        let version = Shell::version();
1482        assert!(!version.is_empty());
1483    }
1484
1485    #[test]
1486    fn test_default_config() {
1487        let config = ShellConfig::default();
1488        assert_eq!(config.history_size, 1000);
1489        // Prompt contains "neumann>" with ANSI color codes
1490        assert!(config.prompt.contains("neumann>"));
1491    }
1492
1493    #[test]
1494    fn test_loop_action() {
1495        assert_eq!(
1496            Shell::process_result(&CommandResult::Empty),
1497            LoopAction::Continue
1498        );
1499        assert_eq!(
1500            Shell::process_result(&CommandResult::Exit),
1501            LoopAction::Exit
1502        );
1503    }
1504
1505    #[test]
1506    fn test_loop_action_output() {
1507        assert_eq!(
1508            Shell::process_result(&CommandResult::Output("test".to_string())),
1509            LoopAction::Continue
1510        );
1511    }
1512
1513    #[test]
1514    fn test_loop_action_error() {
1515        assert_eq!(
1516            Shell::process_result(&CommandResult::Error("error".to_string())),
1517            LoopAction::Continue
1518        );
1519    }
1520
1521    #[test]
1522    fn test_loop_action_help() {
1523        assert_eq!(
1524            Shell::process_result(&CommandResult::Help("help".to_string())),
1525            LoopAction::Continue
1526        );
1527    }
1528
1529    #[test]
1530    fn test_shell_error_display() {
1531        let err = ShellError::Init("test error".to_string());
1532        let display = format!("{err}");
1533        assert!(display.contains("test error"));
1534    }
1535
1536    #[test]
1537    fn test_shell_error_is_error() {
1538        let err = ShellError::Init("test".to_string());
1539        assert!(std::error::Error::source(&err).is_none());
1540    }
1541
1542    #[test]
1543    fn test_create_table_and_query() {
1544        let mut shell = Shell::new();
1545
1546        let result = shell.execute("CREATE TABLE test_users (id INT, name TEXT)");
1547        assert!(matches!(result, CommandResult::Output(_)));
1548
1549        let result = shell.execute("INSERT INTO test_users VALUES (1, 'Alice')");
1550        assert!(matches!(result, CommandResult::Output(_)));
1551
1552        let result = shell.execute("SELECT * FROM test_users");
1553        if let CommandResult::Output(text) = result {
1554            assert!(text.contains("Alice") || text.contains('1'));
1555        }
1556    }
1557
1558    #[test]
1559    fn test_node_operations() {
1560        let mut shell = Shell::new();
1561
1562        let result = shell.execute("NODE CREATE person {name: 'Bob', age: 30}");
1563        assert!(matches!(result, CommandResult::Output(_)));
1564
1565        let result = shell.execute("NODE LIST person");
1566        assert!(matches!(result, CommandResult::Output(_)));
1567    }
1568
1569    #[test]
1570    fn test_embed_operations() {
1571        let mut shell = Shell::new();
1572
1573        let result = shell.execute("EMBED STORE 'doc1' [0.1, 0.2, 0.3, 0.4]");
1574        assert!(matches!(result, CommandResult::Output(_)));
1575
1576        let result = shell.execute("EMBED GET 'doc1'");
1577        assert!(matches!(result, CommandResult::Output(_)));
1578    }
1579
1580    #[test]
1581    fn test_router_access() {
1582        let shell = Shell::new();
1583        let _router = shell.router();
1584        let _router_arc = shell.router_arc();
1585    }
1586
1587    #[test]
1588    fn test_router_mut_access() {
1589        let shell = Shell::new();
1590        let _router = shell.router_mut();
1591    }
1592
1593    #[test]
1594    fn test_help_text() {
1595        let help = Shell::help_text();
1596        assert!(help.contains("Commands"));
1597    }
1598
1599    #[test]
1600    fn test_default_shell() {
1601        let shell = Shell::default();
1602        assert!(!shell.config.prompt.is_empty());
1603    }
1604
1605    #[test]
1606    fn test_vault_init_no_env() {
1607        let shell = Shell::new();
1608        // Clear the env var if set
1609        std::env::remove_var("NEUMANN_VAULT_KEY");
1610        let result = shell.handle_vault_init();
1611        assert!(matches!(result, CommandResult::Error(_)));
1612        if let CommandResult::Error(msg) = result {
1613            assert!(msg.contains("NEUMANN_VAULT_KEY"));
1614        }
1615    }
1616
1617    #[test]
1618    fn test_vault_init_invalid_base64() {
1619        let shell = Shell::new();
1620        std::env::set_var("NEUMANN_VAULT_KEY", "not-valid-base64!!!");
1621        let result = shell.handle_vault_init();
1622        std::env::remove_var("NEUMANN_VAULT_KEY");
1623        assert!(matches!(result, CommandResult::Error(_)));
1624    }
1625
1626    #[test]
1627    fn test_vault_identity_get() {
1628        let shell = Shell::new();
1629        let result = shell.handle_vault_identity("vault identity");
1630        assert!(matches!(result, CommandResult::Output(_)));
1631        if let CommandResult::Output(msg) = result {
1632            assert!(msg.contains("identity"));
1633        }
1634    }
1635
1636    #[test]
1637    fn test_vault_identity_set() {
1638        let shell = Shell::new();
1639        let result = shell.handle_vault_identity("vault identity alice");
1640        assert!(matches!(result, CommandResult::Output(_)));
1641        if let CommandResult::Output(msg) = result {
1642            assert!(msg.contains("alice"));
1643        }
1644    }
1645
1646    #[test]
1647    fn test_vault_identity_set_quoted() {
1648        let shell = Shell::new();
1649        let result = shell.handle_vault_identity("vault identity 'bob'");
1650        assert!(matches!(result, CommandResult::Output(_)));
1651        if let CommandResult::Output(msg) = result {
1652            assert!(msg.contains("bob"));
1653        }
1654    }
1655
1656    #[test]
1657    fn test_cache_init() {
1658        let shell = Shell::new();
1659        let result = shell.handle_cache_init();
1660        // May succeed or fail depending on internal state
1661        assert!(
1662            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
1663        );
1664    }
1665
1666    #[test]
1667    fn test_cluster_connect_no_args() {
1668        let shell = Shell::new();
1669        let result = shell.handle_cluster_connect("cluster connect");
1670        assert!(matches!(result, CommandResult::Error(_)));
1671    }
1672
1673    #[test]
1674    fn test_cluster_connect_invalid_address() {
1675        let shell = Shell::new();
1676        let result = shell.handle_cluster_connect("cluster connect 'invalid'");
1677        assert!(matches!(result, CommandResult::Error(_)));
1678    }
1679
1680    #[test]
1681    fn test_cluster_disconnect_not_connected() {
1682        let shell = Shell::new();
1683        let result = shell.handle_cluster_disconnect();
1684        assert!(matches!(result, CommandResult::Error(_)));
1685        if let CommandResult::Error(msg) = result {
1686            assert!(msg.contains("Not connected"));
1687        }
1688    }
1689
1690    #[test]
1691    fn test_format_wal_replay_result_empty() {
1692        let result = WalReplayResult {
1693            replayed: 0,
1694            errors: vec![],
1695        };
1696        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Strict);
1697        assert!(formatted.is_empty());
1698    }
1699
1700    #[test]
1701    fn test_format_wal_replay_result_with_replayed() {
1702        let result = WalReplayResult {
1703            replayed: 5,
1704            errors: vec![],
1705        };
1706        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Strict);
1707        assert!(formatted.contains("Replayed 5 commands"));
1708    }
1709
1710    #[test]
1711    fn test_format_wal_replay_result_with_errors() {
1712        let result = WalReplayResult {
1713            replayed: 3,
1714            errors: vec![
1715                WalReplayError::new(1, "cmd1", "error1".to_string()),
1716                WalReplayError::new(2, "cmd2", "error2".to_string()),
1717            ],
1718        };
1719        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Recover);
1720        assert!(formatted.contains("Replayed 3 commands"));
1721        assert!(formatted.contains("Skipped 2"));
1722        assert!(formatted.contains("Line 1"));
1723    }
1724
1725    #[test]
1726    fn test_format_wal_replay_result_many_errors() {
1727        let mut errors = Vec::new();
1728        for i in 1..=10 {
1729            errors.push(WalReplayError::new(
1730                i,
1731                &format!("cmd{i}"),
1732                "error".to_string(),
1733            ));
1734        }
1735        let result = WalReplayResult {
1736            replayed: 0,
1737            errors,
1738        };
1739        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Recover);
1740        assert!(formatted.contains("and 5 more"));
1741    }
1742
1743    #[test]
1744    fn test_format_wal_replay_result_strict_mode_ignores_errors() {
1745        let result = WalReplayResult {
1746            replayed: 3,
1747            errors: vec![WalReplayError::new(1, "cmd1", "error1".to_string())],
1748        };
1749        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Strict);
1750        assert!(!formatted.contains("Skipped"));
1751    }
1752
1753    #[test]
1754    fn test_detect_embedding_dimension_empty_store() {
1755        let store = TensorStore::new();
1756        let dim = Shell::detect_embedding_dimension(&store);
1757        assert_eq!(dim, tensor_compress::CompressionDefaults::STANDARD);
1758    }
1759
1760    #[test]
1761    fn test_detect_embedding_dimension_with_vector() {
1762        let store = TensorStore::new();
1763        let mut data = tensor_store::TensorData::new();
1764        data.set(
1765            "embedding",
1766            tensor_store::TensorValue::Vector(vec![0.1, 0.2, 0.3, 0.4]),
1767        );
1768        store.put("test_key", data).unwrap();
1769
1770        let dim = Shell::detect_embedding_dimension(&store);
1771        assert_eq!(dim, 4);
1772    }
1773
1774    #[test]
1775    fn test_detect_embedding_dimension_with_sparse() {
1776        let store = TensorStore::new();
1777        let mut data = tensor_store::TensorData::new();
1778        let sparse = tensor_store::SparseVector::new(100);
1779        data.set("embedding", tensor_store::TensorValue::Sparse(sparse));
1780        store.put("test_key", data).unwrap();
1781
1782        let dim = Shell::detect_embedding_dimension(&store);
1783        assert_eq!(dim, 100);
1784    }
1785
1786    #[test]
1787    fn test_save_invalid_path() {
1788        let shell = Shell::new();
1789        let result = shell.handle_save("save");
1790        assert!(matches!(result, CommandResult::Error(_)));
1791    }
1792
1793    #[test]
1794    fn test_save_compressed_invalid_path() {
1795        let shell = Shell::new();
1796        let result = shell.handle_save_compressed("save compressed");
1797        assert!(matches!(result, CommandResult::Error(_)));
1798    }
1799
1800    #[test]
1801    fn test_load_invalid_path() {
1802        let shell = Shell::new();
1803        let result = shell.handle_load("load");
1804        assert!(matches!(result, CommandResult::Error(_)));
1805    }
1806
1807    #[test]
1808    fn test_execute_via_command() {
1809        let mut shell = Shell::new();
1810
1811        // Test vault init command
1812        let result = shell.execute("vault init");
1813        assert!(matches!(result, CommandResult::Error(_)));
1814
1815        // Test vault identity command
1816        let result = shell.execute("vault identity test_user");
1817        assert!(matches!(result, CommandResult::Output(_)));
1818
1819        // Test cache init command
1820        let result = shell.execute("cache init");
1821        assert!(
1822            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
1823        );
1824
1825        // Test cluster connect command
1826        let result = shell.execute("cluster connect");
1827        assert!(matches!(result, CommandResult::Error(_)));
1828
1829        // Test cluster disconnect command
1830        let result = shell.execute("cluster disconnect");
1831        assert!(matches!(result, CommandResult::Error(_)));
1832    }
1833
1834    #[test]
1835    fn test_dirs_home() {
1836        // This tests the dirs_home function indirectly via ShellConfig
1837        let original = std::env::var_os("HOME");
1838
1839        std::env::set_var("HOME", "/test/home");
1840        let result = dirs_home();
1841        assert_eq!(result, Some(PathBuf::from("/test/home")));
1842
1843        std::env::remove_var("HOME");
1844        let result = dirs_home();
1845        assert!(result.is_none());
1846
1847        // Restore original
1848        if let Some(home) = original {
1849            std::env::set_var("HOME", home);
1850        }
1851    }
1852
1853    #[test]
1854    fn test_router_executor() {
1855        let router = Arc::new(RwLock::new(QueryRouter::new()));
1856        let executor = RouterExecutor(router);
1857
1858        // Test execute
1859        let result = executor.execute("SHOW TABLES");
1860        assert!(result.is_ok() || result.is_err());
1861    }
1862
1863    #[test]
1864    fn test_shell_config_quiet() {
1865        let config = ShellConfig {
1866            quiet: true,
1867            ..Default::default()
1868        };
1869        assert!(config.quiet);
1870    }
1871
1872    #[test]
1873    fn test_shell_config_no_boot() {
1874        let config = ShellConfig {
1875            no_boot: true,
1876            ..Default::default()
1877        };
1878        let shell = Shell::with_config(config);
1879        // Verify config was applied
1880        assert!(shell.config.no_boot);
1881    }
1882
1883    #[test]
1884    fn test_command_result_equality() {
1885        assert_eq!(CommandResult::Empty, CommandResult::Empty);
1886        assert_eq!(CommandResult::Exit, CommandResult::Exit);
1887        assert_eq!(
1888            CommandResult::Output("test".to_string()),
1889            CommandResult::Output("test".to_string())
1890        );
1891        assert_ne!(
1892            CommandResult::Output("a".to_string()),
1893            CommandResult::Output("b".to_string())
1894        );
1895    }
1896
1897    #[test]
1898    fn test_cluster_connect_with_peers() {
1899        let shell = Shell::new();
1900        // Test with peer addresses in various formats
1901        let result = shell.handle_cluster_connect(
1902            "cluster connect 'node1@127.0.0.1:8080' 'node2@127.0.0.1:8081'",
1903        );
1904        // Should error because we can't actually connect in tests
1905        assert!(matches!(result, CommandResult::Error(_)));
1906    }
1907
1908    #[test]
1909    fn test_cluster_connect_double_quoted() {
1910        let shell = Shell::new();
1911        let result = shell.handle_cluster_connect("cluster connect \"node1@127.0.0.1:8080\"");
1912        assert!(matches!(result, CommandResult::Error(_)));
1913    }
1914
1915    #[test]
1916    fn test_cluster_connect_mixed_quotes() {
1917        let shell = Shell::new();
1918        let result = shell.handle_cluster_connect(
1919            "cluster connect 'node1@127.0.0.1:8080', \"node2@127.0.0.1:8081\"",
1920        );
1921        assert!(matches!(result, CommandResult::Error(_)));
1922    }
1923
1924    #[test]
1925    fn test_execute_save_command() {
1926        let mut shell = Shell::new();
1927        let result = shell.execute("SAVE 'test_snapshot.bin'");
1928        // Will error or succeed depending on permissions
1929        assert!(
1930            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
1931        );
1932        // Clean up if file was created
1933        let _ = std::fs::remove_file("test_snapshot.bin");
1934    }
1935
1936    #[test]
1937    fn test_execute_save_compressed_command() {
1938        let mut shell = Shell::new();
1939        let result = shell.execute("SAVE COMPRESSED 'test_compressed.bin'");
1940        assert!(
1941            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
1942        );
1943        let _ = std::fs::remove_file("test_compressed.bin");
1944    }
1945
1946    #[test]
1947    fn test_execute_load_nonexistent() {
1948        let mut shell = Shell::new();
1949        let result = shell.execute("LOAD 'nonexistent_file_12345.bin'");
1950        assert!(matches!(result, CommandResult::Error(_)));
1951    }
1952
1953    #[test]
1954    fn test_execute_load_with_recover() {
1955        let mut shell = Shell::new();
1956        let result = shell.execute("LOAD 'nonexistent_file_12345.bin' RECOVER");
1957        assert!(matches!(result, CommandResult::Error(_)));
1958    }
1959
1960    #[test]
1961    fn test_command_result_debug() {
1962        let result = CommandResult::Output("test".to_string());
1963        let debug_str = format!("{result:?}");
1964        assert!(debug_str.contains("Output"));
1965    }
1966
1967    #[test]
1968    fn test_command_result_clone() {
1969        let result = CommandResult::Error("error".to_string());
1970        let cloned = result.clone();
1971        assert_eq!(result, cloned);
1972    }
1973
1974    #[test]
1975    fn test_shell_config_debug() {
1976        let config = ShellConfig::default();
1977        let debug_str = format!("{config:?}");
1978        assert!(debug_str.contains("history_size"));
1979    }
1980
1981    #[test]
1982    fn test_shell_config_clone() {
1983        let config = ShellConfig::default();
1984        let cloned = config.clone();
1985        assert_eq!(config.history_size, cloned.history_size);
1986    }
1987
1988    #[test]
1989    fn test_shell_error_clone() {
1990        let err = ShellError::Init("test".to_string());
1991        let cloned = err.clone();
1992        assert_eq!(err, cloned);
1993    }
1994
1995    #[test]
1996    fn test_shell_error_debug() {
1997        let err = ShellError::Init("test".to_string());
1998        let debug_str = format!("{err:?}");
1999        assert!(debug_str.contains("Init"));
2000    }
2001
2002    #[test]
2003    fn test_loop_action_debug() {
2004        let action = LoopAction::Continue;
2005        let debug_str = format!("{action:?}");
2006        assert!(debug_str.contains("Continue"));
2007    }
2008
2009    #[test]
2010    fn test_loop_action_clone() {
2011        let action = LoopAction::Exit;
2012        let cloned = action;
2013        assert_eq!(action, cloned);
2014    }
2015
2016    #[test]
2017    fn test_extract_load_path_double_quoted() {
2018        let result = Shell::extract_load_path_and_mode("LOAD \"data.bin\"");
2019        assert_eq!(
2020            result,
2021            Some(("data.bin".to_string(), WalRecoveryMode::Strict))
2022        );
2023    }
2024
2025    #[test]
2026    fn test_extract_load_path_unquoted_recover() {
2027        let result = Shell::extract_load_path_and_mode("LOAD data.bin RECOVER");
2028        assert_eq!(
2029            result,
2030            Some(("data.bin".to_string(), WalRecoveryMode::Recover))
2031        );
2032    }
2033
2034    #[test]
2035    fn test_extract_path_with_spaces() {
2036        // Quoted path with spaces
2037        let path = Shell::extract_path("save 'path with spaces.bin'", "save");
2038        assert_eq!(path, Some("path with spaces.bin".to_string()));
2039    }
2040
2041    #[test]
2042    fn test_vault_init_valid_key() {
2043        let shell = Shell::new();
2044        // Set a valid base64 encoded 32-byte key
2045        let key = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0u8; 32]);
2046        std::env::set_var("NEUMANN_VAULT_KEY", &key);
2047        let result = shell.handle_vault_init();
2048        std::env::remove_var("NEUMANN_VAULT_KEY");
2049        // Should succeed
2050        assert!(matches!(result, CommandResult::Output(_)));
2051    }
2052
2053    #[test]
2054    fn test_execute_cluster_connect_command() {
2055        let mut shell = Shell::new();
2056        let result = shell.execute("CLUSTER CONNECT 'invalid'");
2057        assert!(matches!(result, CommandResult::Error(_)));
2058    }
2059
2060    #[test]
2061    fn test_execute_cluster_disconnect_command() {
2062        let mut shell = Shell::new();
2063        let result = shell.execute("CLUSTER DISCONNECT");
2064        assert!(matches!(result, CommandResult::Error(_)));
2065    }
2066
2067    #[test]
2068    fn test_handle_cluster_connect_quoted_peers() {
2069        let shell = Shell::new();
2070        // Multiple peers with different quote styles
2071        let result = shell.handle_cluster_connect(
2072            "CLUSTER CONNECT 'node1@127.0.0.1:8080', 'node2@127.0.0.1:8081', 'node3@127.0.0.1:8082'",
2073        );
2074        // Will fail to actually connect but should parse
2075        assert!(matches!(result, CommandResult::Error(_)));
2076    }
2077
2078    #[test]
2079    fn test_shell_config_with_all_defaults() {
2080        let config = ShellConfig::default();
2081        assert!(config.history_file.is_some());
2082        assert_eq!(config.history_size, 1000);
2083        assert!(config.prompt.contains("neumann"));
2084    }
2085
2086    #[test]
2087    fn test_shell_with_config_no_color_icons() {
2088        let config = ShellConfig {
2089            no_color: true,
2090            ..Default::default()
2091        };
2092        let shell = Shell::with_config(config);
2093        // icons should be plain
2094        assert_eq!(shell.icons.success, "[ok]");
2095    }
2096
2097    #[test]
2098    fn test_execute_with_wal_active() {
2099        let mut shell = Shell::new();
2100
2101        // Create a temp directory for the snapshot
2102        let temp_dir = std::env::temp_dir();
2103        let snapshot_path = temp_dir.join("test_shell_snapshot.bin");
2104        let snapshot_str = snapshot_path.to_string_lossy().to_string();
2105
2106        // Create some data first
2107        shell.execute("CREATE TABLE wal_test (id INT)");
2108
2109        // Save snapshot
2110        let result = shell.execute(&format!("SAVE '{snapshot_str}'"));
2111        assert!(
2112            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2113        );
2114
2115        // Clean up
2116        let _ = std::fs::remove_file(&snapshot_path);
2117        let _ = std::fs::remove_file(snapshot_path.with_extension("log"));
2118    }
2119
2120    #[test]
2121    fn test_list_tables_after_create() {
2122        let mut shell = Shell::new();
2123        shell.execute("CREATE TABLE test_list_table (id INT)");
2124        let result = shell.list_tables();
2125        assert!(
2126            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2127        );
2128    }
2129
2130    #[test]
2131    fn test_execute_invalid_syntax() {
2132        let mut shell = Shell::new();
2133        let result = shell.execute("@#$%^&*");
2134        assert!(matches!(result, CommandResult::Error(_)));
2135    }
2136
2137    #[test]
2138    fn test_execute_backslash_commands_case() {
2139        let mut shell = Shell::new();
2140
2141        // These should all work
2142        assert!(matches!(shell.execute("\\q"), CommandResult::Exit));
2143        assert!(matches!(shell.execute("\\h"), CommandResult::Help(_)));
2144        assert!(matches!(shell.execute("\\?"), CommandResult::Help(_)));
2145        assert!(matches!(shell.execute("\\c"), CommandResult::Output(_)));
2146        assert!(matches!(shell.execute("\\dt"), CommandResult::Output(_)));
2147    }
2148
2149    #[test]
2150    fn test_extract_load_path_edge_cases() {
2151        // Empty quotes
2152        assert_eq!(Shell::extract_load_path_and_mode("LOAD ''"), None);
2153        assert_eq!(Shell::extract_load_path_and_mode("LOAD \"\""), None);
2154
2155        // Just whitespace
2156        assert_eq!(Shell::extract_load_path_and_mode("load   "), None);
2157    }
2158
2159    #[test]
2160    fn test_extract_path_edge_cases() {
2161        // Whitespace-only after command
2162        assert_eq!(Shell::extract_path("save   ", "save"), None);
2163
2164        // Very long path
2165        let long_path = "a".repeat(1000);
2166        let result = Shell::extract_path(&format!("save {long_path}"), "save");
2167        assert_eq!(result, Some(long_path));
2168    }
2169
2170    #[test]
2171    fn test_parse_node_address_edge_cases() {
2172        // Empty node id
2173        let result = Shell::parse_node_address("@127.0.0.1:8080");
2174        assert!(result.is_ok());
2175
2176        // IPv6 localhost
2177        let result = Shell::parse_node_address("node@[::]:8080");
2178        assert!(result.is_ok());
2179    }
2180
2181    #[test]
2182    fn test_shell_error_eq() {
2183        let e1 = ShellError::Init("error1".to_string());
2184        let e2 = ShellError::Init("error1".to_string());
2185        let e3 = ShellError::Init("error2".to_string());
2186        assert_eq!(e1, e2);
2187        assert_ne!(e1, e3);
2188    }
2189
2190    #[test]
2191    fn test_command_result_all_variants_debug() {
2192        let variants = [
2193            CommandResult::Output("out".to_string()),
2194            CommandResult::Exit,
2195            CommandResult::Help("help".to_string()),
2196            CommandResult::Empty,
2197            CommandResult::Error("err".to_string()),
2198        ];
2199        for v in variants {
2200            let _ = format!("{v:?}");
2201        }
2202    }
2203
2204    #[test]
2205    fn test_handle_save_no_path() {
2206        let shell = Shell::new();
2207        let result = shell.handle_save("save");
2208        assert!(matches!(result, CommandResult::Error(_)));
2209    }
2210
2211    #[test]
2212    fn test_handle_save_compressed_no_path() {
2213        let shell = Shell::new();
2214        let result = shell.handle_save_compressed("save compressed");
2215        assert!(matches!(result, CommandResult::Error(_)));
2216    }
2217
2218    #[test]
2219    fn test_handle_load_no_path() {
2220        let shell = Shell::new();
2221        let result = shell.handle_load("load");
2222        assert!(matches!(result, CommandResult::Error(_)));
2223    }
2224
2225    #[test]
2226    fn test_handle_load_nonexistent() {
2227        let shell = Shell::new();
2228        let result = shell.handle_load("load 'nonexistent.bin'");
2229        assert!(matches!(result, CommandResult::Error(_)));
2230    }
2231
2232    #[test]
2233    fn test_shell_config_clone_2() {
2234        let config = ShellConfig::default();
2235        let cloned = config.clone();
2236        assert_eq!(config.history_size, cloned.history_size);
2237        assert_eq!(config.no_color, cloned.no_color);
2238        assert_eq!(config.no_boot, cloned.no_boot);
2239    }
2240
2241    #[test]
2242    fn test_shell_with_no_boot() {
2243        let config = ShellConfig {
2244            no_boot: true,
2245            ..Default::default()
2246        };
2247        let shell = Shell::with_config(config);
2248        // Verify config was applied
2249        assert!(shell.config.no_boot);
2250    }
2251
2252    #[test]
2253    fn test_list_tables_empty() {
2254        let shell = Shell::new();
2255        let result = shell.list_tables();
2256        // Should work with no tables
2257        assert!(
2258            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2259        );
2260    }
2261
2262    #[test]
2263    fn test_extract_load_with_recover_keyword() {
2264        // Recovery mode variants
2265        let result = Shell::extract_load_path_and_mode("load 'file.bin' RECOVER");
2266        assert!(result.is_some());
2267        let (_, mode) = result.unwrap();
2268        assert_eq!(mode, WalRecoveryMode::Recover);
2269
2270        let result = Shell::extract_load_path_and_mode("LOAD file.bin RECOVER");
2271        assert!(result.is_some());
2272        let (_, mode) = result.unwrap();
2273        assert_eq!(mode, WalRecoveryMode::Recover);
2274    }
2275
2276    #[test]
2277    fn test_wal_replay_result_debug() {
2278        let result = WalReplayResult {
2279            replayed: 5,
2280            errors: vec![],
2281        };
2282        let debug_str = format!("{result:?}");
2283        assert!(debug_str.contains("replayed"));
2284    }
2285
2286    #[test]
2287    fn test_wal_replay_result_clone() {
2288        let result = WalReplayResult {
2289            replayed: 10,
2290            errors: vec![WalReplayError::new(1, "cmd", "error".to_string())],
2291        };
2292        let cloned = result.clone();
2293        assert_eq!(cloned.replayed, result.replayed);
2294        assert_eq!(cloned.errors.len(), result.errors.len());
2295    }
2296
2297    #[test]
2298    fn test_wal_replay_error_debug() {
2299        let error = WalReplayError::new(5, "test command", "test error".to_string());
2300        let debug_str = format!("{error:?}");
2301        assert!(debug_str.contains("line"));
2302    }
2303
2304    #[test]
2305    fn test_wal_replay_error_clone() {
2306        let error = WalReplayError::new(10, "cmd", "err".to_string());
2307        let cloned = error.clone();
2308        assert_eq!(cloned.line, error.line);
2309        assert_eq!(cloned.command, error.command);
2310    }
2311
2312    #[test]
2313    fn test_wal_recovery_mode_debug() {
2314        let mode = WalRecoveryMode::Strict;
2315        let debug_str = format!("{mode:?}");
2316        assert!(debug_str.contains("Strict"));
2317
2318        let mode = WalRecoveryMode::Recover;
2319        let debug_str = format!("{mode:?}");
2320        assert!(debug_str.contains("Recover"));
2321    }
2322
2323    #[test]
2324    fn test_wal_recovery_mode_clone() {
2325        let mode = WalRecoveryMode::Recover;
2326        let cloned = mode;
2327        assert_eq!(cloned, mode);
2328    }
2329
2330    #[test]
2331    fn test_detect_embedding_dimension_no_vectors() {
2332        let store = TensorStore::new();
2333        let mut data = tensor_store::TensorData::new();
2334        data.set(
2335            "name",
2336            tensor_store::TensorValue::Scalar(tensor_store::ScalarValue::String(
2337                "test".to_string(),
2338            )),
2339        );
2340        store.put("test_key", data).unwrap();
2341
2342        // Should return default when no vectors found
2343        let dim = Shell::detect_embedding_dimension(&store);
2344        assert_eq!(dim, tensor_compress::CompressionDefaults::STANDARD);
2345    }
2346
2347    #[test]
2348    fn test_cluster_connect_unquoted_address() {
2349        let shell = Shell::new();
2350        let result = shell.handle_cluster_connect("cluster connect node1@127.0.0.1:8080");
2351        // Will error because we can't actually connect, but should parse the unquoted address
2352        assert!(matches!(result, CommandResult::Error(_)));
2353    }
2354
2355    #[test]
2356    fn test_handle_vault_identity_lowercase() {
2357        let shell = Shell::new();
2358        let result = shell.handle_vault_identity("VAULT IDENTITY test_user");
2359        assert!(matches!(result, CommandResult::Output(_)));
2360    }
2361
2362    #[test]
2363    fn test_execute_query_with_spinner() {
2364        let mut shell = Shell::new();
2365        // SIMILAR command should trigger spinner
2366        let result = shell.execute("SIMILAR [0.1, 0.2, 0.3] LIMIT 5");
2367        // May succeed or fail depending on store state
2368        assert!(
2369            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2370        );
2371    }
2372
2373    #[test]
2374    fn test_handle_save_to_temp_dir() {
2375        let shell = Shell::new();
2376        let temp_dir = std::env::temp_dir();
2377        let path = temp_dir.join("test_shell_save_temp.bin");
2378        let result = shell.handle_save(&format!("save '{}'", path.display()));
2379        // Should succeed writing to temp
2380        assert!(
2381            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2382        );
2383        // Clean up
2384        let _ = std::fs::remove_file(&path);
2385    }
2386
2387    #[test]
2388    fn test_handle_save_compressed_to_temp_dir() {
2389        let shell = Shell::new();
2390        let temp_dir = std::env::temp_dir();
2391        let path = temp_dir.join("test_shell_save_compressed_temp.bin");
2392        let result = shell.handle_save_compressed(&format!("save compressed '{}'", path.display()));
2393        assert!(
2394            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2395        );
2396        // Clean up
2397        let _ = std::fs::remove_file(&path);
2398    }
2399
2400    #[test]
2401    fn test_shell_error_partial_eq() {
2402        let e1 = ShellError::Init("same".to_string());
2403        let e2 = ShellError::Init("same".to_string());
2404        let e3 = ShellError::Init("different".to_string());
2405        assert_eq!(e1, e2);
2406        assert_ne!(e1, e3);
2407    }
2408
2409    #[test]
2410    fn test_loop_action_copy() {
2411        let action = LoopAction::Continue;
2412        let copied: LoopAction = action;
2413        assert_eq!(copied, LoopAction::Continue);
2414    }
2415
2416    #[test]
2417    fn test_format_wal_replay_result_exact_5_errors() {
2418        // Test with exactly 5 errors (boundary case)
2419        let errors: Vec<WalReplayError> = (1..=5)
2420            .map(|i| WalReplayError::new(i, &format!("cmd{i}"), "error".to_string()))
2421            .collect();
2422        let result = WalReplayResult {
2423            replayed: 0,
2424            errors,
2425        };
2426        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Recover);
2427        // Should NOT contain "and X more" since exactly 5 errors are shown
2428        assert!(!formatted.contains("and"));
2429    }
2430
2431    #[test]
2432    fn test_format_wal_replay_result_zero_replayed() {
2433        let result = WalReplayResult {
2434            replayed: 0,
2435            errors: vec![WalReplayError::new(1, "bad", "err".to_string())],
2436        };
2437        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Recover);
2438        // Should not contain "Replayed 0"
2439        assert!(!formatted.contains("Replayed 0"));
2440    }
2441
2442    #[test]
2443    fn test_execute_multiple_backslash_commands() {
2444        let mut shell = Shell::new();
2445        // Test each backslash command variant
2446        assert!(matches!(shell.execute("\\q"), CommandResult::Exit));
2447        assert!(matches!(shell.execute("\\h"), CommandResult::Help(_)));
2448        assert!(matches!(shell.execute("\\?"), CommandResult::Help(_)));
2449        assert!(matches!(shell.execute("\\c"), CommandResult::Output(_)));
2450        assert!(matches!(shell.execute("\\dt"), CommandResult::Output(_)));
2451    }
2452
2453    #[test]
2454    fn test_shell_default_trait() {
2455        let shell1 = Shell::default();
2456        let shell2 = Shell::new();
2457        // Both should have the same config defaults
2458        assert_eq!(shell1.config.history_size, shell2.config.history_size);
2459    }
2460
2461    #[test]
2462    fn test_parse_node_address_with_at_in_nodeid() {
2463        // Multiple @ should use splitn(2)
2464        let result = Shell::parse_node_address("node@name@127.0.0.1:8080");
2465        assert!(result.is_err());
2466    }
2467
2468    #[test]
2469    fn test_is_write_command_case_insensitive() {
2470        // Verify case insensitivity
2471        assert!(Shell::is_write_command("insert into test values (1)"));
2472        assert!(Shell::is_write_command("INSERT into test VALUES (1)"));
2473        assert!(Shell::is_write_command("INSERT INTO TEST VALUES (1)"));
2474    }
2475
2476    #[test]
2477    fn test_execute_with_trailing_whitespace() {
2478        let mut shell = Shell::new();
2479        let result = shell.execute("  help  ");
2480        assert!(matches!(result, CommandResult::Help(_)));
2481    }
2482
2483    #[test]
2484    fn test_cluster_connect_with_unquoted_comma_separated() {
2485        let shell = Shell::new();
2486        // Comma separated without quotes
2487        let result = shell
2488            .handle_cluster_connect("cluster connect node1@127.0.0.1:8080,node2@127.0.0.1:8081");
2489        assert!(matches!(result, CommandResult::Error(_)));
2490    }
2491
2492    #[test]
2493    fn test_save_and_load_cycle() {
2494        let mut shell = Shell::new();
2495        let temp_dir = std::env::temp_dir();
2496        let path = temp_dir.join("test_save_load_cycle.bin");
2497        let path_str = path.to_string_lossy().to_string();
2498
2499        // Create some data
2500        shell.execute("CREATE TABLE cycle_test (id INT, name TEXT)");
2501        shell.execute("INSERT INTO cycle_test VALUES (1, 'Alice')");
2502        shell.execute("INSERT INTO cycle_test VALUES (2, 'Bob')");
2503
2504        // Save
2505        let save_result = shell.execute(&format!("SAVE '{path_str}'"));
2506        assert!(
2507            matches!(save_result, CommandResult::Output(_)),
2508            "Save should succeed"
2509        );
2510
2511        // Create a new shell and load
2512        let mut shell2 = Shell::new();
2513        let load_result = shell2.execute(&format!("LOAD '{path_str}'"));
2514        // Load may succeed or fail based on file system permissions
2515        let loaded = matches!(load_result, CommandResult::Output(_));
2516
2517        // Clean up
2518        let _ = std::fs::remove_file(&path);
2519        let _ = std::fs::remove_file(path.with_extension("log"));
2520
2521        if loaded {
2522            // Verify data was loaded
2523            let result = shell2.execute("SELECT * FROM cycle_test");
2524            assert!(matches!(result, CommandResult::Output(_)));
2525        }
2526    }
2527
2528    #[test]
2529    fn test_save_and_load_compressed_cycle() {
2530        let mut shell = Shell::new();
2531        let temp_dir = std::env::temp_dir();
2532        let path = temp_dir.join("test_save_load_compressed.bin");
2533        let path_str = path.to_string_lossy().to_string();
2534
2535        // Create some data
2536        shell.execute("CREATE TABLE compress_test (id INT)");
2537        shell.execute("INSERT INTO compress_test VALUES (1)");
2538
2539        // Save compressed
2540        let result = shell.execute(&format!("SAVE COMPRESSED '{path_str}'"));
2541        let saved = matches!(result, CommandResult::Output(_));
2542
2543        // Clean up
2544        let _ = std::fs::remove_file(&path);
2545        let _ = std::fs::remove_file(path.with_extension("log"));
2546
2547        // Just verify it ran without panic
2548        assert!(saved || matches!(result, CommandResult::Error(_)));
2549    }
2550
2551    #[test]
2552    fn test_wal_status_when_active() {
2553        let mut shell = Shell::new();
2554        let temp_dir = std::env::temp_dir();
2555        let path = temp_dir.join("test_wal_status_active.bin");
2556        let path_str = path.to_string_lossy().to_string();
2557
2558        // Save to create a snapshot
2559        shell.execute(&format!("SAVE '{path_str}'"));
2560
2561        // Load to activate WAL
2562        shell.execute(&format!("LOAD '{path_str}'"));
2563
2564        // Check WAL status
2565        let result = shell.execute("WAL STATUS");
2566        // May show active or not depending on successful load
2567        assert!(matches!(result, CommandResult::Output(_)));
2568
2569        // Clean up
2570        let _ = std::fs::remove_file(&path);
2571        let _ = std::fs::remove_file(path.with_extension("log"));
2572    }
2573
2574    #[test]
2575    fn test_extract_path_whitespace_path() {
2576        // Path with only whitespace after stripping quotes
2577        let result = Shell::extract_path("save '   '", "save");
2578        assert_eq!(result, Some("   ".to_string()));
2579    }
2580
2581    #[test]
2582    fn test_wal_write_on_successful_command() {
2583        let mut shell = Shell::new();
2584        let temp_dir = std::env::temp_dir();
2585        let path = temp_dir.join("test_wal_write.bin");
2586        let path_str = path.to_string_lossy().to_string();
2587
2588        // Save and load to activate WAL
2589        shell.execute(&format!("SAVE '{path_str}'"));
2590        shell.execute(&format!("LOAD '{path_str}'"));
2591
2592        // Execute write command
2593        let result = shell.execute("CREATE TABLE wal_write_test (id INT)");
2594        assert!(
2595            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2596        );
2597
2598        // Clean up
2599        let _ = std::fs::remove_file(&path);
2600        let _ = std::fs::remove_file(path.with_extension("log"));
2601    }
2602
2603    #[test]
2604    fn test_replay_wal_with_valid_commands() {
2605        use std::io::Write;
2606
2607        let shell = Shell::new();
2608        let temp_dir = std::env::temp_dir();
2609        let wal_path = temp_dir.join("test_replay_valid.log");
2610
2611        // Create a WAL file with valid commands
2612        {
2613            let mut file = std::fs::File::create(&wal_path).unwrap();
2614            writeln!(file, "CREATE TABLE replay_test (id INT)").unwrap();
2615            writeln!(file, "INSERT INTO replay_test VALUES (1)").unwrap();
2616            writeln!(file).unwrap(); // empty line
2617            writeln!(file, "INSERT INTO replay_test VALUES (2)").unwrap();
2618        }
2619
2620        let result = shell.replay_wal(&wal_path, WalRecoveryMode::Strict);
2621        assert!(result.is_ok());
2622        let result = result.unwrap();
2623        assert_eq!(result.replayed, 3);
2624        assert!(result.errors.is_empty());
2625
2626        // Clean up
2627        let _ = std::fs::remove_file(&wal_path);
2628    }
2629
2630    #[test]
2631    fn test_replay_wal_strict_mode_error() {
2632        use std::io::Write;
2633
2634        let shell = Shell::new();
2635        let temp_dir = std::env::temp_dir();
2636        let wal_path = temp_dir.join("test_replay_strict_error.log");
2637
2638        // Create a WAL file with invalid command
2639        {
2640            let mut file = std::fs::File::create(&wal_path).unwrap();
2641            writeln!(file, "CREATE TABLE strict_test (id INT)").unwrap();
2642            writeln!(file, "INVALID_COMMAND @#$%").unwrap(); // This will fail
2643            writeln!(file, "INSERT INTO strict_test VALUES (1)").unwrap();
2644        }
2645
2646        let result = shell.replay_wal(&wal_path, WalRecoveryMode::Strict);
2647        assert!(result.is_err()); // Should fail in strict mode
2648
2649        // Clean up
2650        let _ = std::fs::remove_file(&wal_path);
2651    }
2652
2653    #[test]
2654    fn test_replay_wal_recover_mode() {
2655        use std::io::Write;
2656
2657        let shell = Shell::new();
2658        let temp_dir = std::env::temp_dir();
2659        let wal_path = temp_dir.join("test_replay_recover.log");
2660
2661        // Create a WAL file with one invalid command
2662        {
2663            let mut file = std::fs::File::create(&wal_path).unwrap();
2664            writeln!(file, "CREATE TABLE recover_test (id INT)").unwrap();
2665            writeln!(file, "INVALID_COMMAND @#$%").unwrap(); // This will be skipped
2666            writeln!(file, "INSERT INTO recover_test VALUES (1)").unwrap();
2667        }
2668
2669        let result = shell.replay_wal(&wal_path, WalRecoveryMode::Recover);
2670        assert!(result.is_ok());
2671        let result = result.unwrap();
2672        assert_eq!(result.replayed, 2); // 2 commands succeeded
2673        assert_eq!(result.errors.len(), 1); // 1 error was skipped
2674
2675        // Clean up
2676        let _ = std::fs::remove_file(&wal_path);
2677    }
2678
2679    #[test]
2680    fn test_replay_wal_file_not_found() {
2681        let shell = Shell::new();
2682        let result = shell.replay_wal(
2683            Path::new("/nonexistent/path/wal.log"),
2684            WalRecoveryMode::Strict,
2685        );
2686        assert!(result.is_err());
2687        assert!(result.unwrap_err().contains("Failed to open WAL"));
2688    }
2689
2690    #[test]
2691    fn test_load_with_wal_replay_success() {
2692        use std::io::Write;
2693
2694        let mut shell = Shell::new();
2695        let temp_dir = std::env::temp_dir();
2696        let snap_path = temp_dir.join("test_load_wal_replay.bin");
2697        let wal_path = snap_path.with_extension("log");
2698        let snap_str = snap_path.to_string_lossy().to_string();
2699
2700        // Create data and save snapshot
2701        shell.execute("CREATE TABLE load_replay_test (id INT)");
2702        shell.execute(&format!("SAVE '{snap_str}'"));
2703
2704        // Create a WAL file with additional commands
2705        {
2706            let mut file = std::fs::File::create(&wal_path).unwrap();
2707            writeln!(file, "INSERT INTO load_replay_test VALUES (100)").unwrap();
2708        }
2709
2710        // Load should replay the WAL
2711        let result = shell.handle_load(&format!("LOAD '{snap_str}'"));
2712        // Should succeed and replay the WAL
2713        assert!(matches!(result, CommandResult::Output(_)));
2714
2715        // Clean up
2716        let _ = std::fs::remove_file(&snap_path);
2717        let _ = std::fs::remove_file(&wal_path);
2718    }
2719
2720    #[test]
2721    fn test_load_with_wal_replay_error_strict() {
2722        use std::io::Write;
2723
2724        let mut shell = Shell::new();
2725        let temp_dir = std::env::temp_dir();
2726        let snap_path = temp_dir.join("test_load_wal_error_strict.bin");
2727        let wal_path = snap_path.with_extension("log");
2728        let snap_str = snap_path.to_string_lossy().to_string();
2729
2730        // Save snapshot
2731        shell.execute(&format!("SAVE '{snap_str}'"));
2732
2733        // Create a WAL file with invalid command
2734        {
2735            let mut file = std::fs::File::create(&wal_path).unwrap();
2736            writeln!(file, "INVALID_COMMAND @#$%").unwrap();
2737        }
2738
2739        // Load in strict mode should fail
2740        let result = shell.handle_load(&format!("LOAD '{snap_str}'"));
2741        assert!(matches!(result, CommandResult::Error(_)));
2742        if let CommandResult::Error(msg) = result {
2743            assert!(msg.contains("WAL replay failed"));
2744            assert!(msg.contains("RECOVER")); // Hint should mention RECOVER
2745        }
2746
2747        // Clean up
2748        let _ = std::fs::remove_file(&snap_path);
2749        let _ = std::fs::remove_file(&wal_path);
2750    }
2751
2752    #[test]
2753    fn test_load_with_wal_replay_error_recover() {
2754        use std::io::Write;
2755
2756        let mut shell = Shell::new();
2757        let temp_dir = std::env::temp_dir();
2758        let snap_path = temp_dir.join("test_load_wal_error_recover.bin");
2759        let wal_path = snap_path.with_extension("log");
2760        let snap_str = snap_path.to_string_lossy().to_string();
2761
2762        // Save snapshot
2763        shell.execute(&format!("SAVE '{snap_str}'"));
2764
2765        // Create a WAL file with invalid command
2766        {
2767            let mut file = std::fs::File::create(&wal_path).unwrap();
2768            writeln!(file, "CREATE TABLE recover_load_test (id INT)").unwrap();
2769            writeln!(file, "INVALID_COMMAND @#$%").unwrap();
2770            writeln!(file, "INSERT INTO recover_load_test VALUES (1)").unwrap();
2771        }
2772
2773        // Load with RECOVER should succeed but report skipped entries
2774        let result = shell.handle_load(&format!("LOAD '{snap_str}' RECOVER"));
2775        assert!(matches!(result, CommandResult::Output(_)));
2776        if let CommandResult::Output(msg) = &result {
2777            assert!(msg.contains("Loaded snapshot") || msg.contains("Skipped"));
2778        }
2779
2780        // Clean up
2781        let _ = std::fs::remove_file(&snap_path);
2782        let _ = std::fs::remove_file(&wal_path);
2783    }
2784
2785    #[test]
2786    fn test_wal_truncate_when_active() {
2787        let mut shell = Shell::new();
2788        let temp_dir = std::env::temp_dir();
2789        let path = temp_dir.join("test_wal_truncate_active.bin");
2790        let path_str = path.to_string_lossy().to_string();
2791
2792        // Save and load to activate WAL
2793        shell.execute(&format!("SAVE '{path_str}'"));
2794        shell.execute(&format!("LOAD '{path_str}'"));
2795
2796        // Add some WAL entries
2797        shell.execute("CREATE TABLE truncate_test (id INT)");
2798        shell.execute("INSERT INTO truncate_test VALUES (1)");
2799
2800        // Truncate WAL
2801        let result = shell.execute("WAL TRUNCATE");
2802        assert!(
2803            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2804        );
2805
2806        // Clean up
2807        let _ = std::fs::remove_file(&path);
2808        let _ = std::fs::remove_file(path.with_extension("log"));
2809    }
2810
2811    #[test]
2812    fn test_wal_status_when_active_with_data() {
2813        let mut shell = Shell::new();
2814        let temp_dir = std::env::temp_dir();
2815        let path = temp_dir.join("test_wal_status_with_data.bin");
2816        let path_str = path.to_string_lossy().to_string();
2817
2818        // Save and load to activate WAL
2819        shell.execute(&format!("SAVE '{path_str}'"));
2820        shell.execute(&format!("LOAD '{path_str}'"));
2821
2822        // Add some WAL entries
2823        shell.execute("CREATE TABLE status_test (id INT)");
2824
2825        // Check WAL status
2826        let result = shell.execute("WAL STATUS");
2827        if let CommandResult::Output(msg) = result {
2828            assert!(msg.contains("WAL") || msg.contains("Path") || msg.contains("not active"));
2829        }
2830
2831        // Clean up
2832        let _ = std::fs::remove_file(&path);
2833        let _ = std::fs::remove_file(path.with_extension("log"));
2834    }
2835
2836    #[test]
2837    fn test_save_truncates_wal() {
2838        let mut shell = Shell::new();
2839        let temp_dir = std::env::temp_dir();
2840        let path = temp_dir.join("test_save_truncates_wal.bin");
2841        let path_str = path.to_string_lossy().to_string();
2842
2843        // First save and load to activate WAL
2844        shell.execute(&format!("SAVE '{path_str}'"));
2845        shell.execute(&format!("LOAD '{path_str}'"));
2846
2847        // Add WAL entries
2848        shell.execute("CREATE TABLE truncate_on_save_test (id INT)");
2849        shell.execute("INSERT INTO truncate_on_save_test VALUES (1)");
2850
2851        // Save should truncate WAL
2852        let result = shell.execute(&format!("SAVE '{path_str}'"));
2853        assert!(matches!(result, CommandResult::Output(_)));
2854
2855        // Clean up
2856        let _ = std::fs::remove_file(&path);
2857        let _ = std::fs::remove_file(path.with_extension("log"));
2858    }
2859
2860    #[test]
2861    fn test_save_compressed_truncates_wal() {
2862        let mut shell = Shell::new();
2863        let temp_dir = std::env::temp_dir();
2864        let path = temp_dir.join("test_save_compressed_truncates_wal.bin");
2865        let path_str = path.to_string_lossy().to_string();
2866
2867        // First save and load to activate WAL
2868        shell.execute(&format!("SAVE '{path_str}'"));
2869        shell.execute(&format!("LOAD '{path_str}'"));
2870
2871        // Add WAL entries
2872        shell.execute("CREATE TABLE compressed_wal_test (id INT)");
2873
2874        // Save compressed should truncate WAL
2875        let result = shell.execute(&format!("SAVE COMPRESSED '{path_str}'"));
2876        assert!(
2877            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2878        );
2879
2880        // Clean up
2881        let _ = std::fs::remove_file(&path);
2882        let _ = std::fs::remove_file(path.with_extension("log"));
2883    }
2884
2885    #[test]
2886    fn test_execute_select_query() {
2887        let mut shell = Shell::new();
2888
2889        // Create table and data
2890        shell.execute("CREATE TABLE select_test (id INT, name TEXT)");
2891        shell.execute("INSERT INTO select_test VALUES (1, 'Alice')");
2892        shell.execute("INSERT INTO select_test VALUES (2, 'Bob')");
2893
2894        // Execute SELECT
2895        let result = shell.execute("SELECT * FROM select_test WHERE id = 1");
2896        assert!(matches!(result, CommandResult::Output(_)));
2897    }
2898
2899    #[test]
2900    fn test_execute_update_query() {
2901        let mut shell = Shell::new();
2902
2903        // Create table and data
2904        shell.execute("CREATE TABLE update_test (id INT, name TEXT)");
2905        shell.execute("INSERT INTO update_test VALUES (1, 'Alice')");
2906
2907        // Execute UPDATE
2908        let result = shell.execute("UPDATE update_test SET name = 'Alicia' WHERE id = 1");
2909        assert!(
2910            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2911        );
2912    }
2913
2914    #[test]
2915    fn test_execute_delete_query() {
2916        let mut shell = Shell::new();
2917
2918        // Create table and data
2919        shell.execute("CREATE TABLE delete_test (id INT)");
2920        shell.execute("INSERT INTO delete_test VALUES (1)");
2921        shell.execute("INSERT INTO delete_test VALUES (2)");
2922
2923        // Execute DELETE
2924        let result = shell.execute("DELETE FROM delete_test WHERE id = 1");
2925        assert!(
2926            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2927        );
2928    }
2929
2930    #[test]
2931    fn test_execute_edge_operations() {
2932        let mut shell = Shell::new();
2933
2934        // Create nodes first
2935        shell.execute("NODE CREATE person {name: 'Alice'}");
2936        shell.execute("NODE CREATE person {name: 'Bob'}");
2937
2938        // Create edge
2939        let result = shell.execute("EDGE CREATE knows 1 2 {since: '2024'}");
2940        assert!(
2941            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2942        );
2943
2944        // List edges
2945        let result = shell.execute("EDGE LIST knows");
2946        assert!(
2947            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2948        );
2949    }
2950
2951    #[test]
2952    fn test_execute_similar_with_spinner() {
2953        let mut shell = Shell::new();
2954
2955        // Store some embeddings
2956        shell.execute("EMBED STORE 'doc1' [0.1, 0.2, 0.3, 0.4]");
2957        shell.execute("EMBED STORE 'doc2' [0.2, 0.3, 0.4, 0.5]");
2958
2959        // SIMILAR should use spinner
2960        let result = shell.execute("SIMILAR [0.1, 0.2, 0.3, 0.4] LIMIT 5");
2961        assert!(
2962            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2963        );
2964    }
2965
2966    #[test]
2967    fn test_execute_find_with_spinner() {
2968        let mut shell = Shell::new();
2969
2970        // FIND should use spinner
2971        let result = shell.execute("FIND test_pattern LIMIT 10");
2972        assert!(
2973            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2974        );
2975    }
2976
2977    #[test]
2978    fn test_detect_embedding_dimension_scalar_only() {
2979        let store = TensorStore::new();
2980        let mut data = tensor_store::TensorData::new();
2981        data.set(
2982            "int_field",
2983            tensor_store::TensorValue::Scalar(tensor_store::ScalarValue::Int(42)),
2984        );
2985        data.set(
2986            "string_field",
2987            tensor_store::TensorValue::Scalar(tensor_store::ScalarValue::String(
2988                "test".to_string(),
2989            )),
2990        );
2991        store.put("key1", data).unwrap();
2992
2993        // Should return default when only scalars
2994        let dim = Shell::detect_embedding_dimension(&store);
2995        assert_eq!(dim, tensor_compress::CompressionDefaults::STANDARD);
2996    }
2997
2998    #[test]
2999    fn test_router_executor_execute_valid() {
3000        let router = Arc::new(RwLock::new(QueryRouter::new()));
3001        let executor = RouterExecutor(Arc::clone(&router));
3002
3003        // Create a table first
3004        {
3005            let r = router.write();
3006            let _ = r.execute_parsed("CREATE TABLE executor_test (id INT)");
3007        }
3008
3009        // Execute through RouterExecutor
3010        let result = executor.execute("SELECT * FROM executor_test");
3011        assert!(result.is_ok() || result.is_err());
3012    }
3013
3014    #[test]
3015    fn test_execute_path_query() {
3016        let mut shell = Shell::new();
3017
3018        // Create graph
3019        shell.execute("NODE CREATE city {name: 'A'}");
3020        shell.execute("NODE CREATE city {name: 'B'}");
3021        shell.execute("NODE CREATE city {name: 'C'}");
3022        shell.execute("EDGE CREATE road 1 2");
3023        shell.execute("EDGE CREATE road 2 3");
3024
3025        // Find path
3026        let result = shell.execute("PATH 1 3");
3027        assert!(
3028            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3029        );
3030    }
3031
3032    #[test]
3033    fn test_execute_neighbors_query() {
3034        let mut shell = Shell::new();
3035
3036        // Create graph
3037        shell.execute("NODE CREATE person {name: 'Center'}");
3038        shell.execute("NODE CREATE person {name: 'Friend1'}");
3039        shell.execute("NODE CREATE person {name: 'Friend2'}");
3040        shell.execute("EDGE CREATE knows 1 2");
3041        shell.execute("EDGE CREATE knows 1 3");
3042
3043        // Get neighbors
3044        let result = shell.execute("NEIGHBORS 1");
3045        assert!(
3046            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3047        );
3048    }
3049
3050    #[test]
3051    fn test_cluster_connect_parsing_edge_cases() {
3052        let shell = Shell::new();
3053
3054        // Test with unquoted addresses
3055        let result = shell.handle_cluster_connect("CLUSTER CONNECT node1@127.0.0.1:8080");
3056        assert!(matches!(result, CommandResult::Error(_)));
3057
3058        // Test with mixed addressing
3059        let result = shell.handle_cluster_connect("cluster connect 'node@[::1]:8080'");
3060        assert!(matches!(result, CommandResult::Error(_)));
3061    }
3062
3063    #[test]
3064    fn test_cluster_connect_invalid_peer_address() {
3065        let shell = Shell::new();
3066
3067        // Valid local, invalid peer
3068        let result =
3069            shell.handle_cluster_connect("cluster connect 'node1@127.0.0.1:8080' 'invalid_peer'");
3070        assert!(matches!(result, CommandResult::Error(_)));
3071    }
3072
3073    #[test]
3074    fn test_shell_icons_with_theme() {
3075        let config = ShellConfig {
3076            no_color: false,
3077            ..Default::default()
3078        };
3079        let shell = Shell::with_config(config);
3080        // Should use auto icons
3081        assert!(!shell.icons.success.is_empty());
3082    }
3083
3084    #[test]
3085    fn test_shell_config_history_file_default() {
3086        // Test with HOME set
3087        let original = std::env::var_os("HOME");
3088        std::env::set_var("HOME", "/test/home");
3089
3090        let config = ShellConfig::default();
3091        assert!(config.history_file.is_some());
3092        if let Some(path) = config.history_file {
3093            assert!(path.to_string_lossy().contains(".neumann_history"));
3094        }
3095
3096        // Restore
3097        if let Some(home) = original {
3098            std::env::set_var("HOME", home);
3099        } else {
3100            std::env::remove_var("HOME");
3101        }
3102    }
3103
3104    #[test]
3105    fn test_execute_pagerank() {
3106        let mut shell = Shell::new();
3107
3108        // Create a simple graph for PageRank
3109        shell.execute("NODE CREATE webpage {url: 'a.com'}");
3110        shell.execute("NODE CREATE webpage {url: 'b.com'}");
3111        shell.execute("NODE CREATE webpage {url: 'c.com'}");
3112        shell.execute("EDGE CREATE links 1 2");
3113        shell.execute("EDGE CREATE links 2 3");
3114        shell.execute("EDGE CREATE links 3 1");
3115
3116        // Execute PageRank
3117        let result = shell.execute("GRAPH ALGORITHM PAGERANK");
3118        assert!(
3119            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3120        );
3121    }
3122
3123    #[test]
3124    fn test_execute_centrality() {
3125        let mut shell = Shell::new();
3126
3127        // Create graph
3128        shell.execute("NODE CREATE person {name: 'Alice'}");
3129        shell.execute("NODE CREATE person {name: 'Bob'}");
3130        shell.execute("NODE CREATE person {name: 'Carol'}");
3131        shell.execute("EDGE CREATE knows 1 2");
3132        shell.execute("EDGE CREATE knows 2 3");
3133
3134        // Execute centrality (degree or betweenness)
3135        let result = shell.execute("GRAPH ALGORITHM DEGREE");
3136        assert!(
3137            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3138        );
3139    }
3140
3141    #[test]
3142    fn test_execute_communities() {
3143        let mut shell = Shell::new();
3144
3145        // Create graph with clusters
3146        shell.execute("NODE CREATE person {group: 1}");
3147        shell.execute("NODE CREATE person {group: 1}");
3148        shell.execute("NODE CREATE person {group: 2}");
3149        shell.execute("EDGE CREATE knows 1 2");
3150
3151        // Try community detection
3152        let result = shell.execute("GRAPH ALGORITHM COMMUNITIES");
3153        assert!(
3154            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3155        );
3156    }
3157
3158    #[test]
3159    fn test_execute_unified_query() {
3160        let mut shell = Shell::new();
3161
3162        // Store embeddings
3163        shell.execute("EMBED STORE 'entity:1' [0.1, 0.2, 0.3, 0.4]");
3164        shell.execute("EMBED STORE 'entity:2' [0.2, 0.3, 0.4, 0.5]");
3165
3166        // Create a node
3167        shell.execute("NODE CREATE entity {id: 1}");
3168
3169        // Try FIND with pattern matching
3170        let result = shell.execute("FIND 'entity' LIMIT 10");
3171        assert!(
3172            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3173        );
3174    }
3175
3176    #[test]
3177    fn test_execute_node_list() {
3178        let mut shell = Shell::new();
3179
3180        // Create nodes
3181        shell.execute("NODE CREATE animal {species: 'dog'}");
3182        shell.execute("NODE CREATE animal {species: 'cat'}");
3183
3184        // List nodes
3185        let result = shell.execute("NODE LIST animal");
3186        assert!(matches!(result, CommandResult::Output(_)));
3187    }
3188
3189    #[test]
3190    fn test_execute_node_get() {
3191        let mut shell = Shell::new();
3192
3193        // Create a node
3194        shell.execute("NODE CREATE item {name: 'book'}");
3195
3196        // Get by ID
3197        let result = shell.execute("NODE GET 1");
3198        assert!(
3199            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3200        );
3201    }
3202
3203    #[test]
3204    fn test_execute_edge_list() {
3205        let mut shell = Shell::new();
3206
3207        // Create graph
3208        shell.execute("NODE CREATE person {name: 'A'}");
3209        shell.execute("NODE CREATE person {name: 'B'}");
3210        shell.execute("EDGE CREATE friend 1 2");
3211
3212        // List edges
3213        let result = shell.execute("EDGE LIST friend");
3214        assert!(matches!(result, CommandResult::Output(_)));
3215    }
3216
3217    #[test]
3218    fn test_execute_show_tables() {
3219        let mut shell = Shell::new();
3220
3221        // Create some tables
3222        shell.execute("CREATE TABLE t1 (id INT)");
3223        shell.execute("CREATE TABLE t2 (id INT)");
3224
3225        // Show tables
3226        let result = shell.execute("SHOW TABLES");
3227        assert!(matches!(result, CommandResult::Output(_)));
3228        if let CommandResult::Output(output) = result {
3229            assert!(output.contains("t1") || output.contains("Tables"));
3230        }
3231    }
3232
3233    #[test]
3234    fn test_execute_drop_table() {
3235        let mut shell = Shell::new();
3236
3237        // Create and drop table
3238        shell.execute("CREATE TABLE drop_me (id INT)");
3239        let result = shell.execute("DROP TABLE drop_me");
3240        assert!(
3241            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3242        );
3243    }
3244
3245    #[test]
3246    fn test_execute_aggregate_query() {
3247        let mut shell = Shell::new();
3248
3249        // Create table with data
3250        shell.execute("CREATE TABLE sales (amount INT, category TEXT)");
3251        shell.execute("INSERT INTO sales VALUES (100, 'A')");
3252        shell.execute("INSERT INTO sales VALUES (200, 'A')");
3253        shell.execute("INSERT INTO sales VALUES (150, 'B')");
3254
3255        // Try aggregate query
3256        let result = shell.execute("SELECT COUNT(*) FROM sales");
3257        assert!(
3258            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3259        );
3260    }
3261
3262    #[test]
3263    fn test_execute_embed_delete() {
3264        let mut shell = Shell::new();
3265
3266        // Store then delete
3267        shell.execute("EMBED STORE 'del_key' [0.1, 0.2, 0.3]");
3268        let result = shell.execute("EMBED DELETE 'del_key'");
3269        assert!(
3270            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3271        );
3272    }
3273
3274    #[test]
3275    fn test_execute_embed_get() {
3276        let mut shell = Shell::new();
3277
3278        // Store then get
3279        shell.execute("EMBED STORE 'get_key' [0.5, 0.6, 0.7]");
3280        let result = shell.execute("EMBED GET 'get_key'");
3281        assert!(
3282            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3283        );
3284    }
3285
3286    #[test]
3287    fn test_execute_node_delete() {
3288        let mut shell = Shell::new();
3289
3290        // Create and delete node
3291        shell.execute("NODE CREATE temp {name: 'deleteme'}");
3292        let result = shell.execute("NODE DELETE 1");
3293        assert!(
3294            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3295        );
3296    }
3297
3298    #[test]
3299    fn test_execute_edge_delete() {
3300        let mut shell = Shell::new();
3301
3302        // Create graph and delete edge
3303        shell.execute("NODE CREATE person {name: 'A'}");
3304        shell.execute("NODE CREATE person {name: 'B'}");
3305        shell.execute("EDGE CREATE temp 1 2");
3306        let result = shell.execute("EDGE DELETE temp 1 2");
3307        assert!(
3308            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3309        );
3310    }
3311
3312    #[test]
3313    fn test_execute_node_update() {
3314        let mut shell = Shell::new();
3315
3316        // Create and update node
3317        shell.execute("NODE CREATE item {count: 0}");
3318        let result = shell.execute("NODE UPDATE 1 {count: 5}");
3319        assert!(
3320            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3321        );
3322    }
3323
3324    #[test]
3325    fn test_is_write_command_all_branches() {
3326        // Test all branches explicitly
3327        assert!(!Shell::is_write_command("")); // Empty
3328        assert!(!Shell::is_write_command("SELECT")); // Read
3329        assert!(Shell::is_write_command("NODE CREATE test {}"));
3330        assert!(Shell::is_write_command("node create test {}")); // lowercase
3331        assert!(!Shell::is_write_command("EDGE GET 1"));
3332        assert!(Shell::is_write_command("ENTITY CREATE type {}"));
3333        assert!(!Shell::is_write_command("ENTITY GET 1"));
3334        assert!(!Shell::is_write_command("GRAPH ALGORITHM X"));
3335    }
3336
3337    #[test]
3338    fn test_extract_path_compressed() {
3339        let path = Shell::extract_path("save compressed 'test.bin'", "save compressed");
3340        assert_eq!(path, Some("test.bin".to_string()));
3341    }
3342
3343    #[test]
3344    fn test_detect_embedding_with_multiple_keys() {
3345        let store = TensorStore::new();
3346
3347        // Add multiple entities, first few without vectors
3348        for i in 0..5 {
3349            let mut data = tensor_store::TensorData::new();
3350            data.set(
3351                "name",
3352                tensor_store::TensorValue::Scalar(tensor_store::ScalarValue::String(format!(
3353                    "entity{i}"
3354                ))),
3355            );
3356            store.put(format!("scalar:{i}"), data).unwrap();
3357        }
3358
3359        // Add one with vector
3360        let mut data = tensor_store::TensorData::new();
3361        data.set(
3362            "vec",
3363            tensor_store::TensorValue::Vector(vec![0.1, 0.2, 0.3, 0.4, 0.5]),
3364        );
3365        store.put("with_vec", data).unwrap();
3366
3367        let dim = Shell::detect_embedding_dimension(&store);
3368        // Should find the 5-dim vector
3369        assert!(dim == 5 || dim == tensor_compress::CompressionDefaults::STANDARD);
3370    }
3371
3372    #[test]
3373    fn test_cluster_connect_empty_quotes() {
3374        let shell = Shell::new();
3375        // Empty quoted address
3376        let result = shell.handle_cluster_connect("cluster connect ''");
3377        assert!(matches!(result, CommandResult::Error(_)));
3378    }
3379
3380    #[test]
3381    fn test_vault_identity_from_execute() {
3382        let mut shell = Shell::new();
3383
3384        // Get current identity via execute
3385        let result = shell.execute("VAULT IDENTITY");
3386        assert!(matches!(result, CommandResult::Output(_)));
3387
3388        // Set identity via execute
3389        let result = shell.execute("VAULT IDENTITY 'test_user'");
3390        assert!(matches!(result, CommandResult::Output(_)));
3391    }
3392
3393    #[test]
3394    fn test_cache_init_from_execute() {
3395        let mut shell = Shell::new();
3396
3397        // Initialize cache via execute
3398        let result = shell.execute("CACHE INIT");
3399        assert!(
3400            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3401        );
3402    }
3403
3404    #[test]
3405    fn test_wal_replay_with_only_empty_lines() {
3406        use std::io::Write;
3407
3408        let shell = Shell::new();
3409        let temp_dir = std::env::temp_dir();
3410        let wal_path = temp_dir.join("test_replay_empty_only.log");
3411
3412        // Create WAL with only empty/whitespace lines
3413        {
3414            let mut file = std::fs::File::create(&wal_path).unwrap();
3415            writeln!(file).unwrap();
3416            writeln!(file, "   ").unwrap();
3417            writeln!(file, "\t").unwrap();
3418        }
3419
3420        let result = shell.replay_wal(&wal_path, WalRecoveryMode::Strict);
3421        assert!(result.is_ok());
3422        let result = result.unwrap();
3423        assert_eq!(result.replayed, 0);
3424
3425        // Clean up
3426        let _ = std::fs::remove_file(&wal_path);
3427    }
3428
3429    #[test]
3430    fn test_format_wal_replay_result_6_errors() {
3431        // Test with exactly 6 errors (one more than shown)
3432        let errors: Vec<WalReplayError> = (1..=6)
3433            .map(|i| WalReplayError::new(i, &format!("cmd{i}"), "error".to_string()))
3434            .collect();
3435        let result = WalReplayResult {
3436            replayed: 0,
3437            errors,
3438        };
3439        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Recover);
3440        // Should contain "and 1 more"
3441        assert!(formatted.contains("and 1 more"));
3442    }
3443
3444    #[test]
3445    fn test_shell_confirmation_handler_new() {
3446        let _helper = NeumannHelper::new(Theme::plain());
3447        let editor: Editor<NeumannHelper, rustyline::history::DefaultHistory> =
3448            Editor::new().unwrap();
3449        let editor = Arc::new(Mutex::new(editor));
3450        let _handler = ShellConfirmationHandler::new(Arc::clone(&editor));
3451    }
3452
3453    #[test]
3454    fn test_router_executor_with_error() {
3455        let router = Arc::new(RwLock::new(QueryRouter::new()));
3456        let executor = RouterExecutor(Arc::clone(&router));
3457
3458        // Execute invalid query
3459        let result = executor.execute("INVALID QUERY @#$%");
3460        assert!(result.is_err());
3461    }
3462
3463    #[test]
3464    fn test_shell_execute_with_spinner_operations() {
3465        let mut shell = Shell::new();
3466
3467        // Operations that use spinners
3468        shell.execute("EMBED STORE 'spinner_test' [0.1, 0.2, 0.3]");
3469
3470        // SIMILAR uses spinner
3471        let result = shell.execute("SIMILAR [0.1, 0.2, 0.3] LIMIT 10");
3472        assert!(
3473            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3474        );
3475    }
3476
3477    #[test]
3478    fn test_extract_load_mixed_case_recover() {
3479        // Test RECOVER keyword in different cases
3480        let result = Shell::extract_load_path_and_mode("LOAD 'file.bin' recover");
3481        assert!(result.is_some());
3482        let (_, mode) = result.unwrap();
3483        assert_eq!(mode, WalRecoveryMode::Recover);
3484
3485        let result = Shell::extract_load_path_and_mode("load 'file.bin' RECOVER");
3486        assert!(result.is_some());
3487        let (_, mode) = result.unwrap();
3488        assert_eq!(mode, WalRecoveryMode::Recover);
3489    }
3490
3491    #[test]
3492    fn test_handle_save_with_quotes_inside() {
3493        let shell = Shell::new();
3494        // Path without quotes
3495        let result = shell.handle_save("save /tmp/test_file.bin");
3496        // Should work or fail but not panic
3497        assert!(
3498            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3499        );
3500    }
3501
3502    #[test]
3503    fn test_execute_chain_query() {
3504        let mut shell = Shell::new();
3505
3506        // Chain operations
3507        let result = shell.execute("CHAIN HEIGHT");
3508        assert!(
3509            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3510        );
3511
3512        let result = shell.execute("CHAIN STATUS");
3513        assert!(
3514            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3515        );
3516    }
3517
3518    #[test]
3519    fn test_execute_graph_pattern_match() {
3520        let mut shell = Shell::new();
3521
3522        // Create nodes and edges
3523        shell.execute("NODE CREATE person {name: 'Alice'}");
3524        shell.execute("NODE CREATE person {name: 'Bob'}");
3525        shell.execute("EDGE CREATE knows 1 2");
3526
3527        // Try pattern match query (if supported)
3528        let result = shell.execute("GRAPH MATCH (a)-[r]->(b) WHERE a.name = 'Alice'");
3529        // May not be fully supported, just verify it doesn't panic
3530        let _ = result;
3531    }
3532
3533    #[test]
3534    fn test_execute_batch_operations() {
3535        let mut shell = Shell::new();
3536
3537        // Create table
3538        shell.execute("CREATE TABLE batch_test (id INT, name TEXT)");
3539
3540        // Try batch insert (if supported)
3541        let result = shell.execute("INSERT INTO batch_test VALUES (1, 'a'), (2, 'b'), (3, 'c')");
3542        let _ = result;
3543    }
3544
3545    #[test]
3546    fn test_execute_constraint_operations() {
3547        let mut shell = Shell::new();
3548
3549        // Try constraint operations
3550        let result = shell.execute("GRAPH CONSTRAINT LIST");
3551        let _ = result;
3552    }
3553
3554    #[test]
3555    fn test_execute_index_operations() {
3556        let mut shell = Shell::new();
3557
3558        // Try index operations
3559        let result = shell.execute("GRAPH INDEX LIST");
3560        let _ = result;
3561    }
3562
3563    #[test]
3564    fn test_shell_config_quiet_mode() {
3565        let config = ShellConfig {
3566            quiet: true,
3567            no_color: true,
3568            no_boot: true,
3569            ..Default::default()
3570        };
3571        let shell = Shell::with_config(config);
3572        assert!(shell.config.quiet);
3573        assert!(shell.config.no_color);
3574        assert!(shell.config.no_boot);
3575    }
3576
3577    #[test]
3578    fn test_execute_blob_operations() {
3579        let mut shell = Shell::new();
3580
3581        // Try blob operations
3582        let result = shell.execute("BLOB LIST");
3583        let _ = result;
3584
3585        let result = shell.execute("BLOB STATS");
3586        let _ = result;
3587    }
3588
3589    #[test]
3590    fn test_execute_checkpoint_operations() {
3591        let mut shell = Shell::new();
3592
3593        // Try checkpoint operations
3594        let result = shell.execute("CHECKPOINTS");
3595        let _ = result;
3596    }
3597
3598    #[test]
3599    fn test_execute_cache_operations() {
3600        let mut shell = Shell::new();
3601
3602        // Try cache operations
3603        let result = shell.execute("CACHE STATS");
3604        let _ = result;
3605    }
3606
3607    #[test]
3608    fn test_load_path_double_quoted_recover() {
3609        let result = Shell::extract_load_path_and_mode("LOAD \"file.bin\" RECOVER");
3610        assert!(result.is_some());
3611        let (path, mode) = result.unwrap();
3612        assert_eq!(path, "file.bin");
3613        assert_eq!(mode, WalRecoveryMode::Recover);
3614    }
3615
3616    #[test]
3617    fn test_execute_with_successful_wal_write() {
3618        let mut shell = Shell::new();
3619        let temp_dir = std::env::temp_dir();
3620        let path = temp_dir.join("test_wal_success.bin");
3621        let path_str = path.to_string_lossy().to_string();
3622
3623        // Save and load to activate WAL
3624        shell.execute(&format!("SAVE '{path_str}'"));
3625        shell.execute(&format!("LOAD '{path_str}'"));
3626
3627        // Execute a write command that should be logged to WAL
3628        let result = shell.execute("CREATE TABLE wal_log_test (id INT)");
3629        assert!(matches!(result, CommandResult::Output(_)));
3630
3631        // Execute another write command
3632        let result = shell.execute("INSERT INTO wal_log_test VALUES (1)");
3633        assert!(
3634            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3635        );
3636
3637        // Clean up
3638        let _ = std::fs::remove_file(&path);
3639        let _ = std::fs::remove_file(path.with_extension("log"));
3640    }
3641
3642    #[test]
3643    fn test_execute_edge_with_properties() {
3644        let mut shell = Shell::new();
3645
3646        // Create nodes
3647        shell.execute("NODE CREATE city {name: 'Paris'}");
3648        shell.execute("NODE CREATE city {name: 'London'}");
3649
3650        // Create edge with properties
3651        let result = shell.execute("EDGE CREATE route 1 2 {distance: 450, mode: 'train'}");
3652        assert!(
3653            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3654        );
3655    }
3656
3657    #[test]
3658    fn test_execute_find_operations() {
3659        let mut shell = Shell::new();
3660
3661        // Store some entities
3662        shell.execute("ENTITY CREATE user {name: 'Test'}");
3663        shell.execute("EMBED STORE 'user:1' [0.1, 0.2, 0.3]");
3664
3665        // Try FIND
3666        let result = shell.execute("FIND 'user' LIMIT 5");
3667        let _ = result;
3668    }
3669
3670    #[test]
3671    fn test_execute_entity_operations() {
3672        let mut shell = Shell::new();
3673
3674        // Try entity operations
3675        let result = shell.execute("ENTITY CREATE document {title: 'Test'}");
3676        let _ = result;
3677
3678        let result = shell.execute("ENTITY GET 1");
3679        let _ = result;
3680    }
3681
3682    #[test]
3683    fn test_cluster_connect_with_commas() {
3684        let shell = Shell::new();
3685
3686        // Addresses separated by commas
3687        let result = shell.handle_cluster_connect(
3688            "cluster connect 'node1@127.0.0.1:8080','node2@127.0.0.1:8081'",
3689        );
3690        assert!(matches!(result, CommandResult::Error(_)));
3691    }
3692
3693    #[test]
3694    fn test_cluster_connect_unquoted_with_spaces() {
3695        let shell = Shell::new();
3696
3697        // Unquoted addresses with spaces
3698        let result = shell
3699            .handle_cluster_connect("cluster connect node1@127.0.0.1:8080 node2@127.0.0.1:8081");
3700        assert!(matches!(result, CommandResult::Error(_)));
3701    }
3702
3703    #[test]
3704    fn test_execute_graph_algorithm_betweenness() {
3705        let mut shell = Shell::new();
3706
3707        // Create a small graph
3708        shell.execute("NODE CREATE vertex {}");
3709        shell.execute("NODE CREATE vertex {}");
3710        shell.execute("NODE CREATE vertex {}");
3711        shell.execute("EDGE CREATE link 1 2");
3712        shell.execute("EDGE CREATE link 2 3");
3713
3714        // Try betweenness centrality
3715        let result = shell.execute("GRAPH ALGORITHM BETWEENNESS");
3716        let _ = result;
3717    }
3718
3719    #[test]
3720    fn test_execute_sparse_vector() {
3721        let mut shell = Shell::new();
3722
3723        // Try sparse vector operations
3724        let result =
3725            shell.execute("EMBED STORE 'sparse:1' SPARSE [1:0.5, 10:0.3, 100:0.2] DIM 1000");
3726        let _ = result;
3727    }
3728
3729    #[test]
3730    fn test_execute_with_where_clause() {
3731        let mut shell = Shell::new();
3732
3733        // Create table with data
3734        shell.execute("CREATE TABLE where_test (id INT, status TEXT)");
3735        shell.execute("INSERT INTO where_test VALUES (1, 'active')");
3736        shell.execute("INSERT INTO where_test VALUES (2, 'inactive')");
3737        shell.execute("INSERT INTO where_test VALUES (3, 'active')");
3738
3739        // Query with WHERE
3740        let result = shell.execute("SELECT * FROM where_test WHERE status = 'active'");
3741        assert!(matches!(result, CommandResult::Output(_)));
3742    }
3743
3744    #[test]
3745    fn test_execute_order_by() {
3746        let mut shell = Shell::new();
3747
3748        // Create table with data
3749        shell.execute("CREATE TABLE order_test (id INT, value INT)");
3750        shell.execute("INSERT INTO order_test VALUES (1, 100)");
3751        shell.execute("INSERT INTO order_test VALUES (2, 50)");
3752        shell.execute("INSERT INTO order_test VALUES (3, 75)");
3753
3754        // Query with ORDER BY
3755        let result = shell.execute("SELECT * FROM order_test ORDER BY value");
3756        let _ = result;
3757    }
3758
3759    #[test]
3760    fn test_execute_limit() {
3761        let mut shell = Shell::new();
3762
3763        // Create table with many rows
3764        shell.execute("CREATE TABLE limit_test (id INT)");
3765        for i in 1..=10 {
3766            shell.execute(&format!("INSERT INTO limit_test VALUES ({i})"));
3767        }
3768
3769        // Query with LIMIT
3770        let result = shell.execute("SELECT * FROM limit_test LIMIT 3");
3771        let _ = result;
3772    }
3773
3774    #[test]
3775    fn test_execute_describe() {
3776        let mut shell = Shell::new();
3777
3778        // Create table
3779        shell.execute("CREATE TABLE desc_test (id INT, name TEXT, active BOOL)");
3780
3781        // Describe table
3782        let result = shell.execute("DESCRIBE desc_test");
3783        let _ = result;
3784    }
3785
3786    #[test]
3787    fn test_handle_save_to_readonly_path() {
3788        let shell = Shell::new();
3789
3790        // Try to save to a path that likely doesn't exist/isn't writable
3791        let result = shell.handle_save("save '/nonexistent/path/that/should/not/work/test.bin'");
3792        assert!(matches!(result, CommandResult::Error(_)));
3793    }
3794
3795    #[test]
3796    fn test_handle_save_compressed_to_readonly_path() {
3797        let shell = Shell::new();
3798
3799        // Try to save compressed to a path that likely doesn't exist
3800        let result = shell.handle_save_compressed("save compressed '/nonexistent/path/test.bin'");
3801        assert!(matches!(result, CommandResult::Error(_)));
3802    }
3803
3804    #[test]
3805    fn test_execute_vault_operations() {
3806        let mut shell = Shell::new();
3807
3808        // Vault commands without init should fail gracefully
3809        let result = shell.execute("VAULT GET 'test_key'");
3810        let _ = result;
3811
3812        let result = shell.execute("VAULT LIST");
3813        let _ = result;
3814    }
3815
3816    #[test]
3817    fn test_execute_chain_operations() {
3818        let mut shell = Shell::new();
3819
3820        // Chain queries without cluster should handle gracefully
3821        let result = shell.execute("CHAIN BLOCK 1");
3822        let _ = result;
3823
3824        let result = shell.execute("CHAIN LAST");
3825        let _ = result;
3826    }
3827
3828    #[test]
3829    fn test_execute_complex_graph_query() {
3830        let mut shell = Shell::new();
3831
3832        // Create a more complex graph
3833        shell.execute("NODE CREATE person {name: 'Alice', age: 30}");
3834        shell.execute("NODE CREATE person {name: 'Bob', age: 25}");
3835        shell.execute("NODE CREATE person {name: 'Charlie', age: 35}");
3836        shell.execute("NODE CREATE person {name: 'Diana', age: 28}");
3837
3838        shell.execute("EDGE CREATE knows 1 2 {since: 2020}");
3839        shell.execute("EDGE CREATE knows 2 3 {since: 2019}");
3840        shell.execute("EDGE CREATE knows 3 4 {since: 2021}");
3841        shell.execute("EDGE CREATE knows 1 4 {since: 2018}");
3842
3843        // Try various graph queries
3844        let result = shell.execute("NEIGHBORS 1 OUT");
3845        let _ = result;
3846
3847        let result = shell.execute("NEIGHBORS 2 IN");
3848        let _ = result;
3849
3850        let result = shell.execute("PATH 1 3");
3851        let _ = result;
3852    }
3853
3854    #[test]
3855    fn test_shell_execute_line_error() {
3856        let mut shell = Shell::new();
3857
3858        // Execute invalid syntax
3859        let result = shell.execute_line("@#$%^&*()");
3860        assert!(result.is_err());
3861    }
3862
3863    #[test]
3864    fn test_parse_node_address_missing_at() {
3865        let result = Shell::parse_node_address("node1-127.0.0.1:8080");
3866        assert!(result.is_err());
3867        let err = result.unwrap_err();
3868        assert!(err.contains("Expected format"));
3869    }
3870
3871    #[test]
3872    fn test_parse_node_address_empty_node_id() {
3873        let result = Shell::parse_node_address("@127.0.0.1:8080");
3874        assert!(result.is_ok()); // Empty node ID is valid parsing-wise
3875    }
3876
3877    #[test]
3878    fn test_handle_cluster_disconnect_not_connected() {
3879        let shell = Shell::new();
3880        let result = shell.handle_cluster_disconnect();
3881        assert!(matches!(result, CommandResult::Error(_)));
3882        if let CommandResult::Error(msg) = result {
3883            assert!(msg.contains("Not connected"));
3884        }
3885    }
3886
3887    #[test]
3888    fn test_cluster_disconnect_via_execute() {
3889        let mut shell = Shell::new();
3890        let result = shell.execute("CLUSTER DISCONNECT");
3891        assert!(matches!(result, CommandResult::Error(_)));
3892    }
3893
3894    #[test]
3895    fn test_cluster_status_via_execute() {
3896        let mut shell = Shell::new();
3897        let result = shell.execute("CLUSTER STATUS");
3898        // Should work whether connected or not
3899        let _ = result;
3900    }
3901
3902    #[test]
3903    fn test_cluster_nodes_via_execute() {
3904        let mut shell = Shell::new();
3905        let result = shell.execute("CLUSTER NODES");
3906        let _ = result;
3907    }
3908
3909    #[test]
3910    fn test_cluster_leader_via_execute() {
3911        let mut shell = Shell::new();
3912        let result = shell.execute("CLUSTER LEADER");
3913        let _ = result;
3914    }
3915
3916    #[test]
3917    fn test_detect_embedding_dimension_sparse() {
3918        let store = TensorStore::new();
3919
3920        // Add sparse vector
3921        let sparse = tensor_store::SparseVector::from_parts(1000, vec![0, 10], vec![0.5, 0.3]);
3922        let mut data = tensor_store::TensorData::new();
3923        data.set("embedding", tensor_store::TensorValue::Sparse(sparse));
3924        store.put("sparse_key", data).unwrap();
3925
3926        let dim = Shell::detect_embedding_dimension(&store);
3927        assert_eq!(dim, 1000);
3928    }
3929
3930    #[test]
3931    fn test_handle_vault_init_invalid_base64() {
3932        std::env::set_var("NEUMANN_VAULT_KEY", "not-valid-base64!!!");
3933        let shell = Shell::new();
3934        let result = shell.handle_vault_init();
3935        std::env::remove_var("NEUMANN_VAULT_KEY");
3936        assert!(matches!(result, CommandResult::Error(_)));
3937        if let CommandResult::Error(msg) = result {
3938            assert!(msg.contains("base64") || msg.contains("Invalid"));
3939        }
3940    }
3941
3942    #[test]
3943    fn test_handle_cache_init_success() {
3944        let shell = Shell::new();
3945        let result = shell.handle_cache_init();
3946        // May succeed or fail based on state, just check it doesn't panic
3947        assert!(
3948            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
3949        );
3950    }
3951
3952    #[test]
3953    fn test_handle_vault_identity_get_current() {
3954        let shell = Shell::new();
3955        let result = shell.handle_vault_identity("vault identity");
3956        assert!(matches!(result, CommandResult::Output(_)));
3957        if let CommandResult::Output(msg) = result {
3958            assert!(msg.contains("Current identity"));
3959        }
3960    }
3961
3962    #[test]
3963    fn test_handle_vault_identity_set_unquoted() {
3964        let shell = Shell::new();
3965        let result = shell.handle_vault_identity("vault identity admin");
3966        assert!(matches!(result, CommandResult::Output(_)));
3967        if let CommandResult::Output(msg) = result {
3968            assert!(msg.contains("Identity set"));
3969        }
3970    }
3971
3972    #[test]
3973    fn test_handle_vault_identity_preserves_case() {
3974        // Regression: handler used to lowercase the identity.
3975        let shell = Shell::new();
3976        let result = shell.handle_vault_identity("VAULT IDENTITY Alice:Prod");
3977        if let CommandResult::Output(msg) = result {
3978            assert!(
3979                msg.contains("Alice:Prod"),
3980                "identity should preserve case, got: {msg}"
3981            );
3982        } else {
3983            panic!("expected Output");
3984        }
3985    }
3986
3987    #[test]
3988    fn test_handle_vault_identity_preserves_quoted_case() {
3989        let shell = Shell::new();
3990        let result = shell.handle_vault_identity("vault identity 'Bob:Admin'");
3991        if let CommandResult::Output(msg) = result {
3992            assert!(
3993                msg.contains("Bob:Admin"),
3994                "quoted identity should preserve case, got: {msg}"
3995            );
3996        } else {
3997            panic!("expected Output");
3998        }
3999    }
4000
4001    #[test]
4002    fn test_list_tables_method() {
4003        let shell = Shell::new();
4004        let result = shell.list_tables();
4005        assert!(matches!(result, CommandResult::Output(_)));
4006    }
4007
4008    #[test]
4009    fn test_format_wal_replay_result_with_errors_strict() {
4010        let errors = vec![WalReplayError::new(1, "cmd", "error".to_string())];
4011        let result = WalReplayResult {
4012            replayed: 5,
4013            errors,
4014        };
4015        // In strict mode, errors are not shown (we would have failed)
4016        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Strict);
4017        assert!(formatted.contains("Replayed 5"));
4018        assert!(!formatted.contains("Warning"));
4019    }
4020
4021    #[test]
4022    fn test_format_wal_replay_result_with_errors_recover() {
4023        let errors = vec![
4024            WalReplayError::new(1, "cmd1", "error1".to_string()),
4025            WalReplayError::new(2, "cmd2", "error2".to_string()),
4026        ];
4027        let result = WalReplayResult {
4028            replayed: 3,
4029            errors,
4030        };
4031        let formatted = Shell::format_wal_replay_result(&result, WalRecoveryMode::Recover);
4032        assert!(formatted.contains("Warning"));
4033        assert!(formatted.contains("2 corrupted"));
4034    }
4035
4036    #[test]
4037    fn test_execute_wal_status_via_execute() {
4038        let mut shell = Shell::new();
4039        let result = shell.execute("WAL STATUS");
4040        assert!(matches!(result, CommandResult::Output(_)));
4041    }
4042
4043    #[test]
4044    fn test_execute_wal_truncate_via_execute() {
4045        let mut shell = Shell::new();
4046        let result = shell.execute("WAL TRUNCATE");
4047        assert!(matches!(result, CommandResult::Error(_)));
4048    }
4049
4050    #[test]
4051    fn test_execute_vault_init() {
4052        let mut shell = Shell::new();
4053        std::env::remove_var("NEUMANN_VAULT_KEY");
4054        let result = shell.execute("VAULT INIT");
4055        assert!(matches!(result, CommandResult::Error(_)));
4056    }
4057
4058    #[test]
4059    fn test_handle_save_empty_path() {
4060        let shell = Shell::new();
4061        let result = shell.handle_save("save");
4062        assert!(matches!(result, CommandResult::Error(_)));
4063    }
4064
4065    #[test]
4066    fn test_handle_save_compressed_empty_path() {
4067        let shell = Shell::new();
4068        let result = shell.handle_save_compressed("save compressed");
4069        assert!(matches!(result, CommandResult::Error(_)));
4070    }
4071
4072    #[test]
4073    fn test_handle_load_empty_path() {
4074        let shell = Shell::new();
4075        let result = shell.handle_load("load");
4076        assert!(matches!(result, CommandResult::Error(_)));
4077    }
4078
4079    #[test]
4080    fn test_handle_load_nonexistent_file() {
4081        let shell = Shell::new();
4082        let result = shell.handle_load("load '/nonexistent/path/file.bin'");
4083        assert!(matches!(result, CommandResult::Error(_)));
4084    }
4085
4086    #[test]
4087    fn test_router_executor_success() {
4088        let router = Arc::new(RwLock::new(QueryRouter::new()));
4089        let executor = RouterExecutor(Arc::clone(&router));
4090
4091        // Create table
4092        let result = executor.execute("CREATE TABLE exec_test (id INT)");
4093        assert!(result.is_ok() || result.is_err());
4094    }
4095
4096    #[test]
4097    fn test_cluster_connect_invalid_local() {
4098        let shell = Shell::new();
4099        let result = shell.handle_cluster_connect("cluster connect 'invalid-no-at-sign'");
4100        assert!(matches!(result, CommandResult::Error(_)));
4101    }
4102
4103    #[test]
4104    fn test_cluster_connect_invalid_peer() {
4105        let shell = Shell::new();
4106        let result =
4107            shell.handle_cluster_connect("cluster connect 'node1@127.0.0.1:8080' 'invalid-peer'");
4108        assert!(matches!(result, CommandResult::Error(_)));
4109    }
4110
4111    #[test]
4112    fn test_execute_similar_with_vector() {
4113        let mut shell = Shell::new();
4114
4115        // Store embeddings first
4116        shell.execute("EMBED STORE 'test1' [0.1, 0.2, 0.3]");
4117        shell.execute("EMBED STORE 'test2' [0.2, 0.3, 0.4]");
4118
4119        // Search with vector
4120        let result = shell.execute("SIMILAR [0.15, 0.25, 0.35] LIMIT 5");
4121        let _ = result;
4122    }
4123
4124    #[test]
4125    fn test_execute_graph_shortest_path() {
4126        let mut shell = Shell::new();
4127
4128        // Create graph
4129        shell.execute("NODE CREATE vertex {id: 'a'}");
4130        shell.execute("NODE CREATE vertex {id: 'b'}");
4131        shell.execute("NODE CREATE vertex {id: 'c'}");
4132        shell.execute("EDGE CREATE link 1 2");
4133        shell.execute("EDGE CREATE link 2 3");
4134
4135        // Find path
4136        let result = shell.execute("PATH 1 3");
4137        let _ = result;
4138    }
4139
4140    #[test]
4141    fn test_execute_show_embeddings() {
4142        let mut shell = Shell::new();
4143
4144        shell.execute("EMBED STORE 'key1' [0.1, 0.2]");
4145        let result = shell.execute("SHOW EMBEDDINGS");
4146        let _ = result;
4147    }
4148
4149    #[test]
4150    fn test_dirs_home_with_env() {
4151        // This will use the actual HOME env var
4152        let home = dirs_home();
4153        // HOME should be set in most environments
4154        if std::env::var("HOME").is_ok() {
4155            assert!(home.is_some());
4156        }
4157    }
4158
4159    #[test]
4160    fn test_router_arc_method() {
4161        let shell = Shell::new();
4162        let router_arc = shell.router_arc();
4163        // Verify we can use the arc
4164        let _guard = router_arc.read();
4165    }
4166
4167    #[test]
4168    fn test_router_read_method() {
4169        let shell = Shell::new();
4170        let guard = shell.router();
4171        // Verify we can access through the guard
4172        let _ = guard.execute_parsed("SELECT 1");
4173    }
4174
4175    #[test]
4176    fn test_router_mut_method() {
4177        let shell = Shell::new();
4178        let _guard = shell.router_mut();
4179        // Just verify we can get a write lock
4180    }
4181
4182    #[test]
4183    fn test_replay_wal_recover_mode_with_errors() {
4184        use std::io::Write;
4185
4186        let shell = Shell::new();
4187        let temp_dir = std::env::temp_dir();
4188        let wal_path = temp_dir.join("test_replay_recover_error.log");
4189
4190        // Create WAL with mix of valid and invalid
4191        {
4192            let mut file = std::fs::File::create(&wal_path).unwrap();
4193            writeln!(file, "CREATE TABLE recover_test (id INT)").unwrap();
4194            writeln!(file, "@#$%INVALID!@#$").unwrap();
4195            writeln!(file, "INSERT INTO recover_test VALUES (1)").unwrap();
4196        }
4197
4198        let result = shell.replay_wal(&wal_path, WalRecoveryMode::Recover);
4199        assert!(result.is_ok());
4200        let replay_result = result.unwrap();
4201        assert!(replay_result.replayed > 0);
4202        assert!(!replay_result.errors.is_empty());
4203
4204        // Clean up
4205        let _ = std::fs::remove_file(&wal_path);
4206    }
4207
4208    #[test]
4209    fn test_replay_wal_nonexistent() {
4210        let shell = Shell::new();
4211        let result = shell.replay_wal(
4212            std::path::Path::new("/nonexistent/path/wal.log"),
4213            WalRecoveryMode::Strict,
4214        );
4215        assert!(result.is_err());
4216    }
4217
4218    #[test]
4219    fn test_execute_with_wal_logging() {
4220        let mut shell = Shell::new();
4221        let temp_dir = std::env::temp_dir();
4222        let path = temp_dir.join("test_wal_execute_log.bin");
4223        let path_str = path.to_string_lossy().to_string();
4224
4225        // Save and load to activate WAL
4226        shell.execute(&format!("SAVE '{path_str}'"));
4227        shell.execute(&format!("LOAD '{path_str}'"));
4228
4229        // Now writes should be logged
4230        shell.execute("CREATE TABLE wal_exec_log_test (id INT)");
4231        shell.execute("INSERT INTO wal_exec_log_test VALUES (1)");
4232        shell.execute("UPDATE wal_exec_log_test SET id = 2 WHERE id = 1");
4233        shell.execute("DELETE FROM wal_exec_log_test WHERE id = 2");
4234        shell.execute("DROP TABLE wal_exec_log_test");
4235
4236        // Clean up
4237        let _ = std::fs::remove_file(&path);
4238        let _ = std::fs::remove_file(path.with_extension("log"));
4239    }
4240
4241    #[test]
4242    fn test_execute_uses_spinner_for_similar() {
4243        let mut shell = Shell::new();
4244
4245        // Store embeddings
4246        shell.execute("EMBED STORE 'spinner1' [0.1, 0.2, 0.3]");
4247        shell.execute("EMBED STORE 'spinner2' [0.2, 0.3, 0.4]");
4248
4249        // SIMILAR should use spinner
4250        let result = shell.execute("SIMILAR 'spinner1' LIMIT 5 COSINE");
4251        let _ = result;
4252    }
4253
4254    #[test]
4255    fn test_execute_graph_traversal() {
4256        let mut shell = Shell::new();
4257
4258        // Create graph
4259        shell.execute("NODE CREATE person {name: 'A'}");
4260        shell.execute("NODE CREATE person {name: 'B'}");
4261        shell.execute("NODE CREATE person {name: 'C'}");
4262        shell.execute("EDGE CREATE knows 1 2");
4263        shell.execute("EDGE CREATE knows 2 3");
4264
4265        // Traversal operations
4266        let result = shell.execute("NEIGHBORS 1");
4267        let _ = result;
4268    }
4269
4270    #[test]
4271    fn test_execute_describe_table() {
4272        let mut shell = Shell::new();
4273
4274        shell.execute("CREATE TABLE describe_test (id INT, name TEXT)");
4275        let result = shell.execute("DESCRIBE describe_test");
4276        let _ = result;
4277    }
4278
4279    #[test]
4280    fn test_shell_config_with_all_fields() {
4281        let config = ShellConfig {
4282            history_file: Some(std::path::PathBuf::from("/tmp/test_history")),
4283            history_size: 2000,
4284            prompt: "test> ".to_string(),
4285            theme: Theme::plain(),
4286            no_color: true,
4287            no_boot: true,
4288            quiet: true,
4289        };
4290        let shell = Shell::with_config(config);
4291        assert_eq!(shell.config.history_size, 2000);
4292        assert!(shell.config.quiet);
4293    }
4294
4295    #[test]
4296    fn test_execute_various_query_results() {
4297        let mut shell = Shell::new();
4298
4299        // Create data
4300        shell.execute("CREATE TABLE query_test (id INT, name TEXT, active BOOL)");
4301        shell.execute("INSERT INTO query_test VALUES (1, 'Alice', true)");
4302        shell.execute("INSERT INTO query_test VALUES (2, 'Bob', false)");
4303
4304        // Various queries - just verify they run without panic
4305        let result = shell.execute("SELECT * FROM query_test");
4306        let _ = result;
4307
4308        let result = shell.execute("SELECT * FROM query_test WHERE active = true");
4309        let _ = result;
4310
4311        let result = shell.execute("SELECT COUNT(*) FROM query_test");
4312        let _ = result;
4313    }
4314
4315    #[test]
4316    fn test_execute_node_neighbors_both() {
4317        let mut shell = Shell::new();
4318
4319        shell.execute("NODE CREATE item {}");
4320        shell.execute("NODE CREATE item {}");
4321        shell.execute("NODE CREATE item {}");
4322        shell.execute("EDGE CREATE link 1 2");
4323        shell.execute("EDGE CREATE link 3 1");
4324
4325        let result = shell.execute("NEIGHBORS 1 BOTH");
4326        let _ = result;
4327    }
4328
4329    #[test]
4330    fn test_execute_blob_info() {
4331        let mut shell = Shell::new();
4332        let result = shell.execute("BLOB INFO 'nonexistent'");
4333        let _ = result;
4334    }
4335
4336    #[test]
4337    fn test_execute_checkpoint_info() {
4338        let mut shell = Shell::new();
4339        let result = shell.execute("CHECKPOINT INFO 'nonexistent'");
4340        let _ = result;
4341    }
4342
4343    #[test]
4344    fn test_execute_chain_tip() {
4345        let mut shell = Shell::new();
4346        let result = shell.execute("CHAIN TIP");
4347        let _ = result;
4348    }
4349
4350    #[test]
4351    fn test_execute_chain_verify() {
4352        let mut shell = Shell::new();
4353        let result = shell.execute("CHAIN VERIFY");
4354        let _ = result;
4355    }
4356
4357    #[test]
4358    fn test_execute_cache_evict() {
4359        let mut shell = Shell::new();
4360        let result = shell.execute("CACHE EVICT");
4361        let _ = result;
4362    }
4363
4364    #[test]
4365    fn test_execute_vault_list() {
4366        let mut shell = Shell::new();
4367        let result = shell.execute("VAULT LIST");
4368        let _ = result;
4369    }
4370
4371    #[test]
4372    fn test_execute_show_vector() {
4373        let mut shell = Shell::new();
4374        let result = shell.execute("SHOW VECTOR INDEX");
4375        let _ = result;
4376    }
4377
4378    #[test]
4379    fn test_execute_show_codebook() {
4380        let mut shell = Shell::new();
4381        let result = shell.execute("SHOW CODEBOOK GLOBAL");
4382        let _ = result;
4383    }
4384
4385    #[test]
4386    fn test_shell_error_std_error_trait() {
4387        let error = ShellError::Init("test".to_string());
4388        // Verify it implements std::error::Error
4389        let _: &dyn std::error::Error = &error;
4390    }
4391
4392    #[test]
4393    fn test_shell_config_default_has_history() {
4394        let config = ShellConfig::default();
4395        assert!(config.history_file.is_some());
4396        assert!(config.history_size > 0);
4397    }
4398
4399    #[test]
4400    fn test_command_result_variants() {
4401        let empty = CommandResult::Empty;
4402        let exit = CommandResult::Exit;
4403        let output = CommandResult::Output("out".to_string());
4404        let error = CommandResult::Error("err".to_string());
4405        let help = CommandResult::Help("help".to_string());
4406
4407        // Verify all variants are different
4408        assert_ne!(empty, exit);
4409        assert_ne!(empty, output);
4410        assert_ne!(empty, error);
4411        assert_ne!(empty, help);
4412    }
4413
4414    #[test]
4415    fn test_execute_all_graph_algorithms() {
4416        let mut shell = Shell::new();
4417
4418        // Create a connected graph
4419        shell.execute("NODE CREATE vertex {label: 'A'}");
4420        shell.execute("NODE CREATE vertex {label: 'B'}");
4421        shell.execute("NODE CREATE vertex {label: 'C'}");
4422        shell.execute("NODE CREATE vertex {label: 'D'}");
4423        shell.execute("EDGE CREATE link 1 2");
4424        shell.execute("EDGE CREATE link 2 3");
4425        shell.execute("EDGE CREATE link 3 4");
4426        shell.execute("EDGE CREATE link 4 1");
4427        shell.execute("EDGE CREATE link 1 3");
4428
4429        // Test various graph algorithms
4430        let _ = shell.execute("GRAPH ALGORITHM PAGERANK");
4431        let _ = shell.execute("GRAPH ALGORITHM BETWEENNESS");
4432        let _ = shell.execute("GRAPH ALGORITHM CLOSENESS");
4433        let _ = shell.execute("GRAPH ALGORITHM EIGENVECTOR");
4434        let _ = shell.execute("GRAPH ALGORITHM LOUVAIN");
4435        let _ = shell.execute("GRAPH ALGORITHM LABEL_PROPAGATION");
4436    }
4437
4438    #[test]
4439    fn test_execute_full_snapshot_cycle() {
4440        let mut shell = Shell::new();
4441        let temp_dir = std::env::temp_dir();
4442        let snapshot_path = temp_dir.join("test_full_cycle.bin");
4443        let path_str = snapshot_path.to_string_lossy().to_string();
4444
4445        // Create some data
4446        shell.execute("CREATE TABLE cycle_test (id INT, value TEXT)");
4447        shell.execute("INSERT INTO cycle_test VALUES (1, 'one')");
4448        shell.execute("INSERT INTO cycle_test VALUES (2, 'two')");
4449        shell.execute("NODE CREATE item {name: 'test'}");
4450        shell.execute("EMBED STORE 'cycle:key' [0.1, 0.2, 0.3, 0.4]");
4451
4452        // Save snapshot
4453        let result = shell.execute(&format!("SAVE '{path_str}'"));
4454        assert!(matches!(result, CommandResult::Output(_)));
4455
4456        // Load snapshot
4457        let result = shell.execute(&format!("LOAD '{path_str}'"));
4458        assert!(matches!(result, CommandResult::Output(_)));
4459
4460        // Verify data exists
4461        let _ = shell.execute("SELECT * FROM cycle_test");
4462        let _ = shell.execute("NODE LIST item");
4463        let _ = shell.execute("EMBED GET 'cycle:key'");
4464
4465        // Clean up
4466        let _ = std::fs::remove_file(&snapshot_path);
4467        let _ = std::fs::remove_file(snapshot_path.with_extension("log"));
4468    }
4469
4470    #[test]
4471    fn test_execute_compressed_snapshot() {
4472        let mut shell = Shell::new();
4473        let temp_dir = std::env::temp_dir();
4474        let snapshot_path = temp_dir.join("test_compressed.bin");
4475        let path_str = snapshot_path.to_string_lossy().to_string();
4476
4477        // Create some data
4478        shell.execute("EMBED STORE 'comp:1' [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]");
4479        shell.execute("EMBED STORE 'comp:2' [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]");
4480
4481        // Save compressed
4482        let result = shell.execute(&format!("SAVE COMPRESSED '{path_str}'"));
4483        assert!(
4484            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
4485        );
4486
4487        // Clean up
4488        let _ = std::fs::remove_file(&snapshot_path);
4489        let _ = std::fs::remove_file(snapshot_path.with_extension("log"));
4490    }
4491
4492    #[test]
4493    fn test_execute_with_uppercase_variants() {
4494        let mut shell = Shell::new();
4495
4496        // Test uppercase versions of commands
4497        let _ = shell.execute("HELP");
4498        let _ = shell.execute("TABLES");
4499        let _ = shell.execute("CLEAR");
4500        let _ = shell.execute("WAL STATUS");
4501    }
4502
4503    #[test]
4504    fn test_wal_active_after_load() {
4505        let mut shell = Shell::new();
4506        let temp_dir = std::env::temp_dir();
4507        let snapshot_path = temp_dir.join("test_wal_active.bin");
4508        let path_str = snapshot_path.to_string_lossy().to_string();
4509
4510        // Save
4511        shell.execute(&format!("SAVE '{path_str}'"));
4512
4513        // Load - should activate WAL
4514        shell.execute(&format!("LOAD '{path_str}'"));
4515
4516        // WAL status should show active
4517        let result = shell.execute("WAL STATUS");
4518        if let CommandResult::Output(msg) = result {
4519            assert!(msg.contains("WAL enabled") || msg.contains("Path:"));
4520        }
4521
4522        // Clean up
4523        let _ = std::fs::remove_file(&snapshot_path);
4524        let _ = std::fs::remove_file(snapshot_path.with_extension("log"));
4525    }
4526
4527    #[test]
4528    fn test_wal_truncate_after_load() {
4529        let mut shell = Shell::new();
4530        let temp_dir = std::env::temp_dir();
4531        let snapshot_path = temp_dir.join("test_wal_truncate.bin");
4532        let path_str = snapshot_path.to_string_lossy().to_string();
4533
4534        // Save and load to activate WAL
4535        shell.execute(&format!("SAVE '{path_str}'"));
4536        shell.execute(&format!("LOAD '{path_str}'"));
4537
4538        // Truncate WAL
4539        let result = shell.execute("WAL TRUNCATE");
4540        assert!(matches!(result, CommandResult::Output(_)));
4541
4542        // Clean up
4543        let _ = std::fs::remove_file(&snapshot_path);
4544        let _ = std::fs::remove_file(snapshot_path.with_extension("log"));
4545    }
4546
4547    #[test]
4548    fn test_execute_select_with_limit() {
4549        let mut shell = Shell::new();
4550
4551        shell.execute("CREATE TABLE limit_test2 (id INT)");
4552        for i in 1..=20 {
4553            shell.execute(&format!("INSERT INTO limit_test2 VALUES ({i})"));
4554        }
4555
4556        let result = shell.execute("SELECT * FROM limit_test2 LIMIT 5");
4557        let _ = result;
4558    }
4559
4560    #[test]
4561    fn test_execute_update_with_where() {
4562        let mut shell = Shell::new();
4563
4564        shell.execute("CREATE TABLE update_test2 (id INT, name TEXT)");
4565        shell.execute("INSERT INTO update_test2 VALUES (1, 'old')");
4566        shell.execute("INSERT INTO update_test2 VALUES (2, 'keep')");
4567
4568        let result = shell.execute("UPDATE update_test2 SET name = 'new' WHERE id = 1");
4569        let _ = result;
4570    }
4571
4572    #[test]
4573    fn test_execute_delete_with_where() {
4574        let mut shell = Shell::new();
4575
4576        shell.execute("CREATE TABLE delete_test2 (id INT)");
4577        shell.execute("INSERT INTO delete_test2 VALUES (1)");
4578        shell.execute("INSERT INTO delete_test2 VALUES (2)");
4579
4580        let result = shell.execute("DELETE FROM delete_test2 WHERE id = 1");
4581        let _ = result;
4582    }
4583
4584    #[test]
4585    fn test_replay_wal_mixed_commands() {
4586        use std::io::Write;
4587
4588        let shell = Shell::new();
4589        let temp_dir = std::env::temp_dir();
4590        let wal_path = temp_dir.join("test_replay_mixed.log");
4591
4592        // Create WAL with various commands
4593        {
4594            let mut file = std::fs::File::create(&wal_path).unwrap();
4595            writeln!(file, "CREATE TABLE replay_mix (id INT)").unwrap();
4596            writeln!(file, "INSERT INTO replay_mix VALUES (1)").unwrap();
4597            writeln!(file, "INSERT INTO replay_mix VALUES (2)").unwrap();
4598            writeln!(file).unwrap(); // empty line
4599            writeln!(file, "  ").unwrap(); // whitespace
4600            writeln!(file, "INSERT INTO replay_mix VALUES (3)").unwrap();
4601        }
4602
4603        let result = shell.replay_wal(&wal_path, WalRecoveryMode::Strict);
4604        assert!(result.is_ok());
4605        let replay = result.unwrap();
4606        assert_eq!(replay.replayed, 4);
4607        assert!(replay.errors.is_empty());
4608
4609        // Clean up
4610        let _ = std::fs::remove_file(&wal_path);
4611    }
4612
4613    #[test]
4614    fn test_cluster_connect_full_flow() {
4615        let shell = Shell::new();
4616
4617        // Try connecting with multiple peers
4618        let result = shell.handle_cluster_connect(
4619            "cluster connect 'node1@127.0.0.1:9000' 'node2@127.0.0.1:9001' 'node3@127.0.0.1:9002'",
4620        );
4621        // Will fail to actually connect but should parse addresses correctly
4622        assert!(matches!(result, CommandResult::Error(_)));
4623    }
4624
4625    #[test]
4626    fn test_is_write_command_comprehensive() {
4627        // Comprehensive test of all write command patterns
4628        assert!(Shell::is_write_command("VAULT GRANT read key TO user"));
4629        assert!(Shell::is_write_command("VAULT REVOKE write key FROM user"));
4630        assert!(Shell::is_write_command("CACHE CLEAR"));
4631        assert!(Shell::is_write_command("BLOB META SET hash key val"));
4632        assert!(Shell::is_write_command("GRAPH BATCH CREATE nodes"));
4633        assert!(Shell::is_write_command("GRAPH CONSTRAINT CREATE unique"));
4634        assert!(Shell::is_write_command("GRAPH INDEX CREATE idx"));
4635        assert!(Shell::is_write_command("BEGIN CHAIN transaction"));
4636        assert!(Shell::is_write_command("COMMIT CHAIN"));
4637    }
4638
4639    // ==================== process_result tests ====================
4640
4641    #[test]
4642    fn test_process_result_output() {
4643        let result = CommandResult::Output("test output".to_string());
4644        let action = Shell::process_result(&result);
4645        assert!(matches!(action, LoopAction::Continue));
4646    }
4647
4648    #[test]
4649    fn test_process_result_help() {
4650        let result = CommandResult::Help("help text".to_string());
4651        let action = Shell::process_result(&result);
4652        assert!(matches!(action, LoopAction::Continue));
4653    }
4654
4655    #[test]
4656    fn test_process_result_error() {
4657        let result = CommandResult::Error("error message".to_string());
4658        let action = Shell::process_result(&result);
4659        assert!(matches!(action, LoopAction::Continue));
4660    }
4661
4662    #[test]
4663    fn test_process_result_exit() {
4664        let result = CommandResult::Exit;
4665        let action = Shell::process_result(&result);
4666        assert!(matches!(action, LoopAction::Exit));
4667    }
4668
4669    #[test]
4670    fn test_process_result_empty() {
4671        let result = CommandResult::Empty;
4672        let action = Shell::process_result(&result);
4673        assert!(matches!(action, LoopAction::Continue));
4674    }
4675
4676    // ==================== version test ====================
4677
4678    #[test]
4679    fn test_shell_version() {
4680        let version = Shell::version();
4681        assert!(!version.is_empty());
4682        // Version should be a semver-like string
4683        assert!(version.contains('.'));
4684    }
4685
4686    // ==================== router accessors tests ====================
4687
4688    #[test]
4689    fn test_router_arc_accessor() {
4690        let shell = Shell::new();
4691        let router_arc = shell.router_arc();
4692        // Should be able to read from the router
4693        let _guard = router_arc.read();
4694    }
4695
4696    #[test]
4697    fn test_router_read_accessor() {
4698        let shell = Shell::new();
4699        let _router = shell.router();
4700        // Router read guard should work
4701    }
4702
4703    #[test]
4704    fn test_router_mut_accessor() {
4705        let shell = Shell::new();
4706        let _router = shell.router_mut();
4707        // Router write guard should work
4708    }
4709
4710    // ==================== execute edge cases ====================
4711
4712    #[test]
4713    fn test_execute_whitespace_only() {
4714        let mut shell = Shell::new();
4715        let result = shell.execute("   \t\n  ");
4716        assert!(matches!(result, CommandResult::Empty));
4717    }
4718
4719    #[test]
4720    fn test_execute_semicolon_only() {
4721        let mut shell = Shell::new();
4722        let result = shell.execute(";");
4723        // Semicolon-only may return various results
4724        let _ = result;
4725    }
4726
4727    #[test]
4728    fn test_execute_comment_only() {
4729        let mut shell = Shell::new();
4730        let result = shell.execute("-- this is a comment");
4731        // Comment-only may return various results
4732        let _ = result;
4733    }
4734
4735    #[test]
4736    fn test_execute_clear_command() {
4737        let mut shell = Shell::new();
4738        let result = shell.execute("clear");
4739        assert!(matches!(result, CommandResult::Output(_)));
4740    }
4741
4742    #[test]
4743    fn test_execute_backslash_c() {
4744        let mut shell = Shell::new();
4745        let result = shell.execute("\\c");
4746        assert!(matches!(result, CommandResult::Output(_)));
4747    }
4748
4749    #[test]
4750    fn test_execute_backslash_q() {
4751        let mut shell = Shell::new();
4752        let result = shell.execute("\\q");
4753        assert!(matches!(result, CommandResult::Exit));
4754    }
4755
4756    // ==================== help text test ====================
4757
4758    #[test]
4759    fn test_help_text_contains_commands() {
4760        let help = Shell::help_text();
4761        assert!(help.contains("Commands"));
4762    }
4763
4764    // ==================== execute_line error case ====================
4765
4766    #[test]
4767    fn test_execute_line_with_error() {
4768        let mut shell = Shell::new();
4769        // Invalid SQL should return error
4770        let result = shell.execute_line("SELECT * FROM nonexistent_weird_table_xyz");
4771        // Could be Ok or Err depending on implementation
4772        let _ = result;
4773    }
4774
4775    // ==================== Additional handler tests ====================
4776
4777    #[test]
4778    fn test_list_tables_empty_database() {
4779        let shell = Shell::new();
4780        let result = shell.list_tables();
4781        // Should return output (either "No tables found" or empty list)
4782        assert!(
4783            matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
4784        );
4785    }
4786
4787    #[test]
4788    fn test_handle_wal_truncate_error_case() {
4789        let shell = Shell::new();
4790        // WAL not active should return error
4791        let result = shell.handle_wal_truncate();
4792        assert!(matches!(result, CommandResult::Error(_)));
4793    }
4794}