use std::process::Command; use std::path::PathBuf; use tempfile::TempDir; fn citrine_bin() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_citrine")) } fn run_citrine(dir: &std::path::Path, args: &[&str]) -> std::process::Output { Command::new(citrine_bin()) .current_dir(dir) .args(args) .output() .expect("Failed to execute citrine") } fn stdout(output: &std::process::Output) -> String { String::from_utf8_lossy(&output.stdout).to_string() } fn stderr(output: &std::process::Output) -> String { String::from_utf8_lossy(&output.stderr).to_string() } #[test] fn test_init() { let dir = TempDir::new().unwrap(); let out = run_citrine(dir.path(), &["init"]); assert!(out.status.success()); assert!(stdout(&out).contains("Initialized citrine")); assert!(dir.path().join(".citrine").exists()); assert!(dir.path().join(".gitignore").exists()); let gitignore = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap(); assert!(gitignore.contains(".citrine.local")); } #[test] fn test_init_idempotent() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["init"]); assert!(out.status.success()); } #[test] fn test_add_and_list() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["add", "My first task"]); assert!(out.status.success()); assert!(stdout(&out).contains("Added task")); assert!(stdout(&out).contains("My first task")); let out = run_citrine(dir.path(), &["list"]); assert!(out.status.success()); assert!(stdout(&out).contains("My first task")); } #[test] fn test_add_local() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["add", "Local task", "--local"]); assert!(out.status.success()); assert!(stdout(&out).contains("(local)")); assert!(dir.path().join(".citrine.local").exists()); // Should appear in list let out = run_citrine(dir.path(), &["list"]); assert!(stdout(&out).contains("Local task")); // Should appear in list --local let out = run_citrine(dir.path(), &["list", "--local"]); assert!(stdout(&out).contains("Local task")); } #[test] fn test_add_not_initialized() { let dir = TempDir::new().unwrap(); let out = run_citrine(dir.path(), &["add", "Some task"]); assert!(!out.status.success()); assert_eq!(out.status.code(), Some(3)); assert!(stderr(&out).contains("Not initialized")); } #[test] fn test_set_status() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["add", "Test task"]); let hash = extract_hash(&stdout(&out)); let out = run_citrine(dir.path(), &["set", &hash, "progress"]); assert!(out.status.success()); assert!(stdout(&out).contains("→")); let out = run_citrine(dir.path(), &["list"]); assert!(stdout(&out).contains("[>]")); } #[test] fn test_done_adds_completed() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["add", "Finish this"]); let hash = extract_hash(&stdout(&out)); let out = run_citrine(dir.path(), &["done", &hash]); assert!(out.status.success()); assert!(stdout(&out).contains("@completed:")); let content = std::fs::read_to_string(dir.path().join(".citrine")).unwrap(); assert!(content.contains("[x]")); assert!(content.contains("@completed:")); } #[test] fn test_rm() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["add", "Remove me"]); let hash = extract_hash(&stdout(&out)); let out = run_citrine(dir.path(), &["rm", &hash]); assert!(out.status.success()); assert!(stdout(&out).contains("Removed")); let out = run_citrine(dir.path(), &["list"]); assert!(!stdout(&out).contains("Remove me")); } #[test] fn test_partial_hash() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["add", "Partial hash test"]); let hash = extract_hash(&stdout(&out)); let partial = &hash[..2]; let out = run_citrine(dir.path(), &["set", partial, "progress"]); assert!(out.status.success()); } #[test] fn test_invalid_status() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["add", "Task"]); let hash = extract_hash(&stdout(&out)); let out = run_citrine(dir.path(), &["set", &hash, "invalid"]); assert!(!out.status.success()); assert_eq!(out.status.code(), Some(2)); } #[test] fn test_hash_not_found() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); let out = run_citrine(dir.path(), &["set", "ffff", "done"]); assert!(!out.status.success()); assert!(stderr(&out).contains("Task not found")); } #[test] fn test_status_filter() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); run_citrine(dir.path(), &["add", "Todo task"]); let out = run_citrine(dir.path(), &["add", "Done task"]); let hash = extract_hash(&stdout(&out)); run_citrine(dir.path(), &["done", &hash]); let out = run_citrine(dir.path(), &["list", "--status", "todo"]); let s = stdout(&out); assert!(s.contains("Todo task")); assert!(!s.contains("Done task")); let out = run_citrine(dir.path(), &["list", "--status", "done"]); let s = stdout(&out); assert!(!s.contains("Todo task")); assert!(s.contains("Done task")); } #[test] fn test_merge_main_and_local() { let dir = TempDir::new().unwrap(); run_citrine(dir.path(), &["init"]); run_citrine(dir.path(), &["add", "Main task"]); run_citrine(dir.path(), &["add", "Local task", "--local"]); let out = run_citrine(dir.path(), &["list"]); let s = stdout(&out); assert!(s.contains("Main task")); assert!(s.contains("Local task")); } fn extract_hash(output: &str) -> String { let re = regex::Regex::new(r"\[([0-9a-f]{4})\]").unwrap(); re.captures(output).unwrap()[1].to_string() }