loopflow 0.12.32

Run steps and flows with coding agents
Documentation
mod support;

use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};

use loopflow_test_support::TestRepo;
use serde_json::Value;
use support::{register_task, EnvGuard};

fn read(repo: &Path, home: &Path, args: &[&str], input: Option<&str>) -> Value {
    let mut child = Command::new(env!("CARGO_BIN_EXE_lf"))
        .env_clear()
        .env("HOME", home)
        .env("LF_HOME", home)
        .env("PATH", "/usr/bin:/bin")
        .current_dir(repo)
        .args(args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    if let Some(input) = input {
        child
            .stdin
            .take()
            .unwrap()
            .write_all(input.as_bytes())
            .unwrap();
    }
    let output = child.wait_with_output().unwrap();
    assert!(
        output.status.success(),
        "{args:?}: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    serde_json::from_slice(&output.stdout).unwrap()
}

#[test]
fn diff_files_preserves_comparison_identity_and_draft_editing() {
    let home = tempfile::tempdir().unwrap();
    let _env = EnvGuard::with_lf_home(&[], home.path());
    let repo = TestRepo::new();
    repo.create_file("original.txt", "original content\n");
    repo.create_file("edit.txt", "before\n");
    repo.stage_all();
    repo.commit("Comparison base");
    let base = repo.head_sha();
    repo.create_branch("file-inspection");
    let task = register_task(home.path(), repo.path(), "file-inspection", &base);
    fs::rename(
        repo.path().join("original.txt"),
        repo.path().join("renamed.txt"),
    )
    .unwrap();
    repo.stage_all();
    repo.commit("Rename file");
    repo.create_file("edit.txt", "staged\n");
    repo.stage_all();
    repo.create_file("edit.txt", "working\n");
    repo.create_file("untracked.txt", "untracked\n");
    let query = |args: &[&str], input| read(repo.path(), home.path(), args, input);

    let files = query(&["task", "diff", "INF-123", "--files", "--json"], None);
    assert_eq!(files["task_id"], task.task.id.as_str());
    assert_eq!(files["base_commit"], base);
    assert_eq!(files["head_commit"], repo.head_sha());
    let paths = files["files"].as_array().unwrap();
    let renamed = paths
        .iter()
        .find(|file| file["path"] == "renamed.txt")
        .unwrap();
    assert_eq!(renamed["old_path"], "original.txt");
    assert_eq!(renamed["committed"], true);
    let edited = paths
        .iter()
        .find(|file| file["path"] == "edit.txt")
        .unwrap();
    assert_eq!(edited["staged"], true);
    assert_eq!(edited["unstaged"], true);
    assert!(paths
        .iter()
        .any(|file| file["path"] == "untracked.txt" && file["untracked"] == true));
    assert_eq!(
        files,
        query(
            &["diff", "INF-123", "--files", "--base", &base, "--json"],
            None
        )
    );
    let head = query(
        &["diff", "INF-123", "--files", "--base", "head", "--json"],
        None,
    );
    assert_eq!(head["base_commit"], repo.head_sha());
    assert!(!head["files"]
        .as_array()
        .unwrap()
        .iter()
        .any(|file| file["path"] == "renamed.txt"));

    let patch = query(
        &["diff", "INF-123", "renamed.txt", "--base", &base, "--json"],
        None,
    );
    assert_eq!(patch["base_commit"], base);
    assert!(patch["patch"]
        .as_str()
        .unwrap()
        .contains("rename from original.txt"));
    let file = query(&["file", "INF-123", "edit.txt", "--json"], None);
    let draft = query(
        &["diff", "INF-123", "edit.txt", "--draft", "--json"],
        Some("draft\n"),
    );
    assert_eq!(draft["base_commit"], base);
    assert!(draft["patch"].as_str().unwrap().contains("+draft"));
    assert_eq!(
        fs::read_to_string(repo.path().join("edit.txt")).unwrap(),
        "working\n"
    );
    assert_eq!(
        file,
        query(&["file", "INF-123", "edit.txt", "--json"], None)
    );
    let saved = query(
        &[
            "save",
            "INF-123",
            "edit.txt",
            "--revision",
            file["revision"].as_str().unwrap(),
            "--json",
        ],
        Some("draft\n"),
    );
    assert_eq!(saved["published"], true);
    assert_eq!(saved["file"]["content"], "draft\n");
    assert_ne!(saved["file"]["revision"], file["revision"]);
}