generated from eblume/project-template
Some checks failed
Build / validate (pull_request) Failing after 18m44s
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>
99 lines
3.6 KiB
Rust
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:?}");
|
|
}
|