track/use_cases/
get_task_info.rs1use crate::db::Database;
2use crate::models::{Link, Scrap, Task, TaskRepo, Todo, VcsMode, Worktree};
3use crate::services::agent_context::build_agent_extensions;
4use crate::services::{
5 LinkService, RepoService, ScrapService, TaskService, TodoService, WorktreeService,
6};
7use crate::utils::{Result, TrackError};
8
9#[derive(Debug, Clone)]
11pub struct TaskInfoSnapshot {
12 pub task: Task,
13 pub todos: Vec<Todo>,
14 pub links: Vec<Link>,
15 pub scraps: Vec<Scrap>,
16 pub worktrees: Vec<Worktree>,
17 pub repos: Vec<TaskRepo>,
18 pub vcs_mode: VcsMode,
19}
20
21pub struct GetTaskInfoUseCase<'a> {
23 db: &'a Database,
24}
25
26impl<'a> GetTaskInfoUseCase<'a> {
27 pub fn new(db: &'a Database) -> Self {
28 Self { db }
29 }
30
31 pub fn load(&self, task_id: i64) -> Result<TaskInfoSnapshot> {
32 let task_service = TaskService::new(self.db);
33 let task = task_service.get_task(task_id)?;
34
35 let todo_service = TodoService::new(self.db);
36 let todos = todo_service.list_todos(task_id)?;
37
38 let link_service = LinkService::new(self.db);
39 let links = link_service.list_links(task_id)?;
40
41 let scrap_service = ScrapService::new(self.db);
42 let scraps = scrap_service.list_scraps(task_id)?;
43
44 let worktree_service = WorktreeService::new(self.db);
45 let worktrees = worktree_service.list_worktrees(task_id)?;
46
47 let repo_service = RepoService::new(self.db);
48 let repos = repo_service.list_repos(task_id)?;
49 let vcs_mode = self.db.get_vcs_mode()?;
50
51 Ok(TaskInfoSnapshot {
52 task,
53 todos,
54 links,
55 scraps,
56 worktrees,
57 repos,
58 vcs_mode,
59 })
60 }
61
62 pub fn base_bookmark(snapshot: &TaskInfoSnapshot) -> String {
63 if let Some(base_wt) = snapshot.worktrees.iter().find(|wt| wt.is_base) {
64 return base_wt.branch.clone();
65 }
66
67 if let Some(ticket_id) = &snapshot.task.ticket_id {
68 format!("task/{ticket_id}")
69 } else {
70 format!("task/task-{}", snapshot.task.id)
71 }
72 }
73
74 pub fn todo_worktree_branch(&self, snapshot: &TaskInfoSnapshot, todo: &Todo) -> Option<String> {
75 if let Some(worktree) = snapshot
76 .worktrees
77 .iter()
78 .find(|wt| wt.todo_id == Some(todo.id))
79 {
80 return Some(worktree.branch.clone());
81 }
82
83 if !todo.worktree_requested {
84 return None;
85 }
86
87 WorktreeService::new(self.db)
88 .get_todo_branch_name(
89 snapshot.task.id,
90 snapshot.task.ticket_id.as_deref(),
91 todo.task_index,
92 )
93 .ok()
94 }
95
96 pub fn orphan_worktrees(snapshot: &TaskInfoSnapshot) -> Vec<&Worktree> {
97 snapshot
98 .worktrees
99 .iter()
100 .filter(|wt| wt.todo_id.is_none())
101 .collect()
102 }
103
104 pub fn to_cli_json(&self, snapshot: &TaskInfoSnapshot) -> Result<serde_json::Value> {
105 let worktree_service = WorktreeService::new(self.db);
106
107 let mut todos_json = Vec::with_capacity(snapshot.todos.len());
108 for todo in &snapshot.todos {
109 let mut todo_val = serde_json::to_value(todo)
110 .map_err(|e| TrackError::SerializationFailed(e.to_string()))?;
111 if let Some(obj) = todo_val.as_object_mut() {
112 let worktree_branch = self.todo_worktree_branch(snapshot, todo);
113 obj.insert(
114 "worktree_branch".to_string(),
115 serde_json::to_value(worktree_branch)
116 .map_err(|e| TrackError::SerializationFailed(e.to_string()))?,
117 );
118 }
119 todos_json.push(todo_val);
120 }
121
122 let mut worktrees_json = Vec::with_capacity(snapshot.worktrees.len());
123 for worktree in &snapshot.worktrees {
124 let mut wt_val = serde_json::to_value(worktree)
125 .map_err(|e| TrackError::SerializationFailed(e.to_string()))?;
126 if let Some(obj) = wt_val.as_object_mut() {
127 obj.remove("id");
128 obj.remove("is_base");
129 obj.remove("task_id");
130
131 let task_scoped_id = worktree.todo_id.and_then(|id| {
132 snapshot
133 .todos
134 .iter()
135 .find(|todo| todo.id == id)
136 .map(|todo| todo.task_index)
137 });
138 obj.insert(
139 "todo_id".to_string(),
140 serde_json::to_value(task_scoped_id)
141 .map_err(|e| TrackError::SerializationFailed(e.to_string()))?,
142 );
143 }
144 worktrees_json.push(wt_val);
145 }
146
147 for todo in &snapshot.todos {
148 if todo.worktree_requested
149 && !snapshot
150 .worktrees
151 .iter()
152 .any(|wt| wt.todo_id == Some(todo.id))
153 {
154 let branch_name = self.todo_worktree_branch(snapshot, todo);
155 worktrees_json.push(serde_json::json!({
156 "todo_id": todo.task_index,
157 "branch": branch_name,
158 "status": "requested",
159 "path": null,
160 "created_at": null,
161 "base_repo": null
162 }));
163 }
164 }
165
166 let mut output = serde_json::json!({
167 "task": snapshot.task,
168 "todos": todos_json,
169 "links": snapshot.links,
170 "scraps": snapshot.scraps,
171 "worktrees": worktrees_json,
172 "repos": snapshot.repos,
173 });
174
175 let agent = build_agent_extensions(
176 snapshot.vcs_mode,
177 &snapshot.task,
178 &snapshot.todos,
179 &snapshot.worktrees,
180 &snapshot.repos,
181 &worktree_service,
182 );
183 let agent_val = serde_json::to_value(&agent)
184 .map_err(|e| TrackError::SerializationFailed(e.to_string()))?;
185 if let Some(obj) = output.as_object_mut() {
186 if let Some(agent_obj) = agent_val.as_object() {
187 for (key, value) in agent_obj {
188 obj.insert(key.clone(), value.clone());
189 }
190 }
191 }
192
193 Ok(output)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::services::TaskService;
201
202 #[test]
203 fn base_bookmark_uses_ticket_when_no_base_worktree() {
204 let db = Database::new_in_memory().unwrap();
205 let task = TaskService::new(&db)
206 .create_task("Task", None, Some("ABC-1"), None)
207 .unwrap();
208 let snapshot = GetTaskInfoUseCase::new(&db).load(task.id).unwrap();
209
210 assert_eq!(GetTaskInfoUseCase::base_bookmark(&snapshot), "task/ABC-1");
211 }
212
213 #[test]
214 fn cli_json_includes_task_and_todos() {
215 let db = Database::new_in_memory().unwrap();
216 let task = TaskService::new(&db)
217 .create_task("Task", None, None, None)
218 .unwrap();
219 TodoService::new(&db)
220 .add_todo(task.id, "Do thing", false)
221 .unwrap();
222
223 let use_case = GetTaskInfoUseCase::new(&db);
224 let snapshot = use_case.load(task.id).unwrap();
225 let json = use_case.to_cli_json(&snapshot).unwrap();
226
227 assert_eq!(json["task"]["name"], "Task");
228 assert_eq!(json["todos"].as_array().map(|t| t.len()), Some(1));
229 }
230}