cascade-cli 0.1.152

Stacked diffs CLI for Bitbucket Server
Documentation
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;

/// Test complete stack workflow from creation to PR submission
#[tokio::test]
async fn test_complete_stack_workflow() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Build the binary first
    let build_output = Command::new("cargo")
        .args(["build", "--release"])
        .output()
        .expect("Failed to build binary");

    if !build_output.status.success() {
        panic!(
            "Failed to build binary: {}",
            String::from_utf8_lossy(&build_output.stderr)
        );
    }

    // Test stack creation using the built binary
    let binary_path = super::test_helpers::get_binary_path();
    let output = Command::new(&binary_path)
        .args(["stacks", "create", "test-feature"])
        .current_dir(&repo_path)
        .env("RUST_LOG", "info")
        .output()
        .expect("Failed to create stack");

    if !output.status.success() {
        eprintln!(
            "Command failed. Stderr: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
    }
    assert!(output.status.success());

    // Make test commits
    create_test_commits(&repo_path, 3).await;

    // Test stack push
    let output = Command::new(&binary_path)
        .args(["push", "--yes"])
        .current_dir(&repo_path)
        .env("RUST_LOG", "info")
        .output()
        .expect("Failed to push stack");

    if !output.status.success() {
        eprintln!(
            "Push failed. Stderr: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
    }

    // Test stack list
    let output = Command::new(&binary_path)
        .args(["stacks", "list"])
        .current_dir(&repo_path)
        .env("RUST_LOG", "info")
        .output()
        .expect("Failed to list stacks");

    if !output.status.success() {
        eprintln!(
            "List failed. Stderr: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
    } else {
        println!("List output: {}", String::from_utf8_lossy(&output.stdout));
    }

    assert!(output.status.success());
    assert!(String::from_utf8_lossy(&output.stdout).contains("test-feature"));
}

/// Test rebase scenarios with conflict resolution
#[tokio::test]
async fn test_rebase_conflict_scenarios() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Create conflicting changes on main
    create_conflicting_base_changes(&repo_path).await;

    // Create stack with conflicting commits
    create_conflicting_stack(&repo_path).await;

    // Test automatic conflict resolution
    let binary_path = super::test_helpers::get_binary_path();
    let output = Command::new(&binary_path)
        .args(["rebase"])
        .current_dir(&repo_path)
        .env("RUST_LOG", "info")
        .output()
        .expect("Failed to rebase with conflicts");

    if !output.status.success() {
        eprintln!(
            "Rebase failed. Stderr: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
        // Rebase may fail due to conflicts - that's expected behavior
        println!("Rebase failed as expected (conflict scenarios are complex)");
        return; // Don't fail the test
    }
}

/// Test CLI error handling and recovery scenarios
#[tokio::test]
async fn test_error_recovery_scenarios() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Test with invalid stack name (empty string)
    let binary_path = super::test_helpers::get_binary_path();
    let output = Command::new(&binary_path)
        .args(["stacks", "create", ""])
        .current_dir(&repo_path)
        .env("RUST_LOG", "info")
        .output()
        .expect("Command should run but fail gracefully");

    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);

    // Test should either fail gracefully or handle empty input appropriately
    if !output.status.success() {
        // If it fails, should contain meaningful error message
        assert!(
            stderr.contains("error")
                || stderr.contains("invalid")
                || stdout.contains("error")
                || stdout.contains("invalid")
                || stderr.contains("name")
                || stdout.contains("name"),
            "Should contain error about invalid input. Stderr: {stderr}, Stdout: {stdout}"
        );
    } else {
        // If it succeeds, it should handle empty name gracefully
        println!("CLI handled empty stack name gracefully");
    }
}

async fn create_test_git_repo() -> (TempDir, PathBuf) {
    let temp_dir = TempDir::new().unwrap();
    let repo_path = temp_dir.path().to_path_buf();

    // Initialize git repository with proper setup
    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    Command::new("git")
        .args(["config", "user.name", "Test"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    Command::new("git")
        .args(["config", "user.email", "test@test.com"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Create initial commit
    std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "Initial"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Initialize Cascade
    cascade_cli::config::initialize_repo(
        &repo_path,
        Some("https://test.bitbucket.com".to_string()),
    )
    .unwrap();

    (temp_dir, repo_path)
}

async fn create_test_commits(repo_path: &PathBuf, count: u32) {
    for i in 1..=count {
        std::fs::write(
            repo_path.join(format!("file{i}.txt")),
            format!("Content {i}"),
        )
        .unwrap();
        Command::new("git")
            .args(["add", "."])
            .current_dir(repo_path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["commit", "-m", &format!("Add file {i}")])
            .current_dir(repo_path)
            .output()
            .unwrap();
    }
}

async fn create_conflicting_base_changes(repo_path: &PathBuf) {
    std::fs::write(repo_path.join("shared.txt"), "Base content").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo_path)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "Add shared file"])
        .current_dir(repo_path)
        .output()
        .unwrap();
}

async fn create_conflicting_stack(repo_path: &PathBuf) {
    // Create stack that modifies the same file
    let binary_path = super::test_helpers::get_binary_path();
    Command::new(&binary_path)
        .args(["stacks", "create", "conflict-test"])
        .current_dir(repo_path)
        .output()
        .unwrap();

    std::fs::write(repo_path.join("shared.txt"), "Stack content").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo_path)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "Modify shared file"])
        .current_dir(repo_path)
        .output()
        .unwrap();
}