1pub 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#[derive(Debug, Clone)]
41pub struct ShellConfig {
42 pub history_file: Option<PathBuf>,
44 pub history_size: usize,
46 pub prompt: String,
48 pub theme: Theme,
50 pub no_color: bool,
52 pub no_boot: bool,
54 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 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
73fn dirs_home() -> Option<PathBuf> {
75 std::env::var_os("HOME").map(PathBuf::from)
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum CommandResult {
81 Output(String),
83 Exit,
85 Help(String),
87 Empty,
89 Error(String),
91}
92
93pub struct Shell {
95 router: Arc<RwLock<QueryRouter>>,
96 config: ShellConfig,
97 wal: Mutex<Option<Wal>>,
98 icons: &'static Icons,
99}
100
101struct 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
111struct 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 #[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 #[must_use]
148 pub fn with_config(mut config: ShellConfig) -> Self {
149 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 #[must_use]
171 pub fn router_arc(&self) -> Arc<RwLock<QueryRouter>> {
172 Arc::clone(&self.router)
173 }
174
175 pub fn router(&self) -> parking_lot::RwLockReadGuard<'_, QueryRouter> {
177 self.router.read()
178 }
179
180 pub fn router_mut(&self) -> parking_lot::RwLockWriteGuard<'_, QueryRouter> {
182 self.router.write()
183 }
184
185 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 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 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 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 if lower.starts_with("save compressed") {
309 return self.handle_save_compressed(trimmed);
310 }
311
312 if lower.starts_with("save ") {
314 return self.handle_save(trimmed);
315 }
316
317 if lower.starts_with("load ") {
319 return self.handle_load(trimmed);
320 }
321
322 if lower == "vault init" {
324 return self.handle_vault_init();
325 }
326
327 if lower.starts_with("vault identity") {
329 return self.handle_vault_identity(trimmed);
330 }
331
332 if lower == "cache init" {
334 return self.handle_cache_init();
335 }
336
337 if lower.starts_with("cluster connect") {
339 return self.handle_cluster_connect(trimmed);
340 }
341
342 if lower == "cluster disconnect" {
344 return self.handle_cluster_disconnect();
345 }
346
347 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 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 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 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 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 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 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 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 let wal_path = Path::new(&p).with_extension("log");
479
480 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 let executor: Arc<dyn QueryExecutor> = Arc::new(RouterExecutor(Arc::clone(&self.router)));
834
835 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 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 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 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 #[must_use]
910 pub fn help_text() -> String {
911 output::format_help(&Theme::auto())
912 }
913
914 #[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 #[must_use]
936 pub const fn version() -> &'static str {
937 env!("CARGO_PKG_VERSION")
938 }
939
940 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 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 {
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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1037pub enum LoopAction {
1038 Continue,
1040 Exit,
1042}
1043
1044impl Default for Shell {
1045 fn default() -> Self {
1046 Self::new()
1047 }
1048}
1049
1050#[derive(Debug, Clone, PartialEq, Eq)]
1052pub enum ShellError {
1053 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 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()); }
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 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 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 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 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 let result = shell.execute("vault init");
1813 assert!(matches!(result, CommandResult::Error(_)));
1814
1815 let result = shell.execute("vault identity test_user");
1817 assert!(matches!(result, CommandResult::Output(_)));
1818
1819 let result = shell.execute("cache init");
1821 assert!(
1822 matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
1823 );
1824
1825 let result = shell.execute("cluster connect");
1827 assert!(matches!(result, CommandResult::Error(_)));
1828
1829 let result = shell.execute("cluster disconnect");
1831 assert!(matches!(result, CommandResult::Error(_)));
1832 }
1833
1834 #[test]
1835 fn test_dirs_home() {
1836 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 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 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 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 let result = shell.handle_cluster_connect(
1902 "cluster connect 'node1@127.0.0.1:8080' 'node2@127.0.0.1:8081'",
1903 );
1904 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 assert!(
1930 matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
1931 );
1932 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 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 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 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 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 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 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 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 shell.execute("CREATE TABLE wal_test (id INT)");
2108
2109 let result = shell.execute(&format!("SAVE '{snapshot_str}'"));
2111 assert!(
2112 matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2113 );
2114
2115 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 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 assert_eq!(Shell::extract_load_path_and_mode("LOAD ''"), None);
2153 assert_eq!(Shell::extract_load_path_and_mode("LOAD \"\""), None);
2154
2155 assert_eq!(Shell::extract_load_path_and_mode("load "), None);
2157 }
2158
2159 #[test]
2160 fn test_extract_path_edge_cases() {
2161 assert_eq!(Shell::extract_path("save ", "save"), None);
2163
2164 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 let result = Shell::parse_node_address("@127.0.0.1:8080");
2174 assert!(result.is_ok());
2175
2176 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 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 assert!(
2258 matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2259 );
2260 }
2261
2262 #[test]
2263 fn test_extract_load_with_recover_keyword() {
2264 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 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 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 let result = shell.execute("SIMILAR [0.1, 0.2, 0.3] LIMIT 5");
2367 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 assert!(
2381 matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2382 );
2383 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 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 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 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 assert!(!formatted.contains("Replayed 0"));
2440 }
2441
2442 #[test]
2443 fn test_execute_multiple_backslash_commands() {
2444 let mut shell = Shell::new();
2445 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 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 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 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 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 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 let save_result = shell.execute(&format!("SAVE '{path_str}'"));
2506 assert!(
2507 matches!(save_result, CommandResult::Output(_)),
2508 "Save should succeed"
2509 );
2510
2511 let mut shell2 = Shell::new();
2513 let load_result = shell2.execute(&format!("LOAD '{path_str}'"));
2514 let loaded = matches!(load_result, CommandResult::Output(_));
2516
2517 let _ = std::fs::remove_file(&path);
2519 let _ = std::fs::remove_file(path.with_extension("log"));
2520
2521 if loaded {
2522 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 shell.execute("CREATE TABLE compress_test (id INT)");
2537 shell.execute("INSERT INTO compress_test VALUES (1)");
2538
2539 let result = shell.execute(&format!("SAVE COMPRESSED '{path_str}'"));
2541 let saved = matches!(result, CommandResult::Output(_));
2542
2543 let _ = std::fs::remove_file(&path);
2545 let _ = std::fs::remove_file(path.with_extension("log"));
2546
2547 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 shell.execute(&format!("SAVE '{path_str}'"));
2560
2561 shell.execute(&format!("LOAD '{path_str}'"));
2563
2564 let result = shell.execute("WAL STATUS");
2566 assert!(matches!(result, CommandResult::Output(_)));
2568
2569 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 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 shell.execute(&format!("SAVE '{path_str}'"));
2590 shell.execute(&format!("LOAD '{path_str}'"));
2591
2592 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 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 {
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(); 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 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 {
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(); 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()); 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 {
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(); 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); assert_eq!(result.errors.len(), 1); 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 shell.execute("CREATE TABLE load_replay_test (id INT)");
2702 shell.execute(&format!("SAVE '{snap_str}'"));
2703
2704 {
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 let result = shell.handle_load(&format!("LOAD '{snap_str}'"));
2712 assert!(matches!(result, CommandResult::Output(_)));
2714
2715 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 shell.execute(&format!("SAVE '{snap_str}'"));
2732
2733 {
2735 let mut file = std::fs::File::create(&wal_path).unwrap();
2736 writeln!(file, "INVALID_COMMAND @#$%").unwrap();
2737 }
2738
2739 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")); }
2746
2747 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 shell.execute(&format!("SAVE '{snap_str}'"));
2764
2765 {
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 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 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 shell.execute(&format!("SAVE '{path_str}'"));
2794 shell.execute(&format!("LOAD '{path_str}'"));
2795
2796 shell.execute("CREATE TABLE truncate_test (id INT)");
2798 shell.execute("INSERT INTO truncate_test VALUES (1)");
2799
2800 let result = shell.execute("WAL TRUNCATE");
2802 assert!(
2803 matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2804 );
2805
2806 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 shell.execute(&format!("SAVE '{path_str}'"));
2820 shell.execute(&format!("LOAD '{path_str}'"));
2821
2822 shell.execute("CREATE TABLE status_test (id INT)");
2824
2825 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 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 shell.execute(&format!("SAVE '{path_str}'"));
2845 shell.execute(&format!("LOAD '{path_str}'"));
2846
2847 shell.execute("CREATE TABLE truncate_on_save_test (id INT)");
2849 shell.execute("INSERT INTO truncate_on_save_test VALUES (1)");
2850
2851 let result = shell.execute(&format!("SAVE '{path_str}'"));
2853 assert!(matches!(result, CommandResult::Output(_)));
2854
2855 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 shell.execute(&format!("SAVE '{path_str}'"));
2869 shell.execute(&format!("LOAD '{path_str}'"));
2870
2871 shell.execute("CREATE TABLE compressed_wal_test (id INT)");
2873
2874 let result = shell.execute(&format!("SAVE COMPRESSED '{path_str}'"));
2876 assert!(
2877 matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
2878 );
2879
2880 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 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 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 shell.execute("CREATE TABLE update_test (id INT, name TEXT)");
2905 shell.execute("INSERT INTO update_test VALUES (1, 'Alice')");
2906
2907 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 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 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 shell.execute("NODE CREATE person {name: 'Alice'}");
2936 shell.execute("NODE CREATE person {name: 'Bob'}");
2937
2938 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 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 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 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 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 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 {
3005 let r = router.write();
3006 let _ = r.execute_parsed("CREATE TABLE executor_test (id INT)");
3007 }
3008
3009 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 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 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 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 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 let result = shell.handle_cluster_connect("CLUSTER CONNECT node1@127.0.0.1:8080");
3056 assert!(matches!(result, CommandResult::Error(_)));
3057
3058 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 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 assert!(!shell.icons.success.is_empty());
3082 }
3083
3084 #[test]
3085 fn test_shell_config_history_file_default() {
3086 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 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 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 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 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 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 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 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 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 shell.execute("NODE CREATE entity {id: 1}");
3168
3169 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 shell.execute("NODE CREATE animal {species: 'dog'}");
3182 shell.execute("NODE CREATE animal {species: 'cat'}");
3183
3184 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 shell.execute("NODE CREATE item {name: 'book'}");
3195
3196 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 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 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 shell.execute("CREATE TABLE t1 (id INT)");
3223 shell.execute("CREATE TABLE t2 (id INT)");
3224
3225 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 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 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 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 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 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 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 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 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 assert!(!Shell::is_write_command("")); assert!(!Shell::is_write_command("SELECT")); assert!(Shell::is_write_command("NODE CREATE test {}"));
3330 assert!(Shell::is_write_command("node create test {}")); 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 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 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 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 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 let result = shell.execute("VAULT IDENTITY");
3386 assert!(matches!(result, CommandResult::Output(_)));
3387
3388 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 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 {
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 let _ = std::fs::remove_file(&wal_path);
3427 }
3428
3429 #[test]
3430 fn test_format_wal_replay_result_6_errors() {
3431 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 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 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 shell.execute("EMBED STORE 'spinner_test' [0.1, 0.2, 0.3]");
3469
3470 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 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 let result = shell.handle_save("save /tmp/test_file.bin");
3496 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 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 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 let result = shell.execute("GRAPH MATCH (a)-[r]->(b) WHERE a.name = 'Alice'");
3529 let _ = result;
3531 }
3532
3533 #[test]
3534 fn test_execute_batch_operations() {
3535 let mut shell = Shell::new();
3536
3537 shell.execute("CREATE TABLE batch_test (id INT, name TEXT)");
3539
3540 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 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 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 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 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 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 shell.execute(&format!("SAVE '{path_str}'"));
3625 shell.execute(&format!("LOAD '{path_str}'"));
3626
3627 let result = shell.execute("CREATE TABLE wal_log_test (id INT)");
3629 assert!(matches!(result, CommandResult::Output(_)));
3630
3631 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 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 shell.execute("NODE CREATE city {name: 'Paris'}");
3648 shell.execute("NODE CREATE city {name: 'London'}");
3649
3650 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 shell.execute("ENTITY CREATE user {name: 'Test'}");
3663 shell.execute("EMBED STORE 'user:1' [0.1, 0.2, 0.3]");
3664
3665 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 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 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 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 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 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 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 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 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 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 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 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 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 shell.execute("CREATE TABLE desc_test (id INT, name TEXT, active BOOL)");
3780
3781 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 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 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 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 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 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 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 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()); }
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 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 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 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 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 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 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 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 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 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 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 let home = dirs_home();
4153 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 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 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 }
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 {
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 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 shell.execute(&format!("SAVE '{path_str}'"));
4227 shell.execute(&format!("LOAD '{path_str}'"));
4228
4229 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 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 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 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 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 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 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 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 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 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 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 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 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 let result = shell.execute(&format!("SAVE '{path_str}'"));
4454 assert!(matches!(result, CommandResult::Output(_)));
4455
4456 let result = shell.execute(&format!("LOAD '{path_str}'"));
4458 assert!(matches!(result, CommandResult::Output(_)));
4459
4460 let _ = shell.execute("SELECT * FROM cycle_test");
4462 let _ = shell.execute("NODE LIST item");
4463 let _ = shell.execute("EMBED GET 'cycle:key'");
4464
4465 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 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 let result = shell.execute(&format!("SAVE COMPRESSED '{path_str}'"));
4483 assert!(
4484 matches!(result, CommandResult::Output(_)) || matches!(result, CommandResult::Error(_))
4485 );
4486
4487 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 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 shell.execute(&format!("SAVE '{path_str}'"));
4512
4513 shell.execute(&format!("LOAD '{path_str}'"));
4515
4516 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 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 shell.execute(&format!("SAVE '{path_str}'"));
4536 shell.execute(&format!("LOAD '{path_str}'"));
4537
4538 let result = shell.execute("WAL TRUNCATE");
4540 assert!(matches!(result, CommandResult::Output(_)));
4541
4542 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 {
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(); writeln!(file, " ").unwrap(); 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 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 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 assert!(matches!(result, CommandResult::Error(_)));
4623 }
4624
4625 #[test]
4626 fn test_is_write_command_comprehensive() {
4627 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 #[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 #[test]
4679 fn test_shell_version() {
4680 let version = Shell::version();
4681 assert!(!version.is_empty());
4682 assert!(version.contains('.'));
4684 }
4685
4686 #[test]
4689 fn test_router_arc_accessor() {
4690 let shell = Shell::new();
4691 let router_arc = shell.router_arc();
4692 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 }
4702
4703 #[test]
4704 fn test_router_mut_accessor() {
4705 let shell = Shell::new();
4706 let _router = shell.router_mut();
4707 }
4709
4710 #[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 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 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 #[test]
4759 fn test_help_text_contains_commands() {
4760 let help = Shell::help_text();
4761 assert!(help.contains("Commands"));
4762 }
4763
4764 #[test]
4767 fn test_execute_line_with_error() {
4768 let mut shell = Shell::new();
4769 let result = shell.execute_line("SELECT * FROM nonexistent_weird_table_xyz");
4771 let _ = result;
4773 }
4774
4775 #[test]
4778 fn test_list_tables_empty_database() {
4779 let shell = Shell::new();
4780 let result = shell.list_tables();
4781 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 let result = shell.handle_wal_truncate();
4792 assert!(matches!(result, CommandResult::Error(_)));
4793 }
4794}