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
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.add_task("Test task", None, None).await.unwrap();
103
104 let response = workspace_mgr.set_current_task(task.id).await.unwrap();
105
106 assert_eq!(response.current_task_id, Some(task.id));
107 assert!(response.task.is_some());
108 assert_eq!(response.task.unwrap().id, task.id);
109 }
110
111 #[tokio::test]
112 async fn test_set_current_task_nonexistent() {
113 let ctx = TestContext::new().await;
114 let workspace_mgr = WorkspaceManager::new(ctx.pool());
115
116 let result = workspace_mgr.set_current_task(999).await;
117 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
118 }
119
120 #[tokio::test]
121 async fn test_update_current_task() {
122 let ctx = TestContext::new().await;
123 let task_mgr = TaskManager::new(ctx.pool());
124 let workspace_mgr = WorkspaceManager::new(ctx.pool());
125
126 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
127 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
128
129 workspace_mgr.set_current_task(task1.id).await.unwrap();
131
132 let response = workspace_mgr.set_current_task(task2.id).await.unwrap();
134
135 assert_eq!(response.current_task_id, Some(task2.id));
136 assert_eq!(response.task.unwrap().id, task2.id);
137 }
138
139 #[tokio::test]
140 async fn test_get_current_task_after_set() {
141 let ctx = TestContext::new().await;
142 let task_mgr = TaskManager::new(ctx.pool());
143 let workspace_mgr = WorkspaceManager::new(ctx.pool());
144
145 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
146 workspace_mgr.set_current_task(task.id).await.unwrap();
147
148 let response = workspace_mgr.get_current_task().await.unwrap();
149
150 assert_eq!(response.current_task_id, Some(task.id));
151 assert!(response.task.is_some());
152 }
153
154 #[tokio::test]
155 async fn test_current_task_response_serialization() {
156 let ctx = TestContext::new().await;
157 let task_mgr = TaskManager::new(ctx.pool());
158 let workspace_mgr = WorkspaceManager::new(ctx.pool());
159
160 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
161 let response = workspace_mgr.set_current_task(task.id).await.unwrap();
162
163 let json = serde_json::to_string(&response).unwrap();
165 assert!(json.contains("current_task_id"));
166 assert!(json.contains("task"));
167 }
168
169 #[tokio::test]
170 async fn test_current_task_response_none_serialization() {
171 let ctx = TestContext::new().await;
172 let workspace_mgr = WorkspaceManager::new(ctx.pool());
173
174 let response = workspace_mgr.get_current_task().await.unwrap();
175
176 let json = serde_json::to_string(&response).unwrap();
178 assert!(json.contains("current_task_id"));
179 assert!(!json.contains("\"task\""));
181 }
182
183 #[tokio::test]
184 async fn test_get_current_task_with_invalid_id_in_db() {
185 let ctx = TestContext::new().await;
186
187 sqlx::query(
189 "INSERT INTO workspace_state (key, value) VALUES ('current_task_id', 'invalid')",
190 )
191 .execute(ctx.pool())
192 .await
193 .unwrap();
194
195 let workspace_mgr = WorkspaceManager::new(ctx.pool());
196 let response = workspace_mgr.get_current_task().await.unwrap();
197
198 assert!(response.current_task_id.is_none());
200 assert!(response.task.is_none());
201 }
202
203 #[tokio::test]
204 async fn test_get_current_task_with_deleted_task() {
205 let ctx = TestContext::new().await;
206 let task_mgr = TaskManager::new(ctx.pool());
207 let workspace_mgr = WorkspaceManager::new(ctx.pool());
208
209 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
210 workspace_mgr.set_current_task(task.id).await.unwrap();
211
212 task_mgr.delete_task(task.id).await.unwrap();
214
215 let response = workspace_mgr.get_current_task().await.unwrap();
216
217 assert_eq!(response.current_task_id, Some(task.id));
219 assert!(response.task.is_none());
220 }
221
222 #[tokio::test]
223 async fn test_set_current_task_returns_complete_task() {
224 let ctx = TestContext::new().await;
225 let task_mgr = TaskManager::new(ctx.pool());
226 let workspace_mgr = WorkspaceManager::new(ctx.pool());
227
228 let task = task_mgr
229 .add_task("Test task", Some("Task spec"), None)
230 .await
231 .unwrap();
232
233 let response = workspace_mgr.set_current_task(task.id).await.unwrap();
234
235 let returned_task = response.task.unwrap();
237 assert_eq!(returned_task.id, task.id);
238 assert_eq!(returned_task.name, "Test task");
239 assert_eq!(returned_task.spec, Some("Task spec".to_string()));
240 assert_eq!(returned_task.status, "todo");
241 }
242
243 #[tokio::test]
244 async fn test_set_same_task_multiple_times() {
245 let ctx = TestContext::new().await;
246 let task_mgr = TaskManager::new(ctx.pool());
247 let workspace_mgr = WorkspaceManager::new(ctx.pool());
248
249 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
250
251 workspace_mgr.set_current_task(task.id).await.unwrap();
253 workspace_mgr.set_current_task(task.id).await.unwrap();
254 let response = workspace_mgr.set_current_task(task.id).await.unwrap();
255
256 assert_eq!(response.current_task_id, Some(task.id));
257 }
258
259 #[tokio::test]
260 async fn test_workspace_state_insert_or_replace() {
261 let ctx = TestContext::new().await;
262 let task_mgr = TaskManager::new(ctx.pool());
263 let workspace_mgr = WorkspaceManager::new(ctx.pool());
264
265 let task1 = task_mgr.add_task("Task 1", None, None).await.unwrap();
266 let task2 = task_mgr.add_task("Task 2", None, None).await.unwrap();
267
268 workspace_mgr.set_current_task(task1.id).await.unwrap();
269 workspace_mgr.set_current_task(task2.id).await.unwrap();
270
271 let count: i64 = sqlx::query_scalar(
273 "SELECT COUNT(*) FROM workspace_state WHERE key = 'current_task_id'",
274 )
275 .fetch_one(ctx.pool())
276 .await
277 .unwrap();
278
279 assert_eq!(count, 1);
280 }
281
282 #[tokio::test]
283 async fn test_get_current_task_with_changed_status() {
284 let ctx = TestContext::new().await;
285 let task_mgr = TaskManager::new(ctx.pool());
286 let workspace_mgr = WorkspaceManager::new(ctx.pool());
287
288 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
289 workspace_mgr.set_current_task(task.id).await.unwrap();
290
291 task_mgr.start_task(task.id, false).await.unwrap();
293
294 let response = workspace_mgr.get_current_task().await.unwrap();
295
296 assert_eq!(response.task.unwrap().status, "doing");
298 }
299}