#![allow(dead_code)]
use std::fs;
use std::path::Path;
use std::process::{Child, Command, ExitStatus, Stdio};
use super::poll;
pub fn lernie_bin() -> std::path::PathBuf {
crate::test_support::lernie_binary()
}
const INHERITED_GIT_ENV: &[&str] = &[
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_PREFIX",
"GIT_COMMON_DIR",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
];
pub fn git_command(dest: &Path, args: &[&str]) -> Command {
let mut cmd = Command::new("git");
for var in INHERITED_GIT_ENV {
cmd.env_remove(var);
}
cmd.arg("-C").arg(dest).args(args);
cmd
}
pub fn git_run(dest: &Path, args: &[&str]) {
let out = git_command(dest, args).output().expect("spawn git");
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
}
pub fn write_global_models(harness: &Path) {
fs::write(harness.join("models.yaml"), "# no adapter override\n").unwrap();
}
pub fn write_brazen_config(dir: &Path, endpoint: &str) -> std::path::PathBuf {
let toml = format!(
"timeout = 60\n\
[[provider]]\nname = \"test\"\nbase_url = \"{endpoint}\"\n\
protocol = \"anthropic_messages\"\nauth = \"none\"\n\
body_defaults = {{ max_tokens = 64 }}\n"
);
let path = dir.join("brazen.toml");
fs::write(&path, toml).unwrap();
path
}
pub fn repo_git(dest: &Path) -> std::path::PathBuf {
dest.join("repo.git")
}
pub fn amend_config(dest: &Path, files: &[(&str, &str)]) {
let owned: Vec<(String, String)> = files
.iter()
.map(|(r, c)| (r.to_string(), c.to_string()))
.collect();
crate::template::authoring::author(
dest,
&dest.join(".no-pools"),
"default",
crate::template::authoring::Origin::Advance,
move |dir| {
for (rel, content) in &owned {
let path = dir.join(rel);
fs::create_dir_all(path.parent().unwrap())?;
fs::write(path, content)?;
}
Ok(())
},
&crate::template::RealGit::new(),
)
.unwrap();
}
pub fn scaffold_repo(dest: &Path, harness: &Path) {
let out = Command::new(lernie_bin())
.arg("new")
.arg(dest)
.env("LERNIE_HOME", harness)
.output()
.expect("spawn lernie new");
assert!(
out.status.success(),
"lernie new: {}",
String::from_utf8_lossy(&out.stderr)
);
amend_config(
dest,
&[(
"providers.yaml",
"\
roles:
worker:
provider: test
model: claude-sonnet-5
compactor:
provider: test
model: claude-haiku-4-5
",
)],
);
}
pub fn poll_for_conv_branch_with_diag(dest: &Path, prompt_child: &mut Child) -> String {
let repo = repo_git(dest);
let found = poll::until(dest, || {
if let Some(name) = scan_conv_branches(&repo) {
return Some(name);
}
if let Ok(Some(status)) = prompt_child.try_wait() {
let mut buf = Vec::new();
if let Some(mut e) = prompt_child.stderr.take() {
use std::io::Read as _;
let _ = e.read_to_end(&mut buf);
}
panic!(
"lernie prompt exited early: {status:?}; stderr: {}",
String::from_utf8_lossy(&buf)
);
}
None
});
if let Some(name) = found {
return name;
}
let buf = drain_stderr(prompt_child);
let branches = git_command(&repo, &["for-each-ref"])
.output()
.expect("spawn git");
let _ = prompt_child.kill();
let _ = prompt_child.wait();
panic!(
"no agents/* branch under {}, and {} went untouched for {:?}; refs: {:?}; stderr: {}",
repo.display(),
dest.display(),
poll::patience(),
String::from_utf8_lossy(&branches.stdout),
String::from_utf8_lossy(&buf)
);
}
fn drain_stderr(child: &mut Child) -> Vec<u8> {
use std::io::Read as _;
use std::os::fd::AsRawFd;
let Some(stderr) = child.stderr.as_mut() else {
return Vec::new();
};
let fd = stderr.as_raw_fd();
let prev_flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
unsafe { libc::fcntl(fd, libc::F_SETFL, prev_flags | libc::O_NONBLOCK) };
let mut buf = Vec::new();
let _ = stderr.read_to_end(&mut buf);
unsafe { libc::fcntl(fd, libc::F_SETFL, prev_flags) };
buf
}
fn scan_conv_branches(repo: &Path) -> Option<String> {
let out = git_command(
repo,
&[
"for-each-ref",
"--format=%(refname:short)",
"refs/heads/agents/",
],
)
.output()
.expect("spawn git for-each-ref");
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout);
for line in s.lines() {
let Some(name) = line.trim().strip_prefix("agents/") else {
continue;
};
if name.len() == 25 && name.chars().filter(|c| *c == '-').count() == 1 {
return Some(name.to_owned());
}
}
None
}
pub fn poll_for_path(dest: &Path, path: &Path) {
if poll::until(dest, || path.exists().then_some(())).is_none() {
panic!(
"{} never appeared, and {} went untouched for {:?}",
path.display(),
dest.display(),
poll::patience()
);
}
}
pub fn reap(dest: &Path, child: &mut Child) -> ExitStatus {
let reaped = poll::until(dest, || child.try_wait().expect("try_wait"));
if let Some(status) = reaped {
return status;
}
let _ = child.kill();
let _ = child.wait();
panic!(
"lernie prompt outlived the stop, with {} untouched for {:?}",
dest.display(),
poll::patience()
);
}
pub fn spawn_prompt(
dest: &Path,
harness: &Path,
brazen_config: &Path,
user_message: &str,
) -> Child {
Command::new(lernie_bin())
.arg("prompt")
.arg(dest)
.arg(user_message)
.env("LERNIE_HOME", harness)
.env("BRAZEN_CONFIG", brazen_config)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn lernie prompt")
}
pub const HAPPY_SSE: &str = concat!(
"event: message_start\n",
"data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_e2e\",\"model\":\"claude-sonnet-5\",\"stop_reason\":null,\"content\":[],\"usage\":{\"input_tokens\":2,\"output_tokens\":0}}}\n\n",
"event: content_block_start\n",
"data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n",
"event: content_block_delta\n",
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"pong\"}}\n\n",
"event: content_block_stop\n",
"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
"event: message_delta\n",
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":1}}\n\n",
"event: message_stop\n",
"data: {\"type\":\"message_stop\"}\n\n",
);