hephaestus/crates/heph-core/tests/next_ranking.rs
Erich Blume 7f63f926d0
Some checks failed
Build / validate (pull_request) Failing after 3s
heph-core: "what is next?" ranking (tech-spec §7)
Slice 4 — the flagship Tactical blank-slate engine. Pure and
clock-injected, two stages:

- Candidacy filter: committed ∧ outstanding ∧ ¬tombstoned ∧ ≠blue ∧
  actionable (do_date NULL or ≤ now) ∧ in scope. do_date is used ONLY
  here — a boolean "can I do this now?" gate, never urgency.
- Order: an ordered list of named Dimensions applied lexicographically
  (PastLateOn → LateOverdueAmount → Attention band → CreatedAt FIFO),
  with node_id as final tiebreak for a total order. Reorder RANKING in
  one place to retune. late_on is the sole urgency signal (global tier);
  age never becomes urgency. blue hidden; red always shown past limit.

Storage `Store::next` loads candidates via a SQL join (project +
canonical-context links) and runs the pure engine with the store clock.

13 table-driven unit cases + 3 proptests (antisymmetry, sorted output
fully ordered, equality ⇒ identity) + 2 end-to-end. 38 tests green.

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

95 lines
3 KiB
Rust

//! End-to-end test of `Store::next` through the SQLite loader (slice 4).
//! The pure ranking is unit-tested in `ranking.rs`; here we prove the join
//! (project + canonical-context) and candidacy reach the engine intact.
use heph_core::{Attention, FixedClock, LocalStore, NewTask, Store, TaskState};
const NOW: i64 = 1_700_000_000_000;
fn store() -> LocalStore {
LocalStore::open_in_memory(Box::new(FixedClock(NOW))).unwrap()
}
#[test]
fn next_ranks_red_first_and_hides_blue_and_future() {
let mut s = store();
let red = s
.create_task(NewTask {
title: "Red now".into(),
attention: Some(Attention::Red),
..Default::default()
})
.unwrap();
let _white = s
.create_task(NewTask {
title: "White now".into(),
attention: Some(Attention::White),
..Default::default()
})
.unwrap();
let _blue = s
.create_task(NewTask {
title: "Blue backlog".into(),
attention: Some(Attention::Blue),
..Default::default()
})
.unwrap();
let _future = s
.create_task(NewTask {
title: "Not yet".into(),
attention: Some(Attention::Orange),
do_date: Some(NOW + 1_000),
..Default::default()
})
.unwrap();
let ranked = s.next(None, 5).unwrap();
let titles: Vec<&str> = ranked.iter().map(|t| t.title.as_str()).collect();
// Blue hidden, future not actionable → only the two "now" tasks, red first.
assert_eq!(titles, vec!["Red now", "White now"]);
// The canonical context link is surfaced for the one-keystroke jump.
assert_eq!(ranked[0].node_id, red.node_id);
assert!(ranked[0].canonical_context_id.is_some());
}
#[test]
fn next_respects_scope_and_excludes_completed() {
let mut s = store();
let project = s
.create_node(heph_core::NewNode {
kind: heph_core::NodeKind::Project,
title: "Work".into(),
body: None,
})
.unwrap();
let in_scope = s
.create_task(NewTask {
title: "Work task".into(),
attention: Some(Attention::Orange),
project_id: Some(project.id.clone()),
..Default::default()
})
.unwrap();
let _other = s
.create_task(NewTask {
title: "Life task".into(),
attention: Some(Attention::Orange),
..Default::default()
})
.unwrap();
let done = s
.create_task(NewTask {
title: "Already done".into(),
attention: Some(Attention::Orange),
project_id: Some(project.id.clone()),
..Default::default()
})
.unwrap();
s.set_task_state(&done.node_id, TaskState::Done).unwrap();
let ranked = s.next(Some(&project.id), 5).unwrap();
let ids: Vec<&str> = ranked.iter().map(|t| t.node_id.as_str()).collect();
assert_eq!(ids, vec![in_scope.node_id.as_str()]);
}