use super::git::run_git;
use std::fs;
use std::path::PathBuf;
use tempfile::{TempDir, tempdir};
pub(crate) struct Fixture {
_dir: TempDir,
pub(crate) path: PathBuf,
pub(super) repo: PathBuf,
}
impl Fixture {
pub(crate) fn new() -> Self {
let dir = tempdir().unwrap();
let path = dir.path().to_path_buf();
let repo = path.join("repo.git");
fs::create_dir_all(&repo).unwrap();
run_git(&repo, &["init", "-q", "--bare", "-b", "config/default"]);
run_git(&repo, &["config", "user.email", "t@t.local"]);
run_git(&repo, &["config", "user.name", "Tester"]);
run_git(&repo, &["config", "commit.gpgsign", "false"]);
let fx = Self {
_dir: dir,
path,
repo,
};
let author = fx.path.join(".author");
let author_str = author.to_string_lossy().to_string();
run_git(
&fx.repo,
&[
"worktree",
"add",
"-q",
"--orphan",
"-b",
"config/default",
author_str.as_str(),
],
);
fs::write(author.join("version"), "1\n").unwrap();
run_git(&author, &["add", "version"]);
run_git(
&author,
&["commit", "-q", "-m", "config: init [config/default]"],
);
run_git(&fx.repo, &["worktree", "remove", author_str.as_str()]);
fx
}
pub(crate) fn commit_other(&self, file: &str, body: &str) {
let author = self.path.join(".amend");
let author_str = author.to_string_lossy().to_string();
run_git(
&self.repo,
&[
"worktree",
"add",
"-q",
author_str.as_str(),
"config/default",
],
);
fs::write(author.join(file), body).unwrap();
run_git(&author, &["add", file]);
run_git(&author, &["commit", "-q", "-m", &format!("add {file}")]);
run_git(&self.repo, &["worktree", "remove", author_str.as_str()]);
}
pub(crate) fn build_agent(&self, conv_id: &str, user_message: &str) {
let wt = self.path.join("agents").join(conv_id);
let wt_str = wt.to_string_lossy().to_string();
let branch = format!("agents/{conv_id}");
run_git(
&self.repo,
&[
"worktree",
"add",
"-q",
"-b",
branch.as_str(),
wt_str.as_str(),
"config/default",
],
);
fs::write(wt.join("goal.md"), user_message).unwrap();
fs::write(wt.join("soul.md"), "you are a tester\n").unwrap();
run_git(&wt, &["rm", "-q", "--ignore-unmatch", "version"]);
run_git(&wt, &["add", "goal.md", "soul.md"]);
run_git(
&wt,
&["commit", "-q", "-m", &format!("dispatch [{conv_id}]")],
);
let cmp_id = format!("{conv_id}-c");
let cmp_branch = format!("agents/{cmp_id}");
let cmp_wt = self.path.join("agents").join(&cmp_id);
let cmp_str = cmp_wt.to_string_lossy().to_string();
run_git(
&self.repo,
&[
"worktree",
"add",
"-q",
"-b",
cmp_branch.as_str(),
cmp_str.as_str(),
branch.as_str(),
],
);
fs::create_dir_all(cmp_wt.join("summary")).unwrap();
fs::write(
cmp_wt.join("summary/001.md"),
format!("conversation {conv_id}: pong\n"),
)
.unwrap();
run_git(&cmp_wt, &["add", "summary/001.md"]);
run_git(
&cmp_wt,
&[
"commit",
"-q",
"-m",
&format!("compaction: terminal summary [{conv_id}]"),
],
);
run_git(&wt, &["merge", "--no-ff", "-q", "--no-edit", &cmp_branch]);
run_git(&self.repo, &["worktree", "remove", cmp_str.as_str()]);
run_git(&self.repo, &["branch", "-q", "-D", &cmp_branch]);
self.write_step_record(conv_id, user_message);
}
pub(crate) fn write_step_record(&self, conv_id: &str, user_message: &str) {
let step_dir = self.path.join("steps").join(conv_id).join("001");
fs::create_dir_all(&step_dir).unwrap();
let request_json = serde_json::json!({
"model": "m",
"messages": [{"role": "user", "content": user_message}],
});
fs::write(
step_dir.join("request.json"),
serde_json::to_vec_pretty(&request_json).unwrap(),
)
.unwrap();
}
pub(super) fn write_tool_call(
&self,
conv_id: &str,
seq: u32,
tool_id: &str,
output: Option<&[u8]>,
) {
let tool_dir = self
.path
.join(format!("steps/{conv_id}/{seq:03}/tools/{tool_id}"));
fs::create_dir_all(&tool_dir).unwrap();
fs::write(tool_dir.join("input.json"), b"{}").unwrap();
if let Some(out) = output {
fs::write(tool_dir.join("output.json"), out).unwrap();
}
}
pub(crate) fn deposit_message(&self, agent_id: &str, filename: &str, body: &str) {
let inbox = self.path.join("inbox").join(agent_id);
fs::create_dir_all(&inbox).unwrap();
fs::write(inbox.join(filename), body).unwrap();
}
pub(super) fn mark_ref(&self, refname: &str) {
run_git(&self.repo, &["update-ref", refname, "config/default"]);
}
pub(super) fn build_child(&self, parent_id: &str, child_id: &str) {
let wt = self.path.join("agents").join(child_id);
let wt_str = wt.to_string_lossy().to_string();
let branch = format!("agents/{child_id}");
run_git(
&self.repo,
&[
"worktree",
"add",
"-q",
"-b",
branch.as_str(),
wt_str.as_str(),
&format!("agents/{parent_id}"),
],
);
fs::write(wt.join("child.md"), "child work\n").unwrap();
run_git(&wt, &["add", "child.md"]);
run_git(
&wt,
&["commit", "-q", "-m", &format!("dispatch [{child_id}]")],
);
run_git(&self.repo, &["worktree", "remove", wt_str.as_str()]);
}
pub(super) fn write_response_events(&self, conv_id: &str, seq: u32, events: &[&str]) {
let step_dir = self
.path
.join("steps")
.join(conv_id)
.join(format!("{seq:03}"));
fs::create_dir_all(&step_dir).unwrap();
let mut payload = events.join("\n");
if !events.is_empty() {
payload.push('\n');
}
fs::write(step_dir.join("response.json"), payload).unwrap();
}
}