//! Integration tests for g3-ensembles flock mode use g3_ensembles::{FlockConfig, FlockMode}; use std::fs; use std::path::PathBuf; use std::process::Command; use tempfile::TempDir; /// Create a test config file with the new format fn create_test_config(temp_dir: &TempDir) -> PathBuf { let config_path = temp_dir.path().join(".g3.toml"); let config_content = r#" [providers] default_provider = "databricks.default" [providers.databricks.default] host = "https://test.databricks.com" token = "test-token" model = "test-model" [agent] fallback_default_max_tokens = 8192 enable_streaming = true timeout_seconds = 60 auto_compact = true max_retry_attempts = 3 autonomous_max_retry_attempts = 6 [computer_control] enabled = false require_confirmation = true max_actions_per_second = 10 [webdriver] enabled = false safari_port = 4444 [macax] enabled = false "#; fs::write(&config_path, config_content).expect("Failed to write config"); config_path } /// Helper to create a test git repository with flock-requirements.md fn create_test_project(name: &str) -> TempDir { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let project_path = temp_dir.path(); // Initialize git repo let output = Command::new("git") .arg("init") .current_dir(project_path) .output() .expect("Failed to run git init"); assert!(output.status.success(), "git init failed"); // Configure git user (required for commits) Command::new("git") .args(["config", "user.email", "test@example.com"]) .current_dir(project_path) .output() .expect("Failed to configure git email"); Command::new("git") .args(["config", "user.name", "Test User"]) .current_dir(project_path) .output() .expect("Failed to configure git name"); // Create flock-requirements.md let requirements = format!( "# {} Test Project\n\n\ ## Module A\n\ - Create a simple Rust library\n\ - Add a function that returns \"Hello from Module A\"\n\ - Write a unit test for the function\n\n\ ## Module B\n\ - Create another Rust library\n\ - Add a function that returns \"Hello from Module B\"\n\ - Write a unit test for the function\n", name ); fs::write(project_path.join("flock-requirements.md"), requirements) .expect("Failed to write requirements"); // Create a simple README fs::write(project_path.join("README.md"), format!("# {}\n", name)) .expect("Failed to write README"); // Create initial commit Command::new("git") .args(["add", "."]) .current_dir(project_path) .output() .expect("Failed to git add"); let output = Command::new("git") .args(["commit", "-m", "Initial commit"]) .current_dir(project_path) .output() .expect("Failed to git commit"); assert!(output.status.success(), "git commit failed"); temp_dir } #[test] fn test_flock_config_validation() { let temp_dir = TempDir::new().unwrap(); let config_path = create_test_config(&temp_dir); let project_path = temp_dir.path().to_path_buf(); let workspace_path = temp_dir.path().join("workspace"); // Should fail - not a git repo let result = FlockConfig::new_with_config( project_path.clone(), workspace_path.clone(), 2, Some(config_path.to_str().unwrap())); assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("must be a git repository")); // Initialize git repo Command::new("git") .arg("init") .current_dir(&project_path) .output() .expect("Failed to run git init"); // Should fail - no flock-requirements.md let result = FlockConfig::new_with_config( project_path.clone(), workspace_path.clone(), 2, Some(config_path.to_str().unwrap())); assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("flock-requirements.md")); // Create flock-requirements.md fs::write(project_path.join("flock-requirements.md"), "# Test\n") .expect("Failed to write requirements"); // Should succeed now let result = FlockConfig::new_with_config( project_path, workspace_path, 2, Some(config_path.to_str().unwrap())); assert!(result.is_ok()); } #[test] fn test_flock_config_builder() { let project_dir = create_test_project("builder-test"); let workspace_dir = TempDir::new().unwrap(); let config_path = create_test_config(&workspace_dir); let config = FlockConfig::new_with_config( project_dir.path().to_path_buf(), workspace_dir.path().to_path_buf(), 2, Some(config_path.to_str().unwrap()), ) .expect("Failed to create config") .with_max_turns(15) .with_g3_binary(PathBuf::from("/custom/g3")); assert_eq!(config.num_segments, 2); assert_eq!(config.max_turns, 15); assert_eq!(config.g3_binary, Some(PathBuf::from("/custom/g3"))); } #[test] fn test_workspace_creation() { let project_dir = create_test_project("workspace-test"); let workspace_dir = TempDir::new().unwrap(); let config_path = create_test_config(&workspace_dir); let config = FlockConfig::new_with_config( project_dir.path().to_path_buf(), workspace_dir.path().to_path_buf(), 2, Some(config_path.to_str().unwrap()), ) .expect("Failed to create config"); // Create FlockMode instance let _flock = FlockMode::new(config).expect("Failed to create FlockMode"); // Verify workspace directory structure will be created // (We can't run the full flock without LLM access, but we can test the setup) assert!(project_dir.path().join(".git").exists()); assert!(project_dir.path().join("flock-requirements.md").exists()); } #[test] fn test_git_clone_functionality() { let project_dir = create_test_project("clone-test"); let workspace_dir = TempDir::new().unwrap(); // Manually test git cloning (what flock mode does internally) let segment_dir = workspace_dir.path().join("segment-1"); let output = Command::new("git") .arg("clone") .arg(project_dir.path()) .arg(&segment_dir) .output() .expect("Failed to run git clone"); assert!(output.status.success(), "git clone failed: {:?}", output); // Verify the clone assert!(segment_dir.exists()); assert!(segment_dir.join(".git").exists()); assert!(segment_dir.join("flock-requirements.md").exists()); assert!(segment_dir.join("README.md").exists()); // Verify it's a proper git repo let output = Command::new("git") .args(["log", "--oneline"]) .current_dir(&segment_dir) .output() .expect("Failed to run git log"); assert!(output.status.success()); let log = String::from_utf8_lossy(&output.stdout); assert!(log.contains("Initial commit")); } #[test] fn test_multiple_segment_clones() { let project_dir = create_test_project("multi-clone-test"); let workspace_dir = TempDir::new().unwrap(); // Clone multiple segments for i in 1..=2 { let segment_dir = workspace_dir.path().join(format!("segment-{}", i)); let output = Command::new("git") .arg("clone") .arg(project_dir.path()) .arg(&segment_dir) .output() .expect("Failed to run git clone"); assert!(output.status.success(), "git clone {} failed", i); assert!(segment_dir.exists()); assert!(segment_dir.join(".git").exists()); assert!(segment_dir.join("flock-requirements.md").exists()); } // Verify both segments exist and are independent let segment1 = workspace_dir.path().join("segment-1"); let segment2 = workspace_dir.path().join("segment-2"); assert!(segment1.exists()); assert!(segment2.exists()); // Modify segment 1 fs::write(segment1.join("test.txt"), "segment 1").expect("Failed to write to segment 1"); // Verify segment 2 is unaffected assert!(!segment2.join("test.txt").exists()); } #[test] fn test_segment_requirements_creation() { let project_dir = create_test_project("segment-req-test"); let workspace_dir = TempDir::new().unwrap(); // Clone a segment let segment_dir = workspace_dir.path().join("segment-1"); Command::new("git") .arg("clone") .arg(project_dir.path()) .arg(&segment_dir) .output() .expect("Failed to clone"); // Create segment-requirements.md (what flock mode does) let segment_requirements = "# Module A\n\nImplement module A functionality\n"; fs::write( segment_dir.join("segment-requirements.md"), segment_requirements, ) .expect("Failed to write segment requirements"); // Verify it was created assert!(segment_dir.join("segment-requirements.md").exists()); let content = fs::read_to_string(segment_dir.join("segment-requirements.md")) .expect("Failed to read segment requirements"); assert!(content.contains("Module A")); } #[test] fn test_status_file_operations() { use g3_ensembles::FlockStatus; let temp_dir = TempDir::new().unwrap(); let status_file = temp_dir.path().join("flock-status.json"); // Create a status let status = FlockStatus::new( "test-session".to_string(), PathBuf::from("/test/project"), PathBuf::from("/test/workspace"), 2, ); // Save to file status .save_to_file(&status_file) .expect("Failed to save status"); // Verify file exists assert!(status_file.exists()); // Load from file let loaded = FlockStatus::load_from_file(&status_file).expect("Failed to load status"); assert_eq!(loaded.session_id, "test-session"); assert_eq!(loaded.num_segments, 2); } #[test] fn test_json_extraction() { // Test the JSON extraction logic used in partition_requirements let test_cases = vec![ ( "Here is the result: [{\"module_name\": \"test\"}]", Some("[{\"module_name\": \"test\"}]"), ), ( "```json\n[{\"module_name\": \"test\"}]\n```", Some("[{\"module_name\": \"test\"}]"), ), ( "Some text before\n[{\"a\": 1}, {\"b\": 2}]\nSome text after", Some("[{\"a\": 1}, {\"b\": 2}]"), ), ("No JSON here", None), ]; for (input, expected) in test_cases { let result = extract_json_array(input); match expected { Some(exp) => { assert!(result.is_some(), "Failed to extract from: {}", input); assert_eq!(result.unwrap(), exp); } None => { assert!(result.is_none(), "Should not extract from: {}", input); } } } } // Helper function to extract JSON array (mimics the logic in flock.rs) fn extract_json_array(output: &str) -> Option { if let Some(start) = output.find('[') { if let Some(end) = output.rfind(']') { if end > start { return Some(output[start..=end].to_string()); } } } None } #[test] fn test_partition_json_parsing() { // Test parsing of partition JSON let json = r#"[ { "module_name": "core-library", "requirements": "Build the core library with basic functionality", "dependencies": [] }, { "module_name": "cli-tool", "requirements": "Create a CLI tool that uses the core library", "dependencies": ["core-library"] } ]"#; let partitions: Vec = serde_json::from_str(json).expect("Failed to parse JSON"); assert_eq!(partitions.len(), 2); assert_eq!(partitions[0]["module_name"], "core-library"); assert_eq!(partitions[1]["module_name"], "cli-tool"); assert_eq!(partitions[1]["dependencies"][0], "core-library"); } #[test] fn test_requirements_file_content() { let project_dir = create_test_project("content-test"); let requirements_path = project_dir.path().join("flock-requirements.md"); let content = fs::read_to_string(&requirements_path).expect("Failed to read requirements"); // Verify content structure assert!(content.contains("# content-test Test Project")); assert!(content.contains("## Module A")); assert!(content.contains("## Module B")); assert!(content.contains("Hello from Module A")); assert!(content.contains("Hello from Module B")); } #[test] fn test_git_repo_independence() { let project_dir = create_test_project("independence-test"); let workspace_dir = TempDir::new().unwrap(); // Clone two segments let segment1 = workspace_dir.path().join("segment-1"); let segment2 = workspace_dir.path().join("segment-2"); Command::new("git") .arg("clone") .arg(project_dir.path()) .arg(&segment1) .output() .expect("Failed to clone segment 1"); Command::new("git") .arg("clone") .arg(project_dir.path()) .arg(&segment2) .output() .expect("Failed to clone segment 2"); // Make a commit in segment 1 fs::write(segment1.join("file1.txt"), "content 1").expect("Failed to write file1"); Command::new("git") .args(["add", "file1.txt"]) .current_dir(&segment1) .output() .expect("Failed to git add"); Command::new("git") .args(["commit", "-m", "Add file1"]) .current_dir(&segment1) .output() .expect("Failed to commit in segment 1"); // Make a different commit in segment 2 fs::write(segment2.join("file2.txt"), "content 2").expect("Failed to write file2"); Command::new("git") .args(["add", "file2.txt"]) .current_dir(&segment2) .output() .expect("Failed to git add"); Command::new("git") .args(["commit", "-m", "Add file2"]) .current_dir(&segment2) .output() .expect("Failed to commit in segment 2"); // Verify they have different commits let log1 = Command::new("git") .args(["log", "--oneline"]) .current_dir(&segment1) .output() .expect("Failed to get log 1"); let log2 = Command::new("git") .args(["log", "--oneline"]) .current_dir(&segment2) .output() .expect("Failed to get log 2"); let log1_str = String::from_utf8_lossy(&log1.stdout); let log2_str = String::from_utf8_lossy(&log2.stdout); assert!(log1_str.contains("Add file1")); assert!(!log1_str.contains("Add file2")); assert!(log2_str.contains("Add file2")); assert!(!log2_str.contains("Add file1")); // Verify files exist only in their respective segments assert!(segment1.join("file1.txt").exists()); assert!(!segment1.join("file2.txt").exists()); assert!(segment2.join("file2.txt").exists()); assert!(!segment2.join("file1.txt").exists()); }