tsk/
task_runner.rs

1use crate::agent::AgentProvider;
2use crate::context::file_system::FileSystemOperations;
3use crate::docker::{DockerManager, image_manager::DockerImageManager};
4use crate::git::RepoManager;
5use crate::notifications::NotificationClient;
6use crate::task::Task;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10pub struct TaskExecutionResult {
11    #[allow(dead_code)] // Available for future use by callers
12    pub repo_path: PathBuf,
13    pub branch_name: String,
14    #[allow(dead_code)] // Available for future use by callers
15    pub output: String,
16    pub task_result: Option<crate::agent::TaskResult>,
17}
18
19#[derive(Debug)]
20pub struct TaskExecutionError {
21    pub message: String,
22}
23
24impl From<String> for TaskExecutionError {
25    fn from(message: String) -> Self {
26        Self { message }
27    }
28}
29
30pub struct TaskRunner {
31    repo_manager: RepoManager,
32    docker_manager: DockerManager,
33    docker_image_manager: Arc<DockerImageManager>,
34    notification_client: Arc<dyn NotificationClient>,
35}
36
37impl TaskRunner {
38    pub fn new(
39        repo_manager: RepoManager,
40        docker_manager: DockerManager,
41        docker_image_manager: Arc<DockerImageManager>,
42        _file_system: Arc<dyn FileSystemOperations>, // Keep for compatibility
43        notification_client: Arc<dyn NotificationClient>,
44    ) -> Self {
45        Self {
46            repo_manager,
47            docker_manager,
48            docker_image_manager,
49            notification_client,
50        }
51    }
52
53    /// Execute a task
54    pub async fn execute_task(
55        &self,
56        task: &Task,
57        is_interactive: bool,
58    ) -> Result<TaskExecutionResult, TaskExecutionError> {
59        // Get the agent for this task
60        let agent = AgentProvider::get_agent(&task.agent)
61            .map_err(|e| format!("Error getting agent: {e}"))?;
62
63        // Validate the agent
64        agent
65            .validate()
66            .await
67            .map_err(|e| format!("Agent validation failed: {e}"))?;
68
69        // Run agent warmup
70        agent
71            .warmup()
72            .await
73            .map_err(|e| format!("Agent warmup failed: {e}"))?;
74
75        // Use the pre-copied repository path
76        let repo_path = task.copied_repo_path.clone();
77
78        let branch_name = task.branch_name.clone();
79
80        println!("Using repository copy at: {}", repo_path.display());
81
82        // Get the instructions file path
83        let instructions_file_path = PathBuf::from(&task.instructions_file);
84
85        // Launch Docker container
86        println!("Launching Docker container with {} agent...", agent.name());
87        println!("\n{}", "=".repeat(60));
88
89        let (output, task_result_from_container) = {
90            // Ensure the Docker image exists - always rebuild to pick up any changes
91            let docker_image = self
92                .docker_image_manager
93                .ensure_image(
94                    &task.tech_stack,
95                    &task.agent,
96                    Some(&task.project),
97                    Some(&repo_path),
98                    true,
99                )
100                .await
101                .map_err(|e| format!("Error ensuring Docker image: {e}"))?;
102
103            if docker_image.used_fallback {
104                println!(
105                    "Note: Using default project layer as project-specific layer was not found"
106                );
107            }
108
109            // Prepare log file path for non-interactive sessions
110            let log_file_path = if !is_interactive {
111                let task_dir = repo_path.parent().unwrap_or(&repo_path);
112                Some(task_dir.join(format!("{}-full.log", task.name)))
113            } else {
114                None
115            };
116
117            // Run the container using the unified method
118            self.docker_manager
119                .run_task_container(
120                    &docker_image.tag,
121                    &repo_path,
122                    Some(&instructions_file_path),
123                    agent.as_ref(),
124                    is_interactive,
125                    &task.name,
126                    log_file_path.as_deref(),
127                )
128                .await
129                .map_err(|e| format!("Error running container: {e}"))?
130        };
131
132        println!("\n{}", "=".repeat(60));
133        println!("Container execution completed successfully");
134
135        // Commit any changes made by the container
136        let commit_message = format!("TSK automated changes for task: {}", task.name);
137        if let Err(e) = self
138            .repo_manager
139            .commit_changes(&repo_path, &commit_message)
140            .await
141        {
142            eprintln!("Error committing changes: {e}");
143        }
144
145        // Fetch changes back to main repository
146        match self
147            .repo_manager
148            .fetch_changes(&repo_path, &branch_name, &task.repo_root)
149            .await
150        {
151            Ok(true) => {
152                println!("Branch {branch_name} is now available in the main repository");
153            }
154            Ok(false) => {
155                println!("No changes to merge - branch was not created");
156            }
157            Err(e) => {
158                eprintln!("Error fetching changes: {e}");
159            }
160        }
161
162        // Use the task result from the container execution
163        let task_result = task_result_from_container;
164
165        // Send notification about task completion
166        let success = task_result.as_ref().map(|r| r.success).unwrap_or(false);
167        let message = task_result.as_ref().map(|r| r.message.as_str());
168        self.notification_client
169            .notify_task_complete(&task.name, success, message);
170
171        Ok(TaskExecutionResult {
172            repo_path,
173            branch_name,
174            output,
175            task_result,
176        })
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::task::{Task, TaskStatus};
184    use crate::test_utils::FixedResponseDockerClient;
185    use std::sync::Arc;
186
187    #[tokio::test]
188    #[ignore = "Test requires Docker to be available for image building"]
189    async fn test_execute_task_success() {
190        use crate::context::file_system::tests::MockFileSystem;
191        use crate::git::RepoManager;
192
193        // Set up a temporary home directory with a mock .claude.json file
194        let temp_dir = tempfile::tempdir().unwrap();
195        let claude_json_path = temp_dir.path().join(".claude.json");
196        std::fs::write(&claude_json_path, "{}").unwrap();
197        unsafe {
198            std::env::set_var("HOME", temp_dir.path());
199        }
200
201        // Set up git configuration for tests
202        unsafe {
203            std::env::set_var("GIT_CONFIG_GLOBAL", temp_dir.path().join(".gitconfig"));
204        }
205        unsafe {
206            std::env::set_var("GIT_CONFIG_SYSTEM", "/dev/null");
207        }
208
209        // Configure git for the test
210        std::process::Command::new("git")
211            .args(["config", "--global", "user.name", "Test User"])
212            .output()
213            .unwrap();
214        std::process::Command::new("git")
215            .args(["config", "--global", "user.email", "test@example.com"])
216            .output()
217            .unwrap();
218
219        // Create mock file system with necessary files and directories
220        let fs = Arc::new(
221            MockFileSystem::new()
222                .with_dir(".git")
223                .with_dir(".tsk")
224                .with_dir(".tsk/tasks")
225                .with_file("test.txt", "test content")
226                .with_file("instructions.md", "Test task instructions"),
227        );
228
229        let git_ops = Arc::new(crate::context::git_operations::tests::MockGitOperations::new());
230        let docker_client = Arc::new(FixedResponseDockerClient::default());
231
232        // Create test XDG directories
233        unsafe {
234            std::env::set_var("XDG_DATA_HOME", "/tmp/test-xdg-data");
235        }
236        unsafe {
237            std::env::set_var("XDG_RUNTIME_DIR", "/tmp/test-xdg-runtime");
238        }
239        let xdg_directories = Arc::new(crate::storage::XdgDirectories::new().unwrap());
240
241        let repo_manager = RepoManager::new(xdg_directories.clone(), fs.clone(), git_ops);
242        let docker_manager = crate::docker::DockerManager::new(docker_client.clone(), fs.clone());
243
244        // Create a mock docker image manager
245        use crate::assets::embedded::EmbeddedAssetManager;
246        use crate::docker::composer::DockerComposer;
247        use crate::docker::template_manager::DockerTemplateManager;
248
249        let template_manager =
250            DockerTemplateManager::new(Arc::new(EmbeddedAssetManager), xdg_directories.clone());
251        let composer = DockerComposer::new(DockerTemplateManager::new(
252            Arc::new(EmbeddedAssetManager),
253            xdg_directories,
254        ));
255        let docker_image_manager = Arc::new(DockerImageManager::new(
256            docker_client,
257            template_manager,
258            composer,
259        ));
260
261        let notification_client = Arc::new(crate::notifications::NoOpNotificationClient);
262        let task_runner = TaskRunner::new(
263            repo_manager,
264            docker_manager,
265            docker_image_manager,
266            fs,
267            notification_client,
268        );
269
270        let task = Task {
271            id: "test-task-123".to_string(),
272            repo_root: temp_dir.path().to_path_buf(),
273            name: "test-task".to_string(),
274            task_type: "feature".to_string(),
275            instructions_file: "instructions.md".to_string(),
276            agent: "claude-code".to_string(),
277            timeout: 30,
278            status: TaskStatus::Queued,
279            created_at: chrono::Local::now(),
280            started_at: None,
281            completed_at: None,
282            branch_name: "tsk/test-task-123".to_string(),
283            error_message: None,
284            source_commit: "abc123".to_string(),
285            tech_stack: "default".to_string(),
286            project: "default".to_string(),
287            copied_repo_path: std::env::current_dir().unwrap(),
288        };
289
290        let result = task_runner.execute_task(&task, false).await;
291
292        assert!(result.is_ok(), "Error: {:?}", result.as_ref().err());
293        let execution_result = result.unwrap();
294        assert_eq!(execution_result.output, "Test output");
295        assert!(execution_result.branch_name.contains("test-task"));
296    }
297}