hephaestus/crates/hephd/tests/client_mode.rs
Erich Blume a5fc578525
Some checks failed
Build / validate (pull_request) Failing after 18m44s
feat(views): filter views (§8.2) — saved agenda slices
Make the owner's saved filters first-class so the agenda isn't one flat
list. `list` now takes a ListFilter predicate-as-data (heph-core::filter):
attention include/exclude sets, project-id scope, exclude_projects, and an
actionable do-date gate. New Store::view(name) resolves a built-in ViewSpec
— looking project names up to ids and subtree-expanding them through parent
links — then lists.

Five built-ins seeded from the Todoist queries (design §6.2.1): tom, ondeck,
chores, work, tasks (Schedule dropped — time-of-day isn't modeled on
date-grained do-dates). Surfaced as `heph view <name>` (no name lists them),
the `view` RPC + RemoteStore forward, and `:Heph view <name>` in nvim.

The list RPC/RemoteStore/CLI/heph.nvim migrate to the filter wire; legacy
--scope/--attention/--no-blue map onto it (nvim view.lua updated).

Tests: filter unit predicate, a views integration suite (subtree
scope+exclude, actionable gate, unknown-view error, absent-project empties),
a socket list/view dispatch test, two nvim e2e specs. 154 Rust tests + 18
nvim e2e green; clippy/fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 06:39:07 -07:00

99 lines
3.6 KiB
Rust

//! Client mode over real HTTP (tech-spec §3.1, slice 9b). A server runs the hub
//! router (which includes `/rpc`) over a temp `LocalStore`; a `RemoteStore`
//! proxies the `Store` API to it and we assert the calls land on the server's
//! store. The server runs on its own runtime thread, so the test thread can use
//! the blocking client without nesting runtimes.
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use heph_core::{Attention, Error, FixedClock, LocalStore, NewNode, NewTask, Store};
use hephd::sync::{self, SharedStore};
use hephd::RemoteStore;
const NOW: i64 = 1_704_067_200_000; // 2024-01-01T00:00:00Z
/// Start the hub router over a temp `LocalStore` on an ephemeral port; return
/// its base URL. The server thread + temp dir live for the test's duration.
fn start_server() -> String {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let dir = tempfile::tempdir().unwrap();
let store =
LocalStore::open(dir.path().join("heph.db"), Box::new(FixedClock(NOW))).unwrap();
let shared: SharedStore = Arc::new(Mutex::new(store));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
tx.send(listener.local_addr().unwrap()).unwrap();
let _keep = dir; // keep the temp DB alive while we serve
axum::serve(listener, sync::router(shared, None))
.await
.unwrap();
});
});
let addr = rx.recv_timeout(Duration::from_secs(5)).unwrap();
format!("http://{addr}")
}
#[test]
fn remote_store_proxies_the_store_api() {
let base = start_server();
let mut remote = RemoteStore::new(&base);
// Create + read a node round-trips through HTTP.
let node = remote
.create_node(NewNode::doc("Roof", "shingles need work"))
.unwrap();
let got = remote.get_node(&node.id).unwrap().expect("node on server");
assert_eq!(got.title, "Roof");
assert_eq!(got.body.as_deref(), Some("shingles need work"));
// A missing node is Ok(None), not an error.
assert!(remote.get_node("does-not-exist").unwrap().is_none());
// A body update materializes server-side and is full-text searchable.
remote
.update_node(&node.id, None, Some("new cedar shingles".into()))
.unwrap();
let hits = remote.search("cedar").unwrap();
assert!(hits.iter().any(|h| h.id == node.id), "search missed update");
// Tasks proxy too, and land in the Organizational list.
let task = remote
.create_task(NewTask {
title: "Renew passport".into(),
attention: Some(Attention::Red),
..Default::default()
})
.unwrap();
let fetched = remote
.get_task(&task.node_id)
.unwrap()
.expect("task on server");
assert_eq!(fetched.node_id, task.node_id);
let listed = remote.list(&heph_core::ListFilter::default()).unwrap();
assert!(
listed.iter().any(|t| t.node_id == task.node_id),
"task missing from list"
);
// Read-only aggregates proxy and start empty.
assert!(remote.health().unwrap().active_count >= 1);
assert!(remote.conflicts_list().unwrap().is_empty());
}
#[test]
fn remote_store_preserves_not_found() {
let base = start_server();
let mut remote = RemoteStore::new(&base);
let err = remote
.update_node("missing", Some("x".into()), None)
.unwrap_err();
assert!(matches!(err, Error::NodeNotFound(_)), "got {err:?}");
}