intent_engine/
workspace.rs1use crate::db::models::Task;
2use crate::error::{IntentError, Result};
3use serde::Serialize;
4use sqlx::SqlitePool;
5
6#[derive(Debug, Serialize)]
7pub struct CurrentTaskResponse {
8 pub current_task_id: Option<i64>,
9 #[serde(skip_serializing_if = "Option::is_none")]
10 pub task: Option<Task>,
11}
12
13pub struct WorkspaceManager<'a> {
14 pool: &'a SqlitePool,
15}
16
17impl<'a> WorkspaceManager<'a> {
18 pub fn new(pool: &'a SqlitePool) -> Self {
19 Self { pool }
20 }
21
22 pub async fn get_current_task(&self) -> Result<CurrentTaskResponse> {
24 let current_task_id: Option<String> =
25 sqlx::query_scalar("SELECT value FROM workspace_state WHERE key = 'current_task_id'")
26 .fetch_optional(self.pool)
27 .await?;
28
29 let current_task_id = current_task_id.and_then(|id| id.parse::<i64>().ok());
30
31 let task = if let Some(id) = current_task_id {
32 sqlx::query_as::<_, Task>(
33 r#"
34 SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
35 FROM tasks
36 WHERE id = ?
37 "#,
38 )
39 .bind(id)
40 .fetch_optional(self.pool)
41 .await?
42 } else {
43 None
44 };
45
46 Ok(CurrentTaskResponse {
47 current_task_id,
48 task,
49 })
50 }
51
52 pub async fn set_current_task(&self, task_id: i64) -> Result<CurrentTaskResponse> {
54 let task_exists: bool = sqlx::query_scalar(crate::sql_constants::CHECK_TASK_EXISTS)
56 .bind(task_id)
57 .fetch_one(self.pool)
58 .await?;
59
60 if !task_exists {
61 return Err(IntentError::TaskNotFound(task_id));
62 }
63
64 sqlx::query(
66 r#"
67 INSERT OR REPLACE INTO workspace_state (key, value)
68 VALUES ('current_task_id', ?)
69 "#,
70 )
71 .bind(task_id.to_string())
72 .execute(self.pool)
73 .await?;
74
75 self.get_current_task().await
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::tasks::TaskManager;
83 use crate::test_utils::test_helpers::TestContext;
84
85 #[tokio::test]
86 async fn test_get_current_task_none() {
87 let ctx = TestContext::new().await;
88 let workspace_mgr = WorkspaceManager::new(ctx.pool());
89
90 let response = workspace_mgr.get_current_task().await.unwrap();
91
92 assert!(response.current_task_id.is_none());
93 assert!(response.task.is_none());
94 }
95
96 #[tokio::test]
97 async fn test_set_current_task() {
98 let ctx = TestContext::new().await;
99 let task_mgr = TaskManager::new(ctx.pool());
100 let workspace_mgr = WorkspaceManager::new(ctx.pool());
101
102 let task = task_mgr
103 .add_task("Test task", None, None, None)
104 .await
105 .unwrap();
106
107 let response = workspace_mgr.set_current_task(task.id).await.unwrap();
108
109 assert_eq!(response.current_task_id, Some(task.id));
110 assert!(response.task.is_some());
111 assert_eq!(response.task.unwrap().id, task.id);
112 }
113
114 #[tokio::test]
115 async fn test_set_current_task_nonexistent() {
116 let ctx = TestContext::new().await;
117 let workspace_mgr = WorkspaceManager::new(ctx.pool());
118
119 let result = workspace_mgr.set_current_task(999).await;
120 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
121 }
122
123 #[tokio::test]
124 async fn test_update_current_task() {
125 let ctx = TestContext::new().await;
126 let task_mgr = TaskManager::new(ctx.pool());
127 let workspace_mgr = WorkspaceManager::new(ctx.pool());
128
129 let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
130 let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
131
132 workspace_mgr.set_current_task(task1.id).await.unwrap();
134
135 let response = workspace_mgr.set_current_task(task2.id).await.unwrap();
137
138 assert_eq!(response.current_task_id, Some(task2.id));
139 assert_eq!(response.task.unwrap().id, task2.id);
140 }
141
142 #[tokio::test]
143 async fn test_get_current_task_after_set() {
144 let ctx = TestContext::new().await;
145 let task_mgr = TaskManager::new(ctx.pool());
146 let workspace_mgr = WorkspaceManager::new(ctx.pool());
147
148 let task = task_mgr
149 .add_task("Test task", None, None, None)
150 .await
151 .unwrap();
152 workspace_mgr.set_current_task(task.id).await.unwrap();
153
154 let response = workspace_mgr.get_current_task().await.unwrap();
155
156 assert_eq!(response.current_task_id, Some(task.id));
157 assert!(response.task.is_some());
158 }
159
160 #[tokio::test]
161 async fn test_current_task_response_serialization() {
162 let ctx = TestContext::new().await;
163 let task_mgr = TaskManager::new(ctx.pool());
164 let workspace_mgr = WorkspaceManager::new(ctx.pool());
165
166 let task = task_mgr
167 .add_task("Test task", None, None, None)
168 .await
169 .unwrap();
170 let response = workspace_mgr.set_current_task(task.id).await.unwrap();
171
172 let json = serde_json::to_string(&response).unwrap();
174 assert!(json.contains("current_task_id"));
175 assert!(json.contains("task"));
176 }
177
178 #[tokio::test]
179 async fn test_current_task_response_none_serialization() {
180 let ctx = TestContext::new().await;
181 let workspace_mgr = WorkspaceManager::new(ctx.pool());
182
183 let response = workspace_mgr.get_current_task().await.unwrap();
184
185 let json = serde_json::to_string(&response).unwrap();
187 assert!(json.contains("current_task_id"));
188 assert!(!json.contains("\"task\""));
190 }
191
192 #[tokio::test]
193 async fn test_get_current_task_with_invalid_id_in_db() {
194 let ctx = TestContext::new().await;
195
196 sqlx::query(
198 "INSERT INTO workspace_state (key, value) VALUES ('current_task_id', 'invalid')",
199 )
200 .execute(ctx.pool())
201 .await
202 .unwrap();
203
204 let workspace_mgr = WorkspaceManager::new(ctx.pool());
205 let response = workspace_mgr.get_current_task().await.unwrap();
206
207 assert!(response.current_task_id.is_none());
209 assert!(response.task.is_none());
210 }
211
212 #[tokio::test]
213 async fn test_get_current_task_with_deleted_task() {
214 let ctx = TestContext::new().await;
215 let task_mgr = TaskManager::new(ctx.pool());
216 let workspace_mgr = WorkspaceManager::new(ctx.pool());
217
218 let task = task_mgr
219 .add_task("Test task", None, None, None)
220 .await
221 .unwrap();
222 workspace_mgr.set_current_task(task.id).await.unwrap();
223
224 task_mgr.delete_task(task.id).await.unwrap();
226
227 let response = workspace_mgr.get_current_task().await.unwrap();
228
229 assert_eq!(response.current_task_id, Some(task.id));
231 assert!(response.task.is_none());
232 }
233
234 #[tokio::test]
235 async fn test_set_current_task_returns_complete_task() {
236 let ctx = TestContext::new().await;
237 let task_mgr = TaskManager::new(ctx.pool());
238 let workspace_mgr = WorkspaceManager::new(ctx.pool());
239
240 let task = task_mgr
241 .add_task("Test task", Some("Task spec"), None, None)
242 .await
243 .unwrap();
244
245 let response = workspace_mgr.set_current_task(task.id).await.unwrap();
246
247 let returned_task = response.task.unwrap();
249 assert_eq!(returned_task.id, task.id);
250 assert_eq!(returned_task.name, "Test task");
251 assert_eq!(returned_task.spec, Some("Task spec".to_string()));
252 assert_eq!(returned_task.status, "todo");
253 }
254
255 #[tokio::test]
256 async fn test_set_same_task_multiple_times() {
257 let ctx = TestContext::new().await;
258 let task_mgr = TaskManager::new(ctx.pool());
259 let workspace_mgr = WorkspaceManager::new(ctx.pool());
260
261 let task = task_mgr
262 .add_task("Test task", None, None, None)
263 .await
264 .unwrap();
265
266 workspace_mgr.set_current_task(task.id).await.unwrap();
268 workspace_mgr.set_current_task(task.id).await.unwrap();
269 let response = workspace_mgr.set_current_task(task.id).await.unwrap();
270
271 assert_eq!(response.current_task_id, Some(task.id));
272 }
273
274 #[tokio::test]
275 async fn test_workspace_state_insert_or_replace() {
276 let ctx = TestContext::new().await;
277 let task_mgr = TaskManager::new(ctx.pool());
278 let workspace_mgr = WorkspaceManager::new(ctx.pool());
279
280 let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
281 let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
282
283 workspace_mgr.set_current_task(task1.id).await.unwrap();
284 workspace_mgr.set_current_task(task2.id).await.unwrap();
285
286 let count: i64 = sqlx::query_scalar(
288 "SELECT COUNT(*) FROM workspace_state WHERE key = 'current_task_id'",
289 )
290 .fetch_one(ctx.pool())
291 .await
292 .unwrap();
293
294 assert_eq!(count, 1);
295 }
296
297 #[tokio::test]
298 async fn test_get_current_task_with_changed_status() {
299 let ctx = TestContext::new().await;
300 let task_mgr = TaskManager::new(ctx.pool());
301 let workspace_mgr = WorkspaceManager::new(ctx.pool());
302
303 let task = task_mgr
304 .add_task("Test task", None, None, None)
305 .await
306 .unwrap();
307 workspace_mgr.set_current_task(task.id).await.unwrap();
308
309 task_mgr.start_task(task.id, false).await.unwrap();
311
312 let response = workspace_mgr.get_current_task().await.unwrap();
313
314 assert_eq!(response.task.unwrap().status, "doing");
316 }
317}