//! CLI tests (tech-spec ยง9): run the real `heph` binary against a real `hephd` //! over a unix socket, and assert output + side effects (export files). use std::path::PathBuf; use std::process::Command; use std::thread; use std::time::Duration; use tokio::net::UnixListener; use heph_core::{FixedClock, LocalStore}; use hephd::Daemon; const NOW: i64 = 1_704_067_200_000; /// Spawn a daemon on its own thread+runtime; return (socket, tempdir). fn spawn_daemon() -> (PathBuf, tempfile::TempDir) { let dir = tempfile::tempdir().unwrap(); let db = dir.path().join("heph.db"); let socket = dir.path().join("d.sock"); let store = LocalStore::open(&db, Box::new(FixedClock(NOW))).unwrap(); let socket_for_thread = socket.clone(); thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); rt.block_on(async move { let listener = UnixListener::bind(&socket_for_thread).unwrap(); let _ = Daemon::new(store).serve(listener).await; }); }); for _ in 0..200 { if socket.exists() { break; } thread::sleep(Duration::from_millis(5)); } (socket, dir) } /// Run the `heph` binary against `socket`; return (stdout, success). fn heph(socket: &std::path::Path, args: &[&str]) -> (String, bool) { let out = Command::new(env!("CARGO_BIN_EXE_heph")) .arg("--socket") .arg(socket) .args(args) .output() .expect("run heph"); ( String::from_utf8_lossy(&out.stdout).into_owned(), out.status.success(), ) } #[test] fn task_then_next_shows_the_task() { let (socket, _dir) = spawn_daemon(); let (out, ok) = heph(&socket, &["task", "Buy milk", "--attention", "red"]); assert!(ok, "task create failed: {out}"); assert!(out.contains("Created task"), "{out}"); let (out, ok) = heph(&socket, &["next"]); assert!(ok); assert!(out.contains("[red]"), "{out}"); assert!(out.contains("Buy milk"), "{out}"); } #[test] fn next_on_empty_store_is_friendly() { let (socket, _dir) = spawn_daemon(); let (out, ok) = heph(&socket, &["next"]); assert!(ok); assert!(out.contains("Nothing actionable"), "{out}"); } #[test] fn export_writes_markdown_files() { let (socket, dir) = spawn_daemon(); heph(&socket, &["doc", "Roof log", "--body", "# Roof"]); let export_dir = dir.path().join("export"); let (out, ok) = heph(&socket, &["export", export_dir.to_str().unwrap()]); assert!(ok, "export failed: {out}"); assert!(out.contains("Exported 1 nodes"), "{out}"); // The doc landed as a .md file under doc/. let docs: Vec<_> = std::fs::read_dir(export_dir.join("doc")) .unwrap() .filter_map(|e| e.ok()) .collect(); assert_eq!(docs.len(), 1); let text = std::fs::read_to_string(docs[0].path()).unwrap(); assert!(text.contains("title: \"Roof log\""), "{text}"); }