mod support;
use std::process::Command;
use loopflow::ops::{create_or_update_pr, current_pr, NullProgress, OpsError, PrOptions};
use loopflow_test_support::TestRepo;
use support::EnvGuard;
fn write_gh_script(pr_list: &str, pr_diff: Option<&str>) -> String {
let diff = pr_diff.unwrap_or("");
format!(
"#!/bin/sh\ncase \"$1 $2\" in\n 'pr list')\n cat <<'JSON'\n{pr_list}\nJSON\n exit 0;;\n 'pr diff') echo '{diff}'; exit 0;;\n 'pr create') echo 'https://example.com/pr/1'; exit 0;;\n 'pr edit') exit 0;;\n 'pr ready') exit 0;;\n 'pr view') echo 'OPEN'; exit 0;;\nesac\nexit 0\n"
)
}
fn noop_script() -> &'static str {
"#!/bin/sh\nexit 0\n"
}
fn claude_script() -> &'static str {
"#!/bin/sh\necho '{\"title\":\"generated title\",\"body\":\"generated body\"}'\nexit 0\n"
}
fn codex_script(output: &str) -> String {
format!("#!/bin/sh\ncat <<'EOF'\n{output}\nEOF\nexit 0\n")
}
fn write_gh_script_reject_base(expected_reject: &str) -> String {
format!(
"#!/bin/sh\ncase \"$1 $2\" in\n 'pr list')\n echo '[]'; exit 0;;\n 'pr diff') exit 1;;\n 'pr create')\n base=\"\"\n while [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = \"--base\" ]; then\n shift\n base=\"$1\"\n fi\n shift\n done\n if [ \"$base\" = \"{expected_reject}\" ]; then\n echo \"base branch matches head\" >&2\n exit 1\n fi\n echo 'https://example.com/pr/1'\n exit 0;;\n 'pr edit') exit 0;;\n 'pr ready') exit 0;;\n 'pr view') echo 'OPEN'; exit 0;;\nesac\nexit 0\n"
)
}
fn push_branch(repo: &TestRepo, name: &str) {
let _ = Command::new("git")
.args(["push", "-u", "origin", name])
.current_dir(repo.path())
.status();
}
#[test]
fn pr_create_calls_gh() {
let gh_script = write_gh_script("[]", None);
let home = tempfile::TempDir::new().expect("temp home");
let _env = EnvGuard::with_home(
&[
("gh", gh_script.as_str()),
("open", noop_script()),
("claude", claude_script()),
],
Some(home.path()),
);
let repo = TestRepo::new();
repo.create_branch("feature");
push_branch(&repo, "feature");
let result = create_or_update_pr(
repo.path(),
&PrOptions {
title: Some("test title".to_string()),
body: Some("test body".to_string()),
},
&NullProgress,
)
.expect("pr");
assert!(result.created);
assert_eq!(result.url, "https://example.com/pr/1");
}
#[test]
fn pr_update_refreshes_body() {
let gh_script = write_gh_script(
r#"[{"url":"https://example.com/pr/1","state":"OPEN","isDraft":false,"number":1}]"#,
Some("diff"),
);
let home = tempfile::TempDir::new().expect("temp home");
let _env = EnvGuard::with_home(
&[
("gh", gh_script.as_str()),
("open", noop_script()),
("claude", claude_script()),
],
Some(home.path()),
);
let repo = TestRepo::new();
repo.create_branch("feature");
push_branch(&repo, "feature");
let result = create_or_update_pr(
repo.path(),
&PrOptions {
title: Some("updated title".to_string()),
body: Some("updated body".to_string()),
},
&NullProgress,
)
.expect("pr");
assert!(result.updated);
assert!(!result.created);
}
#[test]
fn pr_create_uses_default_base_when_upstream_matches_head() {
let gh_script = write_gh_script_reject_base("feature");
let home = tempfile::TempDir::new().expect("temp home");
let _env = EnvGuard::with_home(
&[
("gh", gh_script.as_str()),
("open", noop_script()),
("claude", claude_script()),
],
Some(home.path()),
);
let repo = TestRepo::new();
repo.create_branch("feature");
push_branch(&repo, "feature");
let result = create_or_update_pr(
repo.path(),
&PrOptions {
title: Some("test title".to_string()),
body: Some("test body".to_string()),
},
&NullProgress,
)
.expect("pr");
assert!(result.created);
assert_eq!(result.url, "https://example.com/pr/1");
}
#[test]
fn current_pr_surfaces_gh_list_errors() {
let _env = EnvGuard::new(&[(
"gh",
"#!/bin/sh\nif [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ]; then\n echo \"gh pr list failed\" >&2\n exit 1\nfi\nexit 0\n",
)]);
let repo = TestRepo::new();
let result = current_pr(repo.path());
assert!(matches!(
result,
Err(OpsError::CommandFailed { stderr, .. }) if stderr.contains("gh pr list failed")
));
}
#[test]
fn pr_auto_generates_title_when_missing() {
let gh_script = write_gh_script("[]", None);
let home = tempfile::TempDir::new().expect("temp home");
let _env = EnvGuard::with_home(
&[
("gh", gh_script.as_str()),
("open", noop_script()),
("claude", claude_script()),
],
Some(home.path()),
);
let repo = TestRepo::new();
repo.create_branch("feature");
push_branch(&repo, "feature");
let result = create_or_update_pr(
repo.path(),
&PrOptions {
title: None,
body: Some("some body".to_string()),
},
&NullProgress,
);
let Ok(result) = result else {
panic!("expected auto-generated title to succeed");
};
assert!(result.created);
}
#[test]
fn pr_auto_generates_title_from_labeled_codex_output() {
let gh_script = write_gh_script("[]", None);
let codex_output = r#"Title: generated title
Body:
## Usage
- generated body"#;
let codex = codex_script(codex_output);
let home = tempfile::TempDir::new().expect("temp home");
std::fs::create_dir_all(home.path().join(".lf")).expect("config dir");
std::fs::write(home.path().join(".lf/config.yaml"), "agent: codex\n").expect("config");
let _env = EnvGuard::with_home(
&[
("gh", gh_script.as_str()),
("open", noop_script()),
("codex", codex.as_str()),
],
Some(home.path()),
);
let repo = TestRepo::new();
repo.create_branch("feature");
push_branch(&repo, "feature");
let result = create_or_update_pr(
repo.path(),
&PrOptions {
title: None,
body: None,
},
&NullProgress,
);
let Ok(result) = result else {
panic!("expected labeled codex output to succeed");
};
assert!(result.created);
}