use devflow_core::agents;
use devflow_core::state::State;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
pub(crate) static ENV_MUTEX: Mutex<()> = Mutex::new(());
pub(crate) fn init_repo(root: &Path) {
let git = |args: &[&str]| {
let ok = std::process::Command::new("git")
.args(args)
.current_dir(root)
.output()
.unwrap()
.status
.success();
assert!(ok, "git {args:?} failed");
};
git(&["init", "-q"]);
git(&["config", "user.email", "devflow@example.com"]);
git(&["config", "user.name", "DevFlow Tests"]);
git(&["config", "commit.gpgsign", "false"]);
git(&["config", "tag.gpgsign", "false"]);
git(&["config", "core.hooksPath", "/dev/null"]);
std::fs::write(root.join("Cargo.toml"), "[package]\nversion = \"2.0.0\"\n").unwrap();
git(&["add", "."]);
git(&["commit", "-q", "-m", "init"]);
git(&["branch", "-M", "main"]);
git(&["checkout", "-q", "-b", "develop"]);
}
pub(crate) fn init_repo_no_version_file(root: &Path) {
let git = |args: &[&str]| {
let ok = std::process::Command::new("git")
.args(args)
.current_dir(root)
.output()
.unwrap()
.status
.success();
assert!(ok, "git {args:?} failed");
};
git(&["init", "-q"]);
git(&["config", "user.email", "devflow@example.com"]);
git(&["config", "user.name", "DevFlow Tests"]);
git(&["config", "commit.gpgsign", "false"]);
git(&["config", "tag.gpgsign", "false"]);
git(&["config", "core.hooksPath", "/dev/null"]);
std::fs::write(root.join("README.md"), "no version file in this repo\n").unwrap();
git(&["add", "."]);
git(&["commit", "-q", "-m", "init"]);
git(&["branch", "-M", "main"]);
git(&["checkout", "-q", "-b", "develop"]);
}
pub(crate) struct AlwaysFailAdapter;
impl agents::AgentAdapter for AlwaysFailAdapter {
fn name(&self) -> &'static str {
"test-always-fail"
}
fn exec_command(
&self,
_phase: u32,
_prompt: &str,
_roots: &[PathBuf],
) -> (&'static str, Vec<String>) {
("true", Vec::new())
}
fn completion_signal_detected(&self, _output: &str) -> bool {
false
}
fn preflight(&self, _state: &State) -> Result<(), String> {
Err("test adapter always rejects".to_string())
}
}
pub(crate) struct FailOnceAdapter {
failed_once: std::cell::Cell<bool>,
}
impl FailOnceAdapter {
pub(crate) fn new() -> Self {
Self {
failed_once: std::cell::Cell::new(false),
}
}
}
impl agents::AgentAdapter for FailOnceAdapter {
fn name(&self) -> &'static str {
"test-fail-once"
}
fn exec_command(
&self,
_phase: u32,
_prompt: &str,
_roots: &[PathBuf],
) -> (&'static str, Vec<String>) {
("true", Vec::new())
}
fn completion_signal_detected(&self, _output: &str) -> bool {
false
}
fn preflight(&self, _state: &State) -> Result<(), String> {
if self.failed_once.get() {
Ok(())
} else {
self.failed_once.set(true);
Err("test adapter fails on the first preflight call only".to_string())
}
}
}
pub(crate) fn agent_free_git_only_path_dir() -> tempfile::TempDir {
let real_git = std::env::var_os("PATH")
.and_then(|paths| {
std::env::split_paths(&paths).find_map(|dir| {
let candidate = dir.join("git");
candidate.is_file().then_some(candidate)
})
})
.expect("git must be resolvable on PATH to run this test");
let dir = tempfile::tempdir().unwrap();
std::os::unix::fs::symlink(&real_git, dir.path().join("git")).unwrap();
dir
}
pub(crate) fn agent_free_dir_with_agent_stub(program: &str) -> tempfile::TempDir {
use std::os::unix::fs::PermissionsExt;
let dir = agent_free_git_only_path_dir();
let real_sh = std::env::var_os("PATH")
.and_then(|paths| {
std::env::split_paths(&paths).find_map(|d| {
let candidate = d.join("sh");
candidate.is_file().then_some(candidate)
})
})
.expect("sh must be resolvable on PATH to run this test");
std::os::unix::fs::symlink(&real_sh, dir.path().join("sh")).unwrap();
let path = dir.path().join(program);
std::fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap();
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&path, perms).unwrap();
dir
}
pub(crate) fn stub_agent_binary(name: &str) -> tempfile::TempDir {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(name);
std::fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap();
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&path, perms).unwrap();
dir
}
pub(crate) fn prepend_path(
stub_dir: &tempfile::TempDir,
original: &Option<std::ffi::OsString>,
) -> std::ffi::OsString {
let mut dirs = vec![stub_dir.path().to_path_buf()];
if let Some(original) = original {
dirs.extend(std::env::split_paths(original));
}
std::env::join_paths(dirs).unwrap()
}
pub(crate) fn stage_launched_count(root: &Path, phase: u32) -> usize {
std::fs::read_to_string(devflow_core::events::events_path(root))
.unwrap_or_default()
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.filter(|event| {
event.get("phase").and_then(serde_json::Value::as_u64) == Some(u64::from(phase))
&& event.get("event").and_then(serde_json::Value::as_str) == Some("stage_launched")
})
.count()
}