use rusqlite::Connection;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};
use tempfile::TempDir;
use crate::common::aid_cmd_with_cwd;
pub(crate) const AGENT: &str = "settle";
pub(crate) const TERMINAL: [&str; 5] = ["done", "merged", "failed", "skipped", "stopped"];
const RELEASE_WAIT: Duration = Duration::from_secs(60);
pub(crate) struct Harness {
home: TempDir,
project: TempDir,
_scripts: TempDir,
gate: PathBuf,
verifier: PathBuf,
}
impl Harness {
pub(crate) fn new() -> Self {
let home = TempDir::new().unwrap();
let project = TempDir::new().unwrap();
let scripts = TempDir::new().unwrap();
let gate = scripts.path().join("gate");
let agent = write_script(scripts.path(), "settle-agent", AGENT_SCRIPT);
let verifier = write_script(
scripts.path(),
"verifier",
&format!(
"#!/bin/sh\ng='{}'\ntouch \"$g.waiting\"\ni=0\n\
while [ ! -f \"$g\" ] && [ $i -lt 600 ]; do sleep 0.2; i=$((i+1)); done\n\
[ -f \"$g\" ] || exit 2\ncode=$(cat \"$g\")\nrm -f \"$g\"\nexit \"$code\"\n",
gate.display()
),
);
write_custom_agent(home.path(), &agent);
Self { home, project, _scripts: scripts, gate, verifier }
}
pub(crate) fn with_git_project() -> Self {
let harness = Self::new();
let dir = harness.project();
std::fs::write(dir.join("README.md"), "settlement\n").unwrap();
for args in [
&["init", "-q", "-b", "main"][..],
&["add", "README.md"],
&["-c", "user.name=t", "-c", "user.email=t@example.com", "commit", "-qm", "init"],
] {
assert!(Command::new("git").args(args).current_dir(dir).status().unwrap().success());
}
harness
}
pub(crate) fn home(&self) -> &Path {
self.home.path()
}
pub(crate) fn project(&self) -> &Path {
self.project.path()
}
pub(crate) fn verifier(&self) -> &Path {
&self.verifier
}
pub(crate) fn aid(&self) -> Command {
aid_cmd_with_cwd(self.home(), self.project())
}
pub(crate) fn run(&self, id: &str, extra: &[&str]) -> Command {
let mut cmd = self.aid();
cmd.args(["run", AGENT, "settle the task", "--id", id])
.arg("--dir")
.arg(self.project())
.arg("--verify")
.arg(&self.verifier)
.args(extra);
cmd
}
pub(crate) fn release(&self, code: i32) {
self.wait_verifier_holding();
let _ = std::fs::remove_file(self.marker());
let tmp = self.gate.with_extension("tmp");
std::fs::write(&tmp, code.to_string()).unwrap();
std::fs::rename(&tmp, &self.gate).unwrap();
}
pub(crate) fn wait_verifier_holding(&self) {
let deadline = Instant::now() + RELEASE_WAIT;
while !self.marker().exists() {
assert!(Instant::now() < deadline, "verifier never started");
thread::sleep(Duration::from_millis(50));
}
}
fn marker(&self) -> PathBuf {
self.gate.with_extension("waiting")
}
pub(crate) fn status(&self, id: &str) -> Option<String> {
self.query("SELECT status FROM tasks WHERE id = ?1", id)
}
pub(crate) fn child_of(&self, parent: &str) -> Option<String> {
self.query("SELECT id FROM tasks WHERE parent_task_id = ?1", parent)
}
fn query(&self, sql: &str, arg: &str) -> Option<String> {
let conn = Connection::open(self.home().join("aid.db")).ok()?;
conn.query_row(sql, [arg], |row| row.get::<_, String>(0)).ok()
}
pub(crate) fn wait_for<T>(&self, timeout: Duration, probe: impl Fn(&Self) -> Option<T>) -> T {
let deadline = Instant::now() + timeout;
loop {
if let Some(value) = probe(self) {
return value;
}
assert!(Instant::now() < deadline, "condition not reached within {timeout:?}");
thread::sleep(Duration::from_millis(50));
}
}
pub(crate) fn wait_terminal(&self, id: &str) -> String {
self.wait_for(RELEASE_WAIT, |h| h.status(id).filter(|s| TERMINAL.contains(&s.as_str())))
}
}
impl Drop for Harness {
fn drop(&mut self) {
let _ = std::fs::write(&self.gate, "1");
}
}
const AGENT_SCRIPT: &str = "#!/bin/sh\n\
printf 'settled\\n' > settled.txt\n\
if git rev-parse --git-dir >/dev/null 2>&1; then\n\
git add settled.txt && git -c user.name=t -c user.email=t@example.com commit -qm settle\n\
fi\n\
printf '%s\\n' '{\"type\":\"completion\",\"finalText\":\"done\"}'\n";
fn write_custom_agent(aid_home: &Path, script: &Path) {
let agents_dir = aid_home.join("agents");
std::fs::create_dir_all(&agents_dir).unwrap();
std::fs::write(
agents_dir.join(format!("{AGENT}.toml")),
format!(
"[agent]\nid = \"{AGENT}\"\ndisplay_name = \"{AGENT}\"\ncommand = \"{}\"\n\
trust_tier = \"local\"\nstreaming = true\noutput_format = \"jsonl\"\n",
script.display(),
),
)
.unwrap();
}
fn write_script(dir: &Path, name: &str, contents: &str) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, contents).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}