444 lines
14 KiB
Rust
444 lines
14 KiB
Rust
//! 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;
|
|
|
|
/// 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 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(project_path.clone(), workspace_path.clone(), 2);
|
|
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(project_path.clone(), workspace_path.clone(), 2);
|
|
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(project_path, workspace_path, 2);
|
|
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 = FlockConfig::new(
|
|
project_dir.path().to_path_buf(),
|
|
workspace_dir.path().to_path_buf(),
|
|
2,
|
|
)
|
|
.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 = FlockConfig::new(
|
|
project_dir.path().to_path_buf(),
|
|
workspace_dir.path().to_path_buf(),
|
|
2,
|
|
)
|
|
.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<String> {
|
|
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::Value> =
|
|
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());
|
|
}
|