loopflow 0.10.1

Run steps and flows with coding agents
Documentation
mod support;

use std::process::Command;

use loopflow::ops::{next_branch, NextOptions, NullProgress};
use loopflow_test_support::TestRepo;
use support::EnvGuard;

const MOCK_CLAUDE: &str = "#!/bin/sh\necho '{\"title\":\"test commit\",\"body\":\"\"}'\n";

fn write_gh_script(pr_number: Option<u64>, pr_state: Option<&str>) -> String {
    let number = pr_number.map(|n| n.to_string()).unwrap_or_default();
    let state = pr_state.unwrap_or("");
    // The mock checks the current branch: only return PR data when on a non-main branch.
    // This prevents enable_auto_merge from triggering after reset_to_main.
    format!(
        "#!/bin/sh\ncase \"$1 $2\" in\n  'pr list') echo '[]'; exit 0;;\n  'pr create') echo 'https://example.com/pr/1'; exit 0;;\n  'pr view')\n    branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)\n    if [ \"$branch\" = \"main\" ]; then\n      exit 1\n    fi\n    if echo \"$@\" | grep -q 'number'; then\n      echo '{number}'; exit 0;\n    fi\n    if echo \"$@\" | grep -q 'state'; then\n      echo '{state}'; exit 0;\n    fi\n    if echo \"$@\" | grep -q 'url'; then\n      echo 'https://example.com/pr/1'; exit 0;\n    fi\n    echo ''; exit 0;;\n  'pr edit') exit 0;;\n  'pr merge') exit 0;;\nesac\nexit 0\n"
    )
}

fn branch_exists(repo: &TestRepo, name: &str) -> bool {
    Command::new("git")
        .args(["show-ref", "--verify", &format!("refs/heads/{name}")])
        .current_dir(repo.path())
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

#[test]
fn next_creates_branch_from_current() {
    let gh_script = write_gh_script(None, None);
    let _env = EnvGuard::new(&[("gh", gh_script.as_str())]);
    let repo = TestRepo::new();
    repo.create_branch("feature");

    let result = next_branch(
        repo.path(),
        &NextOptions {
            create_pr: false,
            rebase: false,
            wave_name: None,
            agent: None,
        },
        &NullProgress,
    )
    .expect("next");

    assert!(branch_exists(&repo, &result.new_branch));
    assert!(branch_exists(&repo, "feature"));
}

#[test]
fn next_with_naming_schema() {
    let gh_script = write_gh_script(None, None);
    let _env = EnvGuard::new(&[("gh", gh_script.as_str()), ("claude", MOCK_CLAUDE)]);
    let repo = TestRepo::new();
    repo.create_file(
        ".lf/config.yaml",
        "branch_names:\n  schema: '{user}/{words}'\n",
    );
    repo.stage_all();
    repo.commit("add config");
    repo.create_branch("feature");

    let result = next_branch(
        repo.path(),
        &NextOptions {
            create_pr: false,
            rebase: false,
            wave_name: Some("wave".to_string()),
            agent: None,
        },
        &NullProgress,
    )
    .expect("next");

    assert!(result.new_branch.starts_with("jack/"));
}

#[test]
fn next_detects_merged_pr_starts_fresh() {
    let gh_script = write_gh_script(Some(12), Some("MERGED"));
    let _env = EnvGuard::new(&[("gh", gh_script.as_str()), ("claude", MOCK_CLAUDE)]);
    let repo = TestRepo::new();
    repo.create_branch("feature");
    repo.create_file("feature.txt", "feature");
    repo.stage_all();
    repo.commit("feature work");

    repo.checkout("main");
    let main_head = repo.head_sha();
    repo.checkout("feature");

    let result = next_branch(
        repo.path(),
        &NextOptions {
            create_pr: false,
            rebase: false,
            wave_name: None,
            agent: None,
        },
        &NullProgress,
    )
    .expect("next");

    assert!(branch_exists(&repo, &result.new_branch));
    assert_eq!(repo.head_sha(), main_head);
}

#[test]
fn next_appends_suffix_on_branch_name_collision() {
    let gh_script = write_gh_script(None, None);
    let _env = EnvGuard::new(&[("gh", gh_script.as_str()), ("claude", MOCK_CLAUDE)]);
    let repo = TestRepo::new();
    // Schema {name} produces "wave" — but that branch already exists, triggering collision
    repo.create_file(".lf/config.yaml", "branch_names:\n  schema: '{name}'\n");
    repo.stage_all();
    repo.commit("add config");
    repo.create_branch("wave");

    let result = next_branch(
        repo.path(),
        &NextOptions {
            create_pr: false,
            rebase: false,
            wave_name: Some("wave".to_string()),
            agent: None,
        },
        &NullProgress,
    )
    .expect("next should succeed despite name collision");

    assert!(result.new_branch.starts_with("wave."));
    assert!(result.new_branch.len() > "wave.".len());
    assert!(branch_exists(&repo, &result.new_branch));
    assert!(branch_exists(&repo, "wave"));
}