1use crate::assets::layered::LayeredAssetManager;
2use crate::context::{AppContext, file_system::FileSystemOperations};
3use crate::docker::DockerManager;
4use crate::docker::composer::DockerComposer;
5use crate::docker::image_manager::DockerImageManager;
6use crate::docker::template_manager::DockerTemplateManager;
7use crate::git::RepoManager;
8use crate::repo_utils::find_repository_root;
9use crate::storage::XdgDirectories;
10use crate::task::{Task, TaskBuilder, TaskStatus};
11use crate::task_runner::{TaskExecutionError, TaskExecutionResult, TaskRunner};
12use crate::task_storage::{TaskStorage, get_task_storage};
13use std::sync::Arc;
14
15pub struct TaskManager {
16 task_runner: TaskRunner,
17 task_storage: Option<Box<dyn TaskStorage>>,
18 file_system: Arc<dyn FileSystemOperations>,
19 xdg_directories: Arc<XdgDirectories>,
20}
21
22impl TaskManager {
23 pub fn new(ctx: &AppContext) -> Result<Self, String> {
24 let repo_manager = RepoManager::new(
25 ctx.xdg_directories(),
26 ctx.file_system(),
27 ctx.git_operations(),
28 );
29 let docker_manager = DockerManager::new(ctx.docker_client(), ctx.file_system());
30
31 let project_root = find_repository_root(std::path::Path::new(".")).ok();
34 let asset_manager = Arc::new(LayeredAssetManager::new_with_standard_layers(
35 project_root.as_deref(),
36 &ctx.xdg_directories(),
37 ));
38 let template_manager =
39 DockerTemplateManager::new(asset_manager.clone(), ctx.xdg_directories());
40 let composer = DockerComposer::new(DockerTemplateManager::new(
41 asset_manager,
42 ctx.xdg_directories(),
43 ));
44 let image_manager = Arc::new(DockerImageManager::new(
45 ctx.docker_client(),
46 template_manager,
47 composer,
48 ));
49
50 let task_runner = TaskRunner::new(
51 repo_manager,
52 docker_manager,
53 image_manager,
54 ctx.file_system(),
55 ctx.notification_client(),
56 );
57
58 Ok(Self {
59 task_runner,
60 task_storage: None,
61 file_system: ctx.file_system(),
62 xdg_directories: ctx.xdg_directories(),
63 })
64 }
65
66 pub fn with_storage(ctx: &AppContext) -> Result<Self, String> {
67 let repo_manager = RepoManager::new(
68 ctx.xdg_directories(),
69 ctx.file_system(),
70 ctx.git_operations(),
71 );
72 let docker_manager = DockerManager::new(ctx.docker_client(), ctx.file_system());
73
74 let project_root = find_repository_root(std::path::Path::new(".")).ok();
77 let asset_manager = Arc::new(LayeredAssetManager::new_with_standard_layers(
78 project_root.as_deref(),
79 &ctx.xdg_directories(),
80 ));
81 let template_manager =
82 DockerTemplateManager::new(asset_manager.clone(), ctx.xdg_directories());
83 let composer = DockerComposer::new(DockerTemplateManager::new(
84 asset_manager,
85 ctx.xdg_directories(),
86 ));
87 let image_manager = Arc::new(DockerImageManager::new(
88 ctx.docker_client(),
89 template_manager,
90 composer,
91 ));
92
93 let task_runner = TaskRunner::new(
94 repo_manager,
95 docker_manager,
96 image_manager,
97 ctx.file_system(),
98 ctx.notification_client(),
99 );
100
101 Ok(Self {
102 task_runner,
103 task_storage: Some(get_task_storage(ctx.xdg_directories(), ctx.file_system())),
104 file_system: ctx.file_system(),
105 xdg_directories: ctx.xdg_directories(),
106 })
107 }
108
109 pub async fn execute_queued_task(
111 &self,
112 task: &Task,
113 ) -> Result<TaskExecutionResult, TaskExecutionError> {
114 if let Some(ref storage) = self.task_storage {
116 let mut running_task = task.clone();
117 running_task.status = TaskStatus::Running;
118 running_task.started_at = Some(chrono::Utc::now());
119
120 if let Err(e) = storage.update_task(running_task.clone()).await {
121 eprintln!("Error updating task status: {e}");
122 }
123 }
124
125 let execution_result = self.task_runner.execute_task(task, false).await;
127
128 match execution_result {
129 Ok(result) => {
130 if let Some(ref storage) = self.task_storage {
132 let mut updated_task = task.clone();
133 updated_task.completed_at = Some(chrono::Utc::now());
134 updated_task.branch_name = result.branch_name.clone();
135
136 if let Some(task_result) = result.task_result.as_ref() {
138 if task_result.success {
139 updated_task.status = TaskStatus::Complete;
140 } else {
141 updated_task.status = TaskStatus::Failed;
142 updated_task.error_message = Some(task_result.message.clone());
143 }
144 } else {
145 updated_task.status = TaskStatus::Complete;
147 }
148
149 if let Err(e) = storage.update_task(updated_task).await {
150 eprintln!("Error updating task status: {e}");
151 }
152 }
153 Ok(result)
154 }
155 Err(e) => {
156 if let Some(ref storage) = self.task_storage {
158 let mut failed_task = task.clone();
159 failed_task.status = TaskStatus::Failed;
160 failed_task.error_message = Some(e.message.clone());
161 failed_task.completed_at = Some(chrono::Utc::now());
162
163 if let Err(storage_err) = storage.update_task(failed_task).await {
164 eprintln!("Error updating task status: {storage_err}");
165 }
166 }
167 Err(e)
168 }
169 }
170 }
171
172 pub async fn delete_task(&self, task_id: &str) -> Result<(), String> {
174 let storage = match &self.task_storage {
176 Some(s) => s,
177 None => return Err("Task storage not initialized".to_string()),
178 };
179
180 let task = storage
182 .get_task(task_id)
183 .await
184 .map_err(|e| format!("Error getting task: {e}"))?;
185
186 if task.is_none() {
187 return Err(format!("Task with ID '{task_id}' not found"));
188 }
189
190 storage
192 .delete_task(task_id)
193 .await
194 .map_err(|e| format!("Error deleting task from storage: {e}"))?;
195
196 let task = task.unwrap();
198 let repo_hash = crate::storage::get_repo_hash(&task.repo_root);
199 let task_dir = self.xdg_directories.task_dir(task_id, &repo_hash);
200 if self.file_system.exists(&task_dir).await.unwrap_or(false) {
201 self.file_system
202 .remove_dir(&task_dir)
203 .await
204 .map_err(|e| format!("Error deleting task directory: {e}"))?;
205 }
206
207 Ok(())
208 }
209
210 pub async fn clean_tasks(&self) -> Result<usize, String> {
212 let storage = match &self.task_storage {
214 Some(s) => s,
215 None => return Err("Task storage not initialized".to_string()),
216 };
217
218 let all_tasks = storage
220 .list_tasks()
221 .await
222 .map_err(|e| format!("Error listing tasks: {e}"))?;
223
224 let completed_tasks: Vec<&Task> = all_tasks
226 .iter()
227 .filter(|t| t.status == TaskStatus::Complete)
228 .collect();
229
230 for task in &completed_tasks {
232 let repo_hash = crate::storage::get_repo_hash(&task.repo_root);
233 let task_dir = self.xdg_directories.task_dir(&task.id, &repo_hash);
234 if self.file_system.exists(&task_dir).await.unwrap_or(false) {
235 if let Err(e) = self.file_system.remove_dir(&task_dir).await {
236 eprintln!(
237 "Warning: Failed to delete task directory {}: {}",
238 task.id, e
239 );
240 }
241 }
242 }
243
244 let deleted_count = storage
246 .delete_tasks_by_status(vec![TaskStatus::Complete])
247 .await
248 .map_err(|e| format!("Error deleting completed tasks: {e}"))?;
249
250 Ok(deleted_count)
251 }
252
253 pub async fn retry_task(
255 &self,
256 task_id: &str,
257 edit_instructions: bool,
258 ctx: &AppContext,
259 ) -> Result<String, String> {
260 let storage = match &self.task_storage {
262 Some(s) => s,
263 None => return Err("Task storage not initialized".to_string()),
264 };
265
266 let original_task = storage
268 .get_task(task_id)
269 .await
270 .map_err(|e| format!("Error getting task: {e}"))?;
271
272 let original_task = match original_task {
273 Some(task) => task,
274 None => return Err(format!("Task with ID '{task_id}' not found")),
275 };
276
277 if original_task.status == TaskStatus::Queued {
279 return Err("Cannot retry a task that hasn't been executed yet".to_string());
280 }
281
282 let new_task_name = format!("retry-{}", original_task.name);
284
285 let mut builder = TaskBuilder::from_existing(&original_task);
287
288 builder = builder.name(new_task_name).edit(edit_instructions);
289
290 let new_task = builder
291 .build(ctx)
292 .await
293 .map_err(|e| format!("Failed to build retry task: {e}"))?;
294
295 storage
297 .add_task(new_task.clone())
298 .await
299 .map_err(|e| format!("Error adding retry task to storage: {e}"))?;
300
301 Ok(new_task.id)
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::test_utils::FixedResponseDockerClient;
309 use std::path::PathBuf;
310 use std::sync::Arc;
311 use tempfile::TempDir;
312
313 fn create_temp_git_repo() -> (TempDir, PathBuf) {
315 let temp_dir = TempDir::new().unwrap();
316 let repo_root = temp_dir.path().to_path_buf();
317
318 std::fs::create_dir(repo_root.join(".git")).unwrap();
320
321 (temp_dir, repo_root)
322 }
323
324 #[tokio::test]
325 async fn test_delete_task() {
326 use crate::context::file_system::tests::MockFileSystem;
327 use crate::context::git_operations::tests::MockGitOperations;
328 use std::env;
329
330 let temp_dir = std::env::temp_dir();
332 let test_data_dir = temp_dir.join("tsk-test-data");
333 let test_runtime_dir = temp_dir.join("tsk-test-runtime");
334 unsafe {
335 env::set_var("XDG_DATA_HOME", test_data_dir.to_string_lossy().to_string());
336 }
337 unsafe {
338 env::set_var(
339 "XDG_RUNTIME_DIR",
340 test_runtime_dir.to_string_lossy().to_string(),
341 );
342 }
343
344 let xdg = Arc::new(XdgDirectories::new().unwrap());
346
347 let (_temp_repo, repo_root) = create_temp_git_repo();
349 let task_id = "test-task-123".to_string();
350 let repo_hash = crate::storage::get_repo_hash(&repo_root);
351
352 let task_dir_path = xdg.task_dir(&task_id, &repo_hash);
354 let tasks_json_path = xdg.tasks_file();
355 let data_dir = xdg.data_dir().to_path_buf();
356 let tasks_dir = data_dir.join("tasks");
357
358 let git_dir = repo_root.join(".git");
360
361 let fs = Arc::new(
362 MockFileSystem::new()
363 .with_dir(&git_dir.to_string_lossy().to_string())
364 .with_dir(&data_dir.to_string_lossy().to_string())
365 .with_dir(&tasks_dir.to_string_lossy().to_string())
366 .with_dir(&task_dir_path.to_string_lossy().to_string())
367 .with_file(&format!("{}/test.txt", task_dir_path.to_string_lossy()), "test content")
368 .with_file(&tasks_json_path.to_string_lossy().to_string(), &format!(r#"[{{"id":"{}","repo_root":"{}","name":"test-task","task_type":"feat","instructions_file":"instructions.md","agent":"claude-code","timeout":30,"status":"QUEUED","created_at":"2024-01-01T00:00:00Z","started_at":null,"completed_at":null,"branch_name":"tsk/{}","error_message":null,"source_commit":"abc123","tech_stack":"default","project":"default","copied_repo_path":"{}"}}]"#, task_id, repo_root.to_string_lossy(), task_id, task_dir_path.to_string_lossy()))
369 );
370
371 let docker_client = Arc::new(FixedResponseDockerClient::default());
372 let git_ops = Arc::new(MockGitOperations::new());
373
374 let ctx = AppContext::builder()
375 .with_docker_client(docker_client)
376 .with_file_system(fs.clone())
377 .with_git_operations(git_ops)
378 .with_xdg_directories(xdg)
379 .build();
380
381 let task_manager = TaskManager::with_storage(&ctx).unwrap();
382
383 let result = task_manager.delete_task(&task_id).await;
385 assert!(result.is_ok(), "Failed to delete task: {:?}", result);
386
387 let exists = fs.exists(&task_dir_path).await.unwrap();
389 assert!(!exists, "Task directory should have been deleted");
390 }
391
392 #[tokio::test]
393 async fn test_clean_tasks() {
394 use crate::context::file_system::tests::MockFileSystem;
395 use crate::context::git_operations::tests::MockGitOperations;
396 use std::env;
397
398 let temp_dir = std::env::temp_dir();
400 let test_data_dir = temp_dir.join("tsk-test-data2");
401 let test_runtime_dir = temp_dir.join("tsk-test-runtime2");
402 unsafe {
403 env::set_var("XDG_DATA_HOME", test_data_dir.to_string_lossy().to_string());
404 }
405 unsafe {
406 env::set_var(
407 "XDG_RUNTIME_DIR",
408 test_runtime_dir.to_string_lossy().to_string(),
409 );
410 }
411
412 let xdg = Arc::new(XdgDirectories::new().unwrap());
414
415 let (_temp_repo, repo_root) = create_temp_git_repo();
417 let repo_hash = crate::storage::get_repo_hash(&repo_root);
418 let queued_task_id = "queued-task-123".to_string();
419 let completed_task_id = "completed-task-456".to_string();
420
421 let _queued_task = Task::new(
422 queued_task_id.clone(),
423 repo_root.clone(),
424 "queued-task".to_string(),
425 "feat".to_string(),
426 "instructions.md".to_string(),
427 "claude-code".to_string(),
428 30,
429 format!("tsk/{queued_task_id}"),
430 "abc123".to_string(),
431 "default".to_string(),
432 "default".to_string(),
433 chrono::Local::now(),
434 repo_root.clone(),
435 );
436
437 let mut completed_task = Task::new(
438 completed_task_id.clone(),
439 repo_root.clone(),
440 "completed-task".to_string(),
441 "fix".to_string(),
442 "instructions.md".to_string(),
443 "claude-code".to_string(),
444 30,
445 format!("tsk/{completed_task_id}"),
446 "abc123".to_string(),
447 "default".to_string(),
448 "default".to_string(),
449 chrono::Local::now(),
450 repo_root.clone(),
451 );
452 completed_task.status = TaskStatus::Complete;
453
454 let queued_dir_path = xdg.task_dir(&queued_task_id, &repo_hash);
456 let completed_dir_path = xdg.task_dir(&completed_task_id, &repo_hash);
457 let tasks_json_path = xdg.tasks_file();
458 let data_dir = xdg.data_dir().to_path_buf();
459 let tasks_dir = data_dir.join("tasks");
460
461 let tasks_json = format!(
463 r#"[{{"id":"{}","repo_root":"{}","name":"queued-task","task_type":"feat","instructions_file":"instructions.md","agent":"claude-code","timeout":30,"status":"QUEUED","created_at":"2024-01-01T00:00:00Z","started_at":null,"completed_at":null,"branch_name":"tsk/{}","error_message":null,"source_commit":"abc123","tech_stack":"default","project":"default","copied_repo_path":"{}"}},{{"id":"{}","repo_root":"{}","name":"completed-task","task_type":"fix","instructions_file":"instructions.md","agent":"claude-code","timeout":30,"status":"COMPLETE","created_at":"2024-01-01T00:00:00Z","started_at":null,"completed_at":"2024-01-01T01:00:00Z","branch_name":"tsk/{}","error_message":null,"source_commit":"abc123","tech_stack":"default","project":"default","copied_repo_path":"{}"}}]"#,
464 queued_task_id,
465 repo_root.to_string_lossy(),
466 queued_task_id,
467 queued_dir_path.to_string_lossy(),
468 completed_task_id,
469 repo_root.to_string_lossy(),
470 completed_task_id,
471 completed_dir_path.to_string_lossy()
472 );
473
474 let git_dir = repo_root.join(".git");
476
477 let fs = Arc::new(
478 MockFileSystem::new()
479 .with_dir(&git_dir.to_string_lossy().to_string())
480 .with_dir(&data_dir.to_string_lossy().to_string())
481 .with_dir(&tasks_dir.to_string_lossy().to_string())
482 .with_dir(&queued_dir_path.to_string_lossy().to_string())
483 .with_dir(&completed_dir_path.to_string_lossy().to_string())
484 .with_file(&tasks_json_path.to_string_lossy().to_string(), &tasks_json),
485 );
486
487 let docker_client = Arc::new(FixedResponseDockerClient::default());
488 let git_ops = Arc::new(MockGitOperations::new());
489
490 let ctx = AppContext::builder()
491 .with_docker_client(docker_client)
492 .with_file_system(fs.clone())
493 .with_git_operations(git_ops)
494 .with_xdg_directories(xdg)
495 .build();
496
497 let task_manager = TaskManager::with_storage(&ctx).unwrap();
498
499 let result = task_manager.clean_tasks().await;
501 assert!(result.is_ok(), "Failed to clean tasks: {:?}", result);
502 let completed_count = result.unwrap();
503 assert_eq!(completed_count, 1);
504
505 let queued_exists = fs.exists(&queued_dir_path).await.unwrap();
507 let completed_exists = fs.exists(&completed_dir_path).await.unwrap();
508 assert!(queued_exists, "Queued task directory should still exist");
509 assert!(
510 !completed_exists,
511 "Completed task directory should be deleted"
512 );
513 }
514
515 #[tokio::test]
516 async fn test_retry_task() {
517 use crate::context::file_system::tests::MockFileSystem;
518 use crate::context::git_operations::tests::MockGitOperations;
519 use std::env;
520
521 let temp_dir = std::env::temp_dir();
523 let test_data_dir = temp_dir.join("tsk-test-data-retry");
524 let test_runtime_dir = temp_dir.join("tsk-test-runtime-retry");
525 unsafe {
526 env::set_var("XDG_DATA_HOME", test_data_dir.to_string_lossy().to_string());
527 }
528 unsafe {
529 env::set_var(
530 "XDG_RUNTIME_DIR",
531 test_runtime_dir.to_string_lossy().to_string(),
532 );
533 }
534
535 let xdg = Arc::new(XdgDirectories::new().unwrap());
537
538 let (_temp_repo, repo_root) = create_temp_git_repo();
540 let repo_hash = crate::storage::get_repo_hash(&repo_root);
541 let task_id = "2024-01-01-1200-generic-original-task".to_string();
542 let mut completed_task = Task::new(
543 task_id.clone(),
544 repo_root.clone(),
545 "original-task".to_string(),
546 "generic".to_string(),
547 format!(
548 "{}/tasks/{}/{}/instructions.md",
549 test_data_dir.to_string_lossy(),
550 repo_hash,
551 task_id
552 ),
553 "claude-code".to_string(),
554 45,
555 format!("tsk/{task_id}"),
556 "abc123".to_string(),
557 "default".to_string(),
558 "default".to_string(),
559 chrono::Local::now(),
560 repo_root.clone(),
561 );
562 completed_task.status = TaskStatus::Complete;
563
564 let task_dir_path = xdg.task_dir(&task_id, &repo_hash);
566 let instructions_path = task_dir_path.join("instructions.md");
567 let tasks_json_path = xdg.tasks_file();
568 let data_dir = xdg.data_dir().to_path_buf();
569 let tasks_dir = data_dir.join("tasks");
570
571 let tasks_json = format!(
573 r#"[{{"id":"{}","repo_root":"{}","name":"original-task","task_type":"generic","instructions_file":"{}","agent":"claude-code","timeout":45,"status":"COMPLETE","created_at":"2024-01-01T12:00:00Z","started_at":"2024-01-01T12:30:00Z","completed_at":"2024-01-01T13:00:00Z","branch_name":"tsk/{}","error_message":null,"source_commit":"abc123","tech_stack":"default","project":"default","copied_repo_path":"{}"}}]"#,
574 task_id,
575 repo_root.to_string_lossy(),
576 instructions_path.to_string_lossy(),
577 task_id,
578 task_dir_path.to_string_lossy()
579 );
580
581 let git_dir = repo_root.join(".git");
583 let instructions_content =
584 "# Original Task Instructions\n\nThis is the original task content.";
585
586 let fs = Arc::new(
587 MockFileSystem::new()
588 .with_dir(&git_dir.to_string_lossy().to_string())
589 .with_dir(&data_dir.to_string_lossy().to_string())
590 .with_dir(&tasks_dir.to_string_lossy().to_string())
591 .with_dir(&task_dir_path.to_string_lossy().to_string())
592 .with_file(
593 &instructions_path.to_string_lossy().to_string(),
594 instructions_content,
595 )
596 .with_file(&tasks_json_path.to_string_lossy().to_string(), &tasks_json),
597 );
598
599 let docker_client = Arc::new(FixedResponseDockerClient::default());
600 let git_ops = Arc::new(MockGitOperations::new());
601
602 let ctx = AppContext::builder()
603 .with_docker_client(docker_client)
604 .with_file_system(fs.clone())
605 .with_git_operations(git_ops)
606 .with_xdg_directories(xdg.clone())
607 .build();
608
609 let task_manager = TaskManager::with_storage(&ctx).unwrap();
610
611 let result = task_manager.retry_task(&task_id, false, &ctx).await;
613 assert!(result.is_ok(), "Failed to retry task: {:?}", result);
614 let new_task_id = result.unwrap();
615
616 assert!(new_task_id.contains("generic-retry-original-task"));
618
619 let storage = get_task_storage(xdg.clone(), fs.clone());
621 let new_task = storage.get_task(&new_task_id).await.unwrap();
622 assert!(new_task.is_some());
623 let new_task = new_task.unwrap();
624 assert_eq!(new_task.name, "retry-original-task");
625 assert_eq!(new_task.task_type, "generic");
626 assert_eq!(new_task.agent, "claude-code".to_string());
627 assert_eq!(new_task.timeout, 45);
628 assert_eq!(new_task.status, TaskStatus::Queued);
629
630 let new_task_dir = xdg.task_dir(&new_task_id, &repo_hash);
632 let new_instructions_path = new_task_dir.join("instructions.md");
633 assert!(fs.exists(&new_instructions_path).await.unwrap());
634
635 let copied_content = fs.read_file(&new_instructions_path).await.unwrap();
637 assert_eq!(copied_content, instructions_content);
638 }
639
640 #[tokio::test]
641 async fn test_retry_task_not_found() {
642 use crate::context::file_system::tests::MockFileSystem;
643 use crate::context::git_operations::tests::MockGitOperations;
644 use std::env;
645
646 let temp_dir = std::env::temp_dir();
648 let test_data_dir = temp_dir.join("tsk-test-data-retry-notfound");
649 let test_runtime_dir = temp_dir.join("tsk-test-runtime-retry-notfound");
650 unsafe {
651 env::set_var("XDG_DATA_HOME", test_data_dir.to_string_lossy().to_string());
652 }
653 unsafe {
654 env::set_var(
655 "XDG_RUNTIME_DIR",
656 test_runtime_dir.to_string_lossy().to_string(),
657 );
658 }
659
660 let xdg = Arc::new(XdgDirectories::new().unwrap());
662
663 let (_temp_repo, repo_root) = create_temp_git_repo();
665 let tasks_json_path = xdg.tasks_file();
666 let data_dir = xdg.data_dir().to_path_buf();
667 let tasks_dir = data_dir.join("tasks");
668
669 let tasks_json = "[]";
671
672 let git_dir = repo_root.join(".git");
674
675 let fs = Arc::new(
676 MockFileSystem::new()
677 .with_dir(&git_dir.to_string_lossy().to_string())
678 .with_dir(&data_dir.to_string_lossy().to_string())
679 .with_dir(&tasks_dir.to_string_lossy().to_string())
680 .with_file(&tasks_json_path.to_string_lossy().to_string(), tasks_json),
681 );
682
683 let docker_client = Arc::new(FixedResponseDockerClient::default());
684 let git_ops = Arc::new(MockGitOperations::new());
685
686 let ctx = AppContext::builder()
687 .with_docker_client(docker_client)
688 .with_file_system(fs.clone())
689 .with_git_operations(git_ops)
690 .with_xdg_directories(xdg)
691 .build();
692
693 let task_manager = TaskManager::with_storage(&ctx).unwrap();
694
695 let result = task_manager
697 .retry_task("non-existent-task", false, &ctx)
698 .await;
699 assert!(result.is_err());
700 assert!(
701 result
702 .unwrap_err()
703 .contains("Task with ID 'non-existent-task' not found")
704 );
705 }
706
707 #[tokio::test]
708 async fn test_retry_task_queued_error() {
709 use crate::context::file_system::tests::MockFileSystem;
710 use crate::context::git_operations::tests::MockGitOperations;
711 use std::env;
712
713 let temp_dir = std::env::temp_dir();
715 let test_data_dir = temp_dir.join("tsk-test-data-retry-queued");
716 let test_runtime_dir = temp_dir.join("tsk-test-runtime-retry-queued");
717 unsafe {
718 env::set_var("XDG_DATA_HOME", test_data_dir.to_string_lossy().to_string());
719 }
720 unsafe {
721 env::set_var(
722 "XDG_RUNTIME_DIR",
723 test_runtime_dir.to_string_lossy().to_string(),
724 );
725 }
726
727 let xdg = Arc::new(XdgDirectories::new().unwrap());
729
730 let (_temp_repo, repo_root) = create_temp_git_repo();
732 let task_id = "2024-01-01-1200-feat-queued-task".to_string();
733
734 let tasks_json_path = xdg.tasks_file();
736 let data_dir = xdg.data_dir().to_path_buf();
737 let tasks_dir = data_dir.join("tasks");
738
739 let tasks_json = format!(
741 r#"[{{"id":"{}","repo_root":"{}","name":"queued-task","task_type":"feat","instructions_file":"instructions.md","agent":"claude-code","timeout":30,"status":"QUEUED","created_at":"2024-01-01T12:00:00Z","started_at":null,"completed_at":null,"branch_name":"tsk/{}","error_message":null,"source_commit":"abc123","tech_stack":"default","project":"default","copied_repo_path":"{}"}}]"#,
742 task_id,
743 repo_root.to_string_lossy(),
744 task_id,
745 repo_root.to_string_lossy()
746 );
747
748 let git_dir = repo_root.join(".git");
750
751 let fs = Arc::new(
752 MockFileSystem::new()
753 .with_dir(&git_dir.to_string_lossy().to_string())
754 .with_dir(&data_dir.to_string_lossy().to_string())
755 .with_dir(&tasks_dir.to_string_lossy().to_string())
756 .with_file(&tasks_json_path.to_string_lossy().to_string(), &tasks_json),
757 );
758
759 let docker_client = Arc::new(FixedResponseDockerClient::default());
760 let git_ops = Arc::new(MockGitOperations::new());
761
762 let ctx = AppContext::builder()
763 .with_docker_client(docker_client)
764 .with_file_system(fs.clone())
765 .with_git_operations(git_ops)
766 .with_xdg_directories(xdg)
767 .build();
768
769 let task_manager = TaskManager::with_storage(&ctx).unwrap();
770
771 let result = task_manager.retry_task(&task_id, false, &ctx).await;
773 assert!(result.is_err());
774 assert!(
775 result
776 .unwrap_err()
777 .contains("Cannot retry a task that hasn't been executed yet")
778 );
779 }
780
781 #[tokio::test]
782 async fn test_clean_tasks_with_id_matching() {
783 use crate::context::file_system::tests::MockFileSystem;
784 use crate::context::git_operations::tests::MockGitOperations;
785 use std::env;
786
787 let temp_dir = std::env::temp_dir();
789 let test_data_dir = temp_dir.join("tsk-test-data3");
790 let test_runtime_dir = temp_dir.join("tsk-test-runtime3");
791 unsafe {
792 env::set_var("XDG_DATA_HOME", test_data_dir.to_string_lossy().to_string());
793 }
794 unsafe {
795 env::set_var(
796 "XDG_RUNTIME_DIR",
797 test_runtime_dir.to_string_lossy().to_string(),
798 );
799 }
800
801 let xdg = Arc::new(XdgDirectories::new().unwrap());
803
804 let (_temp_repo, repo_root) = create_temp_git_repo();
806 let repo_hash = crate::storage::get_repo_hash(&repo_root);
807 let task_id = "2024-01-15-1430-feat-test-feature".to_string();
808 let mut completed_task = Task::new(
809 task_id.clone(),
810 repo_root.clone(),
811 "test-feature".to_string(),
812 "feat".to_string(),
813 "instructions.md".to_string(),
814 "claude-code".to_string(),
815 30,
816 format!("tsk/{task_id}"),
817 "abc123".to_string(),
818 "default".to_string(),
819 "default".to_string(),
820 chrono::Local::now(),
821 repo_root.clone(),
822 );
823 completed_task.status = TaskStatus::Complete;
824
825 let task_dir_path = xdg.task_dir(&task_id, &repo_hash);
827 let tasks_json_path = xdg.tasks_file();
828 let data_dir = xdg.data_dir().to_path_buf();
829 let tasks_dir = data_dir.join("tasks");
830
831 let tasks_json = format!(
833 r#"[{{"id":"{}","repo_root":"{}","name":"test-feature","task_type":"feat","instructions_file":"instructions.md","agent":"claude-code","timeout":30,"status":"COMPLETE","created_at":"2024-01-15T14:30:00Z","started_at":null,"completed_at":"2024-01-15T15:00:00Z","branch_name":"tsk/{}","error_message":null,"source_commit":"abc123","tech_stack":"default","project":"default","copied_repo_path":"{}"}}]"#,
834 task_id,
835 repo_root.to_string_lossy(),
836 task_id,
837 task_dir_path.to_string_lossy()
838 );
839
840 let git_dir = repo_root.join(".git");
842
843 let fs = Arc::new(
844 MockFileSystem::new()
845 .with_dir(&git_dir.to_string_lossy().to_string())
846 .with_dir(&data_dir.to_string_lossy().to_string())
847 .with_dir(&tasks_dir.to_string_lossy().to_string())
848 .with_dir(&task_dir_path.to_string_lossy().to_string())
849 .with_file(
850 &format!("{}/instructions.md", task_dir_path.to_string_lossy()),
851 "Test instructions",
852 )
853 .with_file(&tasks_json_path.to_string_lossy().to_string(), &tasks_json),
854 );
855
856 let docker_client = Arc::new(FixedResponseDockerClient::default());
857 let git_ops = Arc::new(MockGitOperations::new());
858
859 let ctx = AppContext::builder()
860 .with_docker_client(docker_client)
861 .with_file_system(fs.clone())
862 .with_git_operations(git_ops)
863 .with_xdg_directories(xdg)
864 .build();
865
866 let task_manager = TaskManager::with_storage(&ctx).unwrap();
867
868 let result = task_manager.clean_tasks().await;
870 assert!(result.is_ok(), "Failed to clean tasks: {:?}", result);
871 let completed_count = result.unwrap();
872 assert_eq!(completed_count, 1);
873
874 let task_dir_exists = fs.exists(&task_dir_path).await.unwrap();
876 assert!(!task_dir_exists, "Task directory should have been deleted");
877 }
878
879 #[tokio::test]
880 async fn test_with_storage_no_git_repo() {
881 use crate::context::file_system::tests::MockFileSystem;
882 use crate::context::git_operations::tests::MockGitOperations;
883 use std::env;
884
885 let temp_dir = std::env::temp_dir();
887 let test_data_dir = temp_dir.join("tsk-test-data-no-git");
888 let test_runtime_dir = temp_dir.join("tsk-test-runtime-no-git");
889 unsafe {
890 env::set_var("XDG_DATA_HOME", test_data_dir.to_string_lossy().to_string());
891 }
892 unsafe {
893 env::set_var(
894 "XDG_RUNTIME_DIR",
895 test_runtime_dir.to_string_lossy().to_string(),
896 );
897 }
898
899 let xdg = Arc::new(XdgDirectories::new().unwrap());
901
902 let tasks_json_path = xdg.tasks_file();
904 let data_dir = xdg.data_dir().to_path_buf();
905 let tasks_dir = data_dir.join("tasks");
906
907 let tasks_json = "[]";
909
910 let fs = Arc::new(
912 MockFileSystem::new()
913 .with_dir(&data_dir.to_string_lossy().to_string())
914 .with_dir(&tasks_dir.to_string_lossy().to_string())
915 .with_file(&tasks_json_path.to_string_lossy().to_string(), tasks_json),
916 );
917
918 let docker_client = Arc::new(FixedResponseDockerClient::default());
919 let git_ops = Arc::new(MockGitOperations::new());
920
921 let ctx = AppContext::builder()
922 .with_docker_client(docker_client)
923 .with_file_system(fs.clone())
924 .with_git_operations(git_ops)
925 .with_xdg_directories(xdg)
926 .build();
927
928 let result = TaskManager::with_storage(&ctx);
930 assert!(
931 result.is_ok(),
932 "TaskManager::with_storage should work without a git repository"
933 );
934 }
935}