Skip to main content

voro_core/
lib.rs

1//! Core logic for Voro: the SQLite store, the task state machine, and the
2//! scheduler/scoring. Pure of terminal I/O; every interface (TUI, CLI verbs)
3//! is a thin consumer of this crate. Concepts and invariants are specified in
4//! `docs/DESIGN.md`.
5
6mod agent;
7mod error;
8mod import;
9mod model;
10mod pr;
11pub mod scheduler;
12mod store;
13mod transition;
14
15pub use agent::{
16    AgentSessionEntry, AgentTemplate, AgentsConfig, PROMPT_FILE_PLACEHOLDER, Provenance,
17    ResolvedAgent, SESSION_PLACEHOLDER, TASK_ID_PLACEHOLDER, VIEWER_BASE_PLACEHOLDER,
18    VIEWER_BRANCH_PLACEHOLDER, VIEWER_PATH_PLACEHOLDER, ViewerTemplate, parse_sessions_json,
19};
20pub use error::{Error, Result};
21pub use import::{GithubIssue, already_imported, issue_new_task, issue_task_body};
22pub use model::{
23    Dep, DepKind, DepRef, Event, NextAction, Priority, Project, ReviewAction, ReviewMedium,
24    RunningRow, Session, SessionOutcome, Task, TaskState,
25};
26pub use pr::{PrPlan, PrRef, format_review_feedback, plan_pr};
27pub use scheduler::{Candidate, ScoreBreakdown, StateCounts};
28pub use store::{NewTask, Store, TaskEdit};
29pub use transition::{Action, Triage};
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    fn store() -> Store {
36        Store::open_in_memory().unwrap()
37    }
38
39    fn new_task(project_id: i64, title: &str, state: TaskState) -> NewTask {
40        NewTask {
41            project_id,
42            title: title.into(),
43            body: String::new(),
44            priority: Priority::P2,
45            state,
46            agent: None,
47            human: false,
48        }
49    }
50
51    #[test]
52    fn project_crud_and_weight_bounds() {
53        let mut s = store();
54        let p = s.create_project("voro", "/tmp/voro").unwrap();
55        assert_eq!(p.weight, 3);
56        s.set_weight(p.id, 5).unwrap();
57        assert_eq!(s.project(p.id).unwrap().weight, 5);
58        assert!(s.set_weight(p.id, 6).is_err());
59        assert!(s.set_weight(999, 2).is_err());
60    }
61
62    #[test]
63    fn task_create_defaults_and_event() {
64        let mut s = store();
65        let p = s.create_project("voro", "/tmp/voro").unwrap();
66        let t = s
67            .create_task(new_task(p.id, "First", TaskState::Ready))
68            .unwrap();
69        assert_eq!(t.state, TaskState::Ready);
70        assert!(t.question.is_none());
71        assert!(t.closed_at.is_none());
72        assert!(!t.state_since.is_empty());
73        let events = s.events_for(t.id).unwrap();
74        assert_eq!(events.len(), 1);
75        assert_eq!(events[0].kind, "created");
76        assert_eq!(events[0].detail.as_deref(), Some("ready"));
77    }
78
79    #[test]
80    fn task_cannot_be_created_in_active_or_closed_states() {
81        let mut s = store();
82        let p = s.create_project("voro", "/tmp/voro").unwrap();
83        for state in [
84            TaskState::Running,
85            TaskState::NeedsInput,
86            TaskState::Review,
87            TaskState::Done,
88            TaskState::Rejected,
89        ] {
90            assert!(s.create_task(new_task(p.id, "bad", state)).is_err());
91        }
92    }
93
94    #[test]
95    fn dep_rejects_self_reference() {
96        let mut s = store();
97        let p = s.create_project("voro", "/tmp/voro").unwrap();
98        let t = s
99            .create_task(new_task(p.id, "t", TaskState::Parked))
100            .unwrap();
101        assert!(s.add_dep(t.id, t.id, DepKind::Blocks).is_err());
102    }
103
104    #[test]
105    fn dep_add_and_remove() {
106        let mut s = store();
107        let p = s.create_project("voro", "/tmp/voro").unwrap();
108        let a = s
109            .create_task(new_task(p.id, "a", TaskState::Ready))
110            .unwrap();
111        let b = s
112            .create_task(new_task(p.id, "b", TaskState::Parked))
113            .unwrap();
114        s.add_dep(b.id, a.id, DepKind::Blocks).unwrap();
115        let deps = s.deps_of(b.id).unwrap();
116        assert_eq!(deps.len(), 1);
117        assert_eq!(deps[0].depends_on, a.id);
118        assert_eq!(deps[0].kind, DepKind::Blocks);
119        s.remove_dep(b.id, a.id).unwrap();
120        assert!(s.deps_of(b.id).unwrap().is_empty());
121    }
122}