//! 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")); for flag in ["--performance", "--debug-performance"] { let measured = fixture.run(&["search", "--db", "index.db", "termination", "--json", flag]); assert!( measured.status.success(), "{}", String::from_utf8_lossy(&measured.stderr) ); let envelope: serde_json::Value = serde_json::from_slice(&measured.stdout).unwrap(); assert_eq!( envelope["results"], serde_json::Value::Array(results.clone()) ); assert_eq!(envelope["metrics"]["result_count"], 1); let total = envelope["metrics"]["total_ms"].as_f64().unwrap(); assert!(total.is_finite() && total >= 0.0); if flag == "--performance" { assert!(envelope["metrics"]["trace"].is_null()); } else { let spans = envelope["metrics"]["trace"].as_array().unwrap(); assert!(spans.iter().any(|span| span["name"] == "cli.index_open")); assert!(spans.len() > 2); for span in spans { let start = span["start_ms"].as_f64().unwrap(); let duration = span["duration_ms"].as_f64().unwrap(); assert!(start >= 0.0 && duration >= 0.0 && start + duration <= total + 0.001); assert!(!span["name"].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); } #[test] fn code_only_filters_real_results_and_finds_exact_symbols() { let fixture = Fixture::new(); fs::create_dir(fixture.0.join("code")).unwrap(); fs::write( fixture.0.join("code/retry.rs"), "pub fn refreshAccessToken() { /* retry failed requests with backoff */ }", ) .unwrap(); fs::write( fixture.0.join("code/notes.txt"), "refreshAccessToken retry failed requests with backoff", ) .unwrap(); let indexed = fixture.run(&["index", "--db", "index.db", "--root", "code"]); assert!( indexed.status.success(), "{}", String::from_utf8_lossy(&indexed.stderr) ); let all = fixture.run(&["search", "--db", "index.db", "refreshAccessToken", "--json"]); assert_eq!( serde_json::from_slice::(&all.stdout) .unwrap() .as_array() .unwrap() .len(), 2 ); let code = fixture.run(&[ "search", "--db", "index.db", "refreshAccessToken", "--code-only", "--json", ]); assert!( code.status.success(), "{}", String::from_utf8_lossy(&code.stderr) ); let results: serde_json::Value = serde_json::from_slice(&code.stdout).unwrap(); assert_eq!(results.as_array().unwrap().len(), 1); assert_eq!(results[0]["filename"], "retry.rs"); assert!(results[0]["snippet"] .as_str() .unwrap() .contains("refreshAccessToken")); assert_eq!(results[0]["source"]["kind"], "line"); }