use httpmock::Method::POST;
use httpmock::MockServer;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
use super::poll;
use super::prompt_end_to_end::{
HAPPY_SSE, scaffold_repo, write_brazen_config, write_global_models,
};
fn lernie_bin() -> PathBuf {
crate::test_support::lernie_binary()
}
struct Fixture {
holder: TempDir,
harness: PathBuf,
brazen_config: PathBuf,
ws: PathBuf,
}
fn fixture(endpoint: &str) -> Fixture {
let holder = TempDir::new().unwrap();
let harness = holder.path().join("harness");
fs::create_dir_all(&harness).unwrap();
write_global_models(&harness);
let brazen_config = write_brazen_config(holder.path(), endpoint);
let ws = holder.path().join("ws");
scaffold_repo(&ws, &harness);
Fixture {
holder,
harness,
brazen_config,
ws,
}
}
impl Fixture {
fn lernie(&self, args: &[&str]) -> String {
let out = Command::new(lernie_bin())
.args(args)
.env("LERNIE_HOME", &self.harness)
.env("BRAZEN_CONFIG", &self.brazen_config)
.output()
.expect("spawn lernie");
assert!(
out.status.success(),
"lernie {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
fn run_bundle_replay(&self) -> (PathBuf, String) {
let agent = self.lernie(&["prompt", self.ws.to_str().unwrap(), "ping"]);
let arch = self.holder.path().join("arch");
self.lernie(&[
"bundle",
self.ws.to_str().unwrap(),
&agent,
arch.to_str().unwrap(),
]);
let scratch = self.lernie(&["replay", arch.to_str().unwrap()]);
(PathBuf::from(scratch), agent)
}
}
fn wait_for(workspace: &Path, path: &Path) {
if poll::until(workspace, || path.exists().then_some(())).is_none() {
panic!(
"{path:?} never appeared, and {} went untouched for {:?} — nothing is driving it",
workspace.display(),
poll::patience()
);
}
}
fn mock_server() -> MockServer {
let server = MockServer::start();
server.mock(|when, then| {
when.method(POST).path("/v1/messages");
then.status(200)
.header("content-type", "text/event-stream")
.body(HAPPY_SSE);
});
server
}
#[test]
fn a_replayed_workspace_takes_a_fresh_prompt() {
let server = mock_server();
let fx = fixture(&server.base_url());
let (scratch, archived) = fx.run_bundle_replay();
let scratch_repo = scratch.join("repo.git");
let branches = crate::template::GitRunner::run_capture(
&crate::template::RealGit::new(),
&scratch_repo,
&["branch", "--list", "--format=%(refname:short)"],
)
.unwrap();
assert!(branches.contains("config/default"), "{branches}");
assert!(branches.contains(&archived), "{branches}");
let fresh = fx.lernie(&["prompt", scratch.to_str().unwrap(), "ping again"]);
assert_ne!(fresh, archived);
let transcript = scratch.join("agents").join(&fresh).join("messages");
assert!(transcript.join("001-user.md").exists());
assert!(
fs::read_dir(&transcript)
.unwrap()
.filter_map(Result::ok)
.any(|e| e.file_name().to_string_lossy().starts_with("002-")),
"the fresh root landed no assistant entry"
);
}
#[test]
fn a_replayed_agent_advances_on_its_governing_config() {
let server = mock_server();
let fx = fixture(&server.base_url());
let (scratch, agent) = fx.run_bundle_replay();
let git = crate::template::RealGit::new();
let source_gov =
crate::workspace::governing_config(&fx.ws, &crate::workspace::agent_ref(&agent), &git)
.unwrap();
let scratch_gov =
crate::workspace::governing_config(&scratch, &crate::workspace::agent_ref(&agent), &git)
.unwrap();
assert_eq!(source_gov, scratch_gov);
fx.lernie(&["message", scratch.to_str().unwrap(), &agent, "again"]);
let transcript = scratch.join("agents").join(&agent).join("messages");
wait_for(&scratch, &transcript.join("003-user.md"));
let landed = poll::until(&scratch, || {
fs::read_dir(&transcript)
.unwrap()
.filter_map(Result::ok)
.any(|e| e.file_name().to_string_lossy().starts_with("004-"))
.then_some(())
});
assert!(
landed.is_some(),
"no step landed on the replay, and the scratch went untouched for {:?}",
poll::patience()
);
}