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)] pub repo_path: PathBuf,
13 pub branch_name: String,
14 #[allow(dead_code)] 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>, 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 pub async fn execute_task(
55 &self,
56 task: &Task,
57 is_interactive: bool,
58 ) -> Result<TaskExecutionResult, TaskExecutionError> {
59 let agent = AgentProvider::get_agent(&task.agent)
61 .map_err(|e| format!("Error getting agent: {e}"))?;
62
63 agent
65 .validate()
66 .await
67 .map_err(|e| format!("Agent validation failed: {e}"))?;
68
69 agent
71 .warmup()
72 .await
73 .map_err(|e| format!("Agent warmup failed: {e}"))?;
74
75 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 let instructions_file_path = PathBuf::from(&task.instructions_file);
84
85 println!("Launching Docker container with {} agent...", agent.name());
87 println!("\n{}", "=".repeat(60));
88
89 let (output, task_result_from_container) = {
90 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 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 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 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 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 let task_result = task_result_from_container;
164
165 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 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 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 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 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 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 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}