generated from eblume/project-template
Some checks failed
Build / validate (pull_request) Failing after 2s
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>
97 lines
2.9 KiB
Rust
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}");
|
|
}
|