hephaestus/crates/heph/tests/cli.rs
Erich Blume 739214bd07
Some checks failed
Build / validate (pull_request) Failing after 2s
heph CLI + export
Slice 7 (tech-spec §1, §5, §9).

- Export (heph-core): render each non-tombstoned node to `<kind>/<id>.md`
  with YAML frontmatter (id, kind, title, timestamps, task scalars,
  aliases, outgoing links) + body. One-way snapshot; `Store::export`
  writes the tree; tombstones excluded. Added `export` RPC method and
  Error::Io.
- `heph` CLI (clap): thin client of hephd over the socket — `next`
  (concise ranked rows), `task`, `doc`, `get`, `export`. Never touches
  SQLite directly.

Tests: 3 export render unit + 2 export round-trip integration + 3 CLI
process tests driving the real `heph` binary against a real daemon
(task→next, empty-store message, export writes files). 70 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:33:59 -07:00

97 lines
2.9 KiB
Rust

//! 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}");
}