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 cap;
8pub mod config_edit;
9mod error;
10mod import;
11mod model;
12mod pr;
13mod review;
14pub mod scheduler;
15pub mod seed;
16mod store;
17mod template;
18mod transition;
19
20pub use agent::{
21    AgentSessionEntry, AgentTemplate, AgentsConfig, BUILTIN_VIEWER_NAMES, Launch, LaunchSpec,
22    NEW_SESSION_PLACEHOLDER, PROMPT_FILE_PLACEHOLDER, Provenance, RenderedMessage, ResolvedAgent,
23    SESSION_NAME_PLACEHOLDER, SESSION_PLACEHOLDER, SessionLiveness, TASK_ID_PLACEHOLDER,
24    VIEWER_BASE_PLACEHOLDER, VIEWER_BRANCH_PLACEHOLDER, VIEWER_PATH_PLACEHOLDER, ViewerTemplate,
25    is_builtin_viewer, model_for_depth, parse_sessions_json, render_cap, render_message,
26    render_session,
27};
28pub use cap::{
29    AccountCap, CAP_SIGNATURES, CapReading, CapWindow, parse_reset_epoch, read_cap, strip_ansi,
30};
31pub use error::{Error, Result};
32pub use import::{GithubIssue, already_imported, issue_new_task, issue_task_body};
33pub use model::{
34    Dep, DepKind, DepRef, Doc, Event, LivenessSource, NextAction, Priority, Project, RefineOutcome,
35    Repo, RunningRow, Session, SessionOutcome, Task, TaskState, location_is_url,
36    projects_for_new_task,
37};
38pub use pr::{Mergeability, PrPlan, PrRef, format_review_feedback, parse_mergeable, plan_pr};
39pub use review::{
40    CompletionReport, PrRevisions, REVIEWED_EVENT, ReviewDiff, completion_report,
41    parse_pr_revisions, plan_review_diff, was_rejected,
42};
43pub use scheduler::{
44    ActionRow, AttentionCosts, Candidate, DigestRow, EffectiveScore, Queue, QueueRow,
45    ScoreBreakdown, StateCounts, WipGate,
46};
47pub use store::{NewTask, Store, TaskEdit};
48pub use template::{render, shell_quote};
49pub use transition::{Action, Triage};
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    fn store() -> Store {
56        Store::open_in_memory().unwrap()
57    }
58
59    fn new_task(project_id: i64, title: &str, state: TaskState) -> NewTask {
60        NewTask {
61            project_id,
62            repo_id: None,
63            title: title.into(),
64            body: String::new(),
65            priority: Priority::P2,
66            state,
67            agent: None,
68            human: false,
69            deep: false,
70        }
71    }
72
73    #[test]
74    fn project_crud_and_weight_bounds() {
75        let mut s = store();
76        let p = s.create_project("voro", "/tmp/voro").unwrap();
77        assert_eq!(p.weight, 3);
78        s.set_weight(p.id, 5).unwrap();
79        assert_eq!(s.project(p.id).unwrap().weight, 5);
80        assert!(s.set_weight(p.id, 6).is_err());
81        assert!(s.set_weight(999, 2).is_err());
82    }
83
84    #[test]
85    fn task_create_defaults_and_event() {
86        let mut s = store();
87        let p = s.create_project("voro", "/tmp/voro").unwrap();
88        let t = s
89            .create_task(new_task(p.id, "First", TaskState::Ready))
90            .unwrap();
91        assert_eq!(t.state, TaskState::Ready);
92        assert!(t.question.is_none());
93        assert!(t.closed_at.is_none());
94        assert!(!t.state_since.is_empty());
95        let events = s.events_for(t.id).unwrap();
96        assert_eq!(events.len(), 1);
97        assert_eq!(events[0].kind, "created");
98        assert_eq!(events[0].detail.as_deref(), Some("ready"));
99    }
100
101    #[test]
102    fn task_cannot_be_created_in_active_or_closed_states() {
103        let mut s = store();
104        let p = s.create_project("voro", "/tmp/voro").unwrap();
105        for state in [
106            TaskState::Running,
107            TaskState::NeedsInput,
108            TaskState::Review,
109            TaskState::Done,
110            TaskState::Rejected,
111        ] {
112            assert!(s.create_task(new_task(p.id, "bad", state)).is_err());
113        }
114    }
115
116    #[test]
117    fn dep_rejects_self_reference() {
118        let mut s = store();
119        let p = s.create_project("voro", "/tmp/voro").unwrap();
120        let t = s
121            .create_task(new_task(p.id, "t", TaskState::Parked))
122            .unwrap();
123        assert!(s.add_dep(t.id, t.id, DepKind::Blocks).is_err());
124    }
125
126    #[test]
127    fn dep_add_and_remove() {
128        let mut s = store();
129        let p = s.create_project("voro", "/tmp/voro").unwrap();
130        let a = s
131            .create_task(new_task(p.id, "a", TaskState::Ready))
132            .unwrap();
133        let b = s
134            .create_task(new_task(p.id, "b", TaskState::Parked))
135            .unwrap();
136        s.add_dep(b.id, a.id, DepKind::Blocks).unwrap();
137        let deps = s.deps_of(b.id).unwrap();
138        assert_eq!(deps.len(), 1);
139        assert_eq!(deps[0].depends_on, a.id);
140        assert_eq!(deps[0].kind, DepKind::Blocks);
141        s.remove_dep(b.id, a.id, DepKind::Blocks).unwrap();
142        assert!(s.deps_of(b.id).unwrap().is_empty());
143    }
144
145    /// A pair carrying edges of two kinds keeps both, and removing one leaves
146    /// the other standing.
147    #[test]
148    fn dep_remove_is_scoped_to_one_kind() {
149        let mut s = store();
150        let p = s.create_project("voro", "/tmp/voro").unwrap();
151        let a = s
152            .create_task(new_task(p.id, "a", TaskState::Ready))
153            .unwrap();
154        let b = s
155            .create_task(new_task(p.id, "b", TaskState::Parked))
156            .unwrap();
157        s.add_dep(b.id, a.id, DepKind::DiscoveredFrom).unwrap();
158        s.add_dep(b.id, a.id, DepKind::Blocks).unwrap();
159        assert_eq!(s.deps_of(b.id).unwrap().len(), 2);
160
161        s.remove_dep(b.id, a.id, DepKind::Blocks).unwrap();
162        let left = s.deps_of(b.id).unwrap();
163        assert_eq!(left.len(), 1);
164        assert_eq!(left[0].kind, DepKind::DiscoveredFrom);
165
166        let err = s.remove_dep(b.id, a.id, DepKind::Blocks).unwrap_err();
167        assert!(err.to_string().contains("no blocks dependency"), "{err}");
168    }
169
170    /// The same edge twice is a write that changes nothing, and says so.
171    #[test]
172    fn dep_add_rejects_a_duplicate_edge() {
173        let mut s = store();
174        let p = s.create_project("voro", "/tmp/voro").unwrap();
175        let a = s
176            .create_task(new_task(p.id, "a", TaskState::Ready))
177            .unwrap();
178        let b = s
179            .create_task(new_task(p.id, "b", TaskState::Parked))
180            .unwrap();
181        s.add_dep(b.id, a.id, DepKind::Blocks).unwrap();
182        let err = s.add_dep(b.id, a.id, DepKind::Blocks).unwrap_err();
183        assert!(err.to_string().contains("already has a blocks"), "{err}");
184    }
185}