use std::{
fs,
process::{Command, Output},
str,
};
use serde_json::Value;
use tempfile::TempDir;
#[path = "multi_agent_worktrees/agent_registry.rs"]
mod agent_registry;
#[cfg(target_os = "linux")]
#[path = "multi_agent_worktrees/daemon_lifecycle.rs"]
mod daemon_lifecycle;
#[path = "multi_agent_worktrees/e2e.rs"]
mod e2e;
#[path = "multi_agent_worktrees/materialized_threads_e2e.rs"]
mod materialized_threads_e2e;
#[path = "multi_agent_worktrees/objectstore_pointer.rs"]
mod objectstore_pointer;
#[path = "multi_agent_worktrees/thread_create.rs"]
mod thread_create;
#[cfg(target_os = "linux")]
#[path = "multi_agent_worktrees/virtualized_mount.rs"]
mod virtualized_mount;
#[path = "multi_agent_worktrees/worktree_add.rs"]
mod worktree_add;
fn translate_legacy_args(args: &[&str]) -> Vec<String> {
let mut prefix = Vec::new();
let mut i = 0;
while i < args.len() && args[i].starts_with("--") {
prefix.push(args[i].to_string());
i += 1;
}
let rest = &args[i..];
let translated = match rest {
["thread", "delete", name] => vec![
"thread".into(),
"drop".into(),
(*name).into(),
"--delete-thread".into(),
],
_ => rest.iter().map(|arg| (*arg).to_string()).collect(),
};
prefix.extend(translated);
prefix
}
fn heddle(args: &[&str], cwd: Option<&std::path::Path>) -> Result<String, String> {
let output = heddle_output(args, cwd)?;
let stdout = str::from_utf8(&output.stdout).unwrap_or("").to_string();
let stderr = str::from_utf8(&output.stderr).unwrap_or("").to_string();
if output.status.success() {
Ok(stdout)
} else {
Err(format!(
"Exit code: {:?}\nstdout: {}\nstderr: {}",
output.status.code(),
stdout,
stderr
))
}
}
fn heddle_output(args: &[&str], cwd: Option<&std::path::Path>) -> Result<Output, String> {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_heddle"));
cmd.args(translate_legacy_args(args));
cmd.env("HEDDLE_PRINCIPAL_NAME", "Heddle Test")
.env("HEDDLE_PRINCIPAL_EMAIL", "test@heddle.dev");
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
cmd.output().map_err(|e| e.to_string())
}
fn heddle_argv_json<I, S>(args: I) -> Value
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
serde_json::json!(
std::iter::once(env!("CARGO_BIN_EXE_heddle").to_string())
.chain(args.into_iter().map(|arg| arg.as_ref().to_string()))
.collect::<Vec<_>>()
)
}
fn canonical_path_string(path: &std::path::Path) -> String {
path.canonicalize()
.unwrap_or_else(|_| path.to_path_buf())
.display()
.to_string()
}
fn heddle_output_with_env(
args: &[&str],
cwd: Option<&std::path::Path>,
envs: &[(&str, &str)],
) -> Result<Output, String> {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_heddle"));
cmd.args(translate_legacy_args(args));
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
for (key, value) in envs {
cmd.env(key, value);
}
cmd.output().map_err(|e| e.to_string())
}
pub struct RepoFixture {
inner: Option<TempDir>,
}
impl RepoFixture {
fn from_temp(temp: TempDir) -> Self {
Self { inner: Some(temp) }
}
}
impl std::ops::Deref for RepoFixture {
type Target = TempDir;
fn deref(&self) -> &TempDir {
self.inner
.as_ref()
.expect("RepoFixture inner TempDir taken before Drop")
}
}
impl Drop for RepoFixture {
fn drop(&mut self) {
if let Some(temp) = self.inner.take() {
let _ = heddle(&["daemon", "stop"], Some(temp.path()));
}
}
}
fn setup_repo(filename: &str, content: &str) -> RepoFixture {
let temp = TempDir::new().unwrap();
heddle(&["init"], Some(temp.path())).unwrap();
fs::write(temp.path().join(filename), content).unwrap();
heddle(&["capture", "-m", "init"], Some(temp.path())).unwrap();
RepoFixture::from_temp(temp)
}
fn head_track(path: &std::path::Path) -> String {
let out = heddle(&["--output", "json", "status"], Some(path)).unwrap();
let v: Value = serde_json::from_str(&out).unwrap();
v["thread"].as_str().unwrap_or("").to_string()
}
pub(crate) fn inject_post_verification_at(cwd: &std::path::Path, mut value: Value) -> Value {
let obj = match value.as_object_mut() {
Some(obj) => obj,
None => return value,
};
if obj.contains_key("verification") {
return value;
}
let verify_out = match heddle_output(&["--output", "json", "verify"], Some(cwd)) {
Ok(out) => out,
Err(_) => return value,
};
let stream = if !verify_out.status.success() {
verify_out.stderr
} else {
verify_out.stdout
};
let text = std::str::from_utf8(&stream).unwrap_or("");
let parsed: Value = match serde_json::from_str(text) {
Ok(v) => v,
Err(_) => return value,
};
let verification = if parsed.get("kind") == Some(&Value::String("verify_failed".to_string())) {
parsed.get("verification").cloned().unwrap_or(Value::Null)
} else {
let mut obj_map = parsed.as_object().cloned().unwrap_or_default();
obj_map.remove("output_kind");
obj_map.remove("repository_label");
obj_map.remove("repository_context");
obj_map.remove("clean");
Value::Object(obj_map)
};
obj.insert("verification".to_string(), verification);
value
}