1mod 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 #[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 #[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}