//! Exercise the executable boundary: exit status, durable indexing, and JSON. use std::{ fs, path::PathBuf, process::{Command, Output}, sync::atomic::{AtomicU64, Ordering}, }; static NEXT_ID: AtomicU64 = AtomicU64::new(0); struct Fixture(PathBuf); impl Fixture { fn new() -> Self { let path = std::env::temp_dir().join(format!( "searchi-cli-test-{}-{}", std::process::id(), NEXT_ID.fetch_add(1, Ordering::Relaxed) )); fs::create_dir(&path).unwrap(); Self(path) } fn run(&self, args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_searchi")) .current_dir(&self.0) .args(args) .output() .unwrap() } } impl Drop for Fixture { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } #[test] fn indexes_persists_and_returns_search_and_stats_json() { let fixture = Fixture::new(); fs::create_dir(fixture.0.join("documents")).unwrap(); fs::write( fixture.0.join("documents/contract.txt"), "Supplier termination clause requires sixty days notice.", ) .unwrap(); let indexed = fixture.run(&["index", "--db", "index.db", "--root", "documents"]); assert!( indexed.status.success(), "{}", String::from_utf8_lossy(&indexed.stderr) ); // Each command is a new process, so retrieval proves the index survived exit. let searched = fixture.run(&["search", "--db", "index.db", "termination", "--json"]); assert!( searched.status.success(), "{}", String::from_utf8_lossy(&searched.stderr) ); let results: serde_json::Value = serde_json::from_slice(&searched.stdout).unwrap(); let results = results.as_array().unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0]["filename"], "contract.txt"); assert!(results[0]["snippet"] .as_str() .unwrap() .contains("termination")); let stats = fixture.run(&["stats", "--db", "index.db", "--json"]); assert!(stats.status.success()); let stats: serde_json::Value = serde_json::from_slice(&stats.stdout).unwrap(); assert_eq!(stats["files"], 1); assert_eq!(stats["content_files"], 1); } #[test] fn errors_are_nonzero_and_missing_database_is_not_created() { let fixture = Fixture::new(); let missing = fixture.run(&["search", "--db", "missing.db", "invoice", "--json"]); assert!(!missing.status.success()); assert!(String::from_utf8_lossy(&missing.stderr).contains("does not exist")); assert!(!fixture.0.join("missing.db").exists()); assert!(missing.stdout.is_empty()); let bad_root = fixture.run(&["index", "--db", "index.db", "--root", "missing-folder"]); assert!(!bad_root.status.success()); assert!(String::from_utf8_lossy(&bad_root.stderr).contains("missing-folder")); } #[test] fn exclusions_and_filters_change_real_results() { let fixture = Fixture::new(); fs::create_dir_all(fixture.0.join("documents/private")).unwrap(); fs::write(fixture.0.join("documents/report.txt"), "quarterly invoice").unwrap(); fs::write(fixture.0.join("documents/report.md"), "quarterly invoice").unwrap(); fs::write(fixture.0.join("documents/private/secret.txt"), "invoice").unwrap(); let indexed = fixture.run(&[ "index", "--db", "index.db", "--root", "documents", "--exclude", "private", ]); assert!(indexed.status.success()); let searched = fixture.run(&[ "search", "--db", "index.db", "invoice", "--extension", "md", "--json", ]); assert!( searched.status.success(), "{}", String::from_utf8_lossy(&searched.stderr) ); let results: serde_json::Value = serde_json::from_slice(&searched.stdout).unwrap(); assert_eq!(results.as_array().unwrap().len(), 1); assert_eq!(results[0]["filename"], "report.md"); let stats = fixture.run(&["stats", "--db", "index.db", "--json"]); let stats: serde_json::Value = serde_json::from_slice(&stats.stdout).unwrap(); assert_eq!(stats["files"], 2); }