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