1use crate::git::GitError;
8use crate::manager::WorktreeInfo;
9use miyabi_types::error::MiyabiError;
10use miyabi_types::world::WorldId;
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14use tokio::process::Command;
15use tokio::sync::Mutex;
16use tracing::{debug, error, info, warn};
17
18#[derive(Debug, Clone)]
20pub struct WorldWorktreeHandle {
21 pub world_id: WorldId,
23 pub path: PathBuf,
25 pub branch: String,
27 pub info: WorktreeInfo,
29}
30
31pub struct FiveWorldsManager {
33 base_path: PathBuf,
35 repo_path: PathBuf,
37 active_worlds: Arc<Mutex<HashMap<WorldId, WorldWorktreeHandle>>>,
39}
40
41impl FiveWorldsManager {
42 pub fn new(base_path: PathBuf, repo_path: PathBuf) -> Self {
48 Self {
49 base_path,
50 repo_path,
51 active_worlds: Arc::new(Mutex::new(HashMap::new())),
52 }
53 }
54
55 pub async fn spawn_all_worlds(
86 &self,
87 issue_number: u64,
88 task_name: &str,
89 ) -> Result<HashMap<WorldId, WorldWorktreeHandle>, GitError> {
90 info!(issue_number = issue_number, task_name = task_name, "Spawning all 5 worlds");
91
92 let mut handles = HashMap::new();
93
94 for world_id in WorldId::all() {
96 match self.spawn_world(issue_number, task_name, world_id).await {
97 Ok(handle) => {
98 debug!(
99 world_id = ?world_id,
100 path = ?handle.path,
101 "World spawned successfully"
102 );
103 handles.insert(world_id, handle);
104 },
105 Err(e) => {
106 error!(
107 world_id = ?world_id,
108 error = %e,
109 "Failed to spawn world"
110 );
111 self.cleanup_worlds(&handles).await;
113 return Err(e);
114 },
115 }
116 }
117
118 info!("All 5 worlds spawned successfully");
119 Ok(handles)
120 }
121
122 pub async fn spawn_world(
129 &self,
130 issue_number: u64,
131 task_name: &str,
132 world_id: WorldId,
133 ) -> Result<WorldWorktreeHandle, GitError> {
134 let branch_name = format!(
136 "world-{}-issue-{}-{}",
137 world_id.to_string().to_lowercase(),
138 issue_number,
139 task_name
140 );
141
142 let worktree_path = self
144 .base_path
145 .join(format!("world-{}", world_id.to_string().to_lowercase()))
146 .join(format!("issue-{}", issue_number))
147 .join(task_name);
148
149 debug!(
150 world_id = ?world_id,
151 branch = %branch_name,
152 path = ?worktree_path,
153 "Creating world worktree"
154 );
155
156 self.create_worktree_direct(&worktree_path, &branch_name)
158 .await
159 .map_err(|e| GitError::CommandFailed(e.to_string()))?;
160
161 let info = WorktreeInfo {
163 id: uuid::Uuid::new_v4().to_string(),
164 issue_number,
165 path: worktree_path.clone(),
166 branch_name: branch_name.clone(),
167 created_at: chrono::Utc::now(),
168 status: crate::manager::WorktreeStatus::Active,
169 };
170
171 let handle = WorldWorktreeHandle {
172 world_id,
173 path: worktree_path.clone(),
174 branch: branch_name,
175 info,
176 };
177
178 self.active_worlds.lock().await.insert(world_id, handle.clone());
180
181 Ok(handle)
182 }
183
184 async fn create_worktree_direct(
186 &self,
187 worktree_path: &Path,
188 branch_name: &str,
189 ) -> Result<(), MiyabiError> {
190 if let Some(parent) = worktree_path.parent() {
192 tokio::fs::create_dir_all(parent)
193 .await
194 .map_err(|e| MiyabiError::Git(format!("Failed to create parent dirs: {}", e)))?;
195 }
196
197 let output = Command::new("git")
199 .arg("worktree")
200 .arg("add")
201 .arg("-b")
202 .arg(branch_name)
203 .arg(worktree_path)
204 .arg("HEAD")
205 .current_dir(&self.repo_path)
206 .output()
207 .await
208 .map_err(|e| MiyabiError::Git(format!("Failed to execute git worktree add: {}", e)))?;
209
210 if !output.status.success() {
211 let stderr = String::from_utf8_lossy(&output.stderr);
212 return Err(MiyabiError::Git(format!("Failed to create worktree: {}", stderr)));
213 }
214
215 Ok(())
216 }
217
218 pub async fn cleanup_world(&self, world_id: WorldId) -> Result<(), GitError> {
223 let handle = {
224 let mut active = self.active_worlds.lock().await;
225 active.remove(&world_id)
226 };
227
228 if let Some(handle) = handle {
229 debug!(
230 world_id = ?world_id,
231 path = ?handle.path,
232 "Cleaning up world worktree"
233 );
234
235 self.remove_worktree_direct(&handle.path)
236 .await
237 .map_err(|e| GitError::CommandFailed(e.to_string()))?;
238
239 info!(world_id = ?world_id, "World worktree cleaned up");
240 } else {
241 warn!(world_id = ?world_id, "World not found in active worlds");
242 }
243
244 Ok(())
245 }
246
247 async fn remove_worktree_direct(&self, worktree_path: &Path) -> Result<(), MiyabiError> {
249 let output = Command::new("git")
250 .arg("worktree")
251 .arg("remove")
252 .arg("--force")
253 .arg(worktree_path)
254 .current_dir(&self.repo_path)
255 .output()
256 .await
257 .map_err(|e| {
258 MiyabiError::Git(format!("Failed to execute git worktree remove: {}", e))
259 })?;
260
261 if !output.status.success() {
262 let stderr = String::from_utf8_lossy(&output.stderr);
263 return Err(MiyabiError::Git(format!("Failed to remove worktree: {}", stderr)));
264 }
265
266 Ok(())
267 }
268
269 async fn cleanup_worlds(&self, handles: &HashMap<WorldId, WorldWorktreeHandle>) {
274 use futures::stream::{FuturesUnordered, StreamExt};
275
276 let mut cleanup_futures = FuturesUnordered::new();
278
279 for (world_id, handle) in handles {
280 let world_id = *world_id;
281 let path = handle.path.clone();
282 let repo_path = self.repo_path.clone();
283
284 cleanup_futures.push(async move {
285 let result = Command::new("git")
287 .arg("worktree")
288 .arg("remove")
289 .arg("--force")
290 .arg(&path)
291 .current_dir(&repo_path)
292 .output()
293 .await;
294
295 match result {
296 Ok(output) if output.status.success() => {
297 debug!(world_id = ?world_id, path = ?path, "World worktree cleaned up");
298 },
299 Ok(output) => {
300 let stderr = String::from_utf8_lossy(&output.stderr);
301 error!(
302 world_id = ?world_id,
303 path = ?path,
304 error = %stderr,
305 "Failed to cleanup world worktree"
306 );
307 },
308 Err(e) => {
309 error!(
310 world_id = ?world_id,
311 path = ?path,
312 error = %e,
313 "Failed to execute git worktree remove"
314 );
315 },
316 }
317 });
318 }
319
320 while cleanup_futures.next().await.is_some() {}
322 }
323
324 pub async fn cleanup_all_worlds_for_issue(&self, issue_number: u64) -> Result<(), GitError> {
329 use futures::stream::{FuturesUnordered, StreamExt};
330
331 info!(issue_number = issue_number, "Cleaning up all worlds for issue");
332
333 let worlds_to_cleanup: Vec<(WorldId, Option<WorldWorktreeHandle>)> = {
334 let mut active = self.active_worlds.lock().await;
335 WorldId::all()
336 .into_iter()
337 .map(|world_id| {
338 let handle = active.remove(&world_id);
339 (world_id, handle)
340 })
341 .collect()
342 };
343
344 let mut cleanup_futures = FuturesUnordered::new();
346
347 for (world_id, handle_opt) in worlds_to_cleanup {
348 if let Some(handle) = handle_opt {
349 let path = handle.path.clone();
350 let repo_path = self.repo_path.clone();
351
352 cleanup_futures.push(async move {
353 let result = Command::new("git")
354 .arg("worktree")
355 .arg("remove")
356 .arg("--force")
357 .arg(&path)
358 .current_dir(&repo_path)
359 .output()
360 .await;
361
362 match result {
363 Ok(output) if output.status.success() => {
364 debug!(world_id = ?world_id, "World cleaned up");
365 },
366 Ok(output) => {
367 let stderr = String::from_utf8_lossy(&output.stderr);
368 warn!(
369 world_id = ?world_id,
370 error = %stderr,
371 "Failed to cleanup world"
372 );
373 },
374 Err(e) => {
375 warn!(
376 world_id = ?world_id,
377 error = %e,
378 "Failed to execute git worktree remove"
379 );
380 },
381 }
382 });
383 }
384 }
385
386 while cleanup_futures.next().await.is_some() {}
388
389 info!(issue_number = issue_number, "All worlds cleaned up");
390 Ok(())
391 }
392
393 pub async fn get_world_handle(&self, world_id: WorldId) -> Option<WorldWorktreeHandle> {
401 self.active_worlds.lock().await.get(&world_id).cloned()
402 }
403
404 pub async fn get_all_world_handles(&self) -> HashMap<WorldId, WorldWorktreeHandle> {
409 self.active_worlds.lock().await.clone()
410 }
411
412 pub async fn is_world_active(&self, world_id: WorldId) -> bool {
417 self.active_worlds.lock().await.contains_key(&world_id)
418 }
419
420 pub async fn active_world_count(&self) -> usize {
422 self.active_worlds.lock().await.len()
423 }
424
425 pub async fn merge_winning_world(
431 &self,
432 world_id: WorldId,
433 target_branch: &str,
434 ) -> Result<(), GitError> {
435 let handle = self
436 .get_world_handle(world_id)
437 .await
438 .ok_or_else(|| GitError::InvalidPath(format!("World {:?} not found", world_id)))?;
439
440 info!(
441 world_id = ?world_id,
442 branch = %handle.branch,
443 target = target_branch,
444 "Merging winning world"
445 );
446
447 Ok(())
452 }
453
454 pub fn base_path(&self) -> &Path {
456 &self.base_path
457 }
458
459 pub async fn get_statistics(&self) -> WorldStatistics {
461 let active = self.active_worlds.lock().await;
462
463 WorldStatistics {
464 total_active: active.len(),
465 worlds: active.iter().map(|(id, handle)| (*id, handle.path.clone())).collect(),
466 }
467 }
468}
469
470#[derive(Debug, Clone)]
472pub struct WorldStatistics {
473 pub total_active: usize,
475 pub worlds: HashMap<WorldId, PathBuf>,
477}
478
479impl WorldStatistics {
480 pub fn all_active(&self) -> bool {
482 self.total_active == 5
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use tempfile::TempDir;
490
491 async fn setup_test_manager() -> (FiveWorldsManager, TempDir, TempDir) {
492 let repo_dir = TempDir::new().unwrap();
493 let worktree_dir = TempDir::new().unwrap();
494
495 let repo = git2::Repository::init(repo_dir.path()).unwrap();
497 let sig = git2::Signature::now("Test", "test@example.com").unwrap();
498 let tree_id = {
499 let mut index = repo.index().unwrap();
500 index.write_tree().unwrap()
501 };
502 let tree = repo.find_tree(tree_id).unwrap();
503 repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[]).unwrap();
504
505 let manager = FiveWorldsManager::new(
506 worktree_dir.path().to_path_buf(),
507 repo_dir.path().to_path_buf(),
508 );
509
510 (manager, repo_dir, worktree_dir)
511 }
512
513 #[tokio::test]
514 async fn test_spawn_single_world() {
515 let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
516
517 let handle = manager
518 .spawn_world(270, "test_task", WorldId::Alpha)
519 .await
520 .expect("Failed to spawn world");
521
522 assert_eq!(handle.world_id, WorldId::Alpha);
523 assert!(handle.path.to_string_lossy().contains("world-alpha"));
524 assert!(handle.branch.contains("world-alpha-issue-270"));
525
526 assert!(manager.is_world_active(WorldId::Alpha).await);
528 assert_eq!(manager.active_world_count().await, 1);
529
530 manager.cleanup_world(WorldId::Alpha).await.unwrap();
532 assert!(!manager.is_world_active(WorldId::Alpha).await);
533 }
534
535 #[tokio::test]
536 async fn test_spawn_all_worlds() {
537 let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
538
539 let handles = manager
540 .spawn_all_worlds(270, "test_task")
541 .await
542 .expect("Failed to spawn all worlds");
543
544 assert_eq!(handles.len(), 5);
545 assert!(handles.contains_key(&WorldId::Alpha));
546 assert!(handles.contains_key(&WorldId::Beta));
547 assert!(handles.contains_key(&WorldId::Gamma));
548 assert!(handles.contains_key(&WorldId::Delta));
549 assert!(handles.contains_key(&WorldId::Epsilon));
550
551 for world_id in WorldId::all() {
553 assert!(manager.is_world_active(world_id).await);
554 }
555
556 let stats = manager.get_statistics().await;
557 assert!(stats.all_active());
558
559 manager.cleanup_all_worlds_for_issue(270).await.unwrap();
561 assert_eq!(manager.active_world_count().await, 0);
562 }
563
564 #[tokio::test]
565 async fn test_world_handle_retrieval() {
566 let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
567
568 manager.spawn_world(270, "test_task", WorldId::Alpha).await.unwrap();
569
570 let handle = manager.get_world_handle(WorldId::Alpha).await;
572 assert!(handle.is_some());
573 assert_eq!(handle.unwrap().world_id, WorldId::Alpha);
574
575 let no_handle = manager.get_world_handle(WorldId::Beta).await;
577 assert!(no_handle.is_none());
578
579 let all_handles = manager.get_all_world_handles().await;
581 assert_eq!(all_handles.len(), 1);
582 assert!(all_handles.contains_key(&WorldId::Alpha));
583
584 manager.cleanup_world(WorldId::Alpha).await.unwrap();
586 }
587
588 #[tokio::test]
589 async fn test_statistics() {
590 let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
591
592 let stats = manager.get_statistics().await;
594 assert_eq!(stats.total_active, 0);
595 assert!(!stats.all_active());
596
597 manager.spawn_all_worlds(270, "test_task").await.unwrap();
599
600 let stats = manager.get_statistics().await;
601 assert_eq!(stats.total_active, 5);
602 assert!(stats.all_active());
603 assert_eq!(stats.worlds.len(), 5);
604
605 manager.cleanup_all_worlds_for_issue(270).await.unwrap();
607 }
608
609 #[tokio::test]
610 async fn test_partial_cleanup() {
611 let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
612
613 manager.spawn_all_worlds(270, "test_task").await.unwrap();
614 assert_eq!(manager.active_world_count().await, 5);
615
616 manager.cleanup_world(WorldId::Alpha).await.unwrap();
618 manager.cleanup_world(WorldId::Beta).await.unwrap();
619
620 assert_eq!(manager.active_world_count().await, 3);
621 assert!(!manager.is_world_active(WorldId::Alpha).await);
622 assert!(!manager.is_world_active(WorldId::Beta).await);
623 assert!(manager.is_world_active(WorldId::Gamma).await);
624
625 manager.cleanup_all_worlds_for_issue(270).await.unwrap();
627 }
628}