use super::prompt_end_to_end::{
HAPPY_SSE, scaffold_repo, write_brazen_config, write_global_models,
};
use httpmock::Method::POST;
use httpmock::MockServer;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::TempDir;
struct Fixture {
_holder: TempDir,
_server: MockServer,
ws: PathBuf,
harness: PathBuf,
brazen_config: PathBuf,
}
fn fixture() -> Fixture {
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);
});
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(), &server.base_url());
let ws = holder.path().join("conv");
scaffold_repo(&ws, &harness);
Fixture {
_holder: holder,
_server: server,
ws,
harness,
brazen_config,
}
}
impl Fixture {
fn prompt(&self, message: &str, args: &[&str]) -> std::process::Output {
Command::new(crate::test_support::lernie_binary())
.arg("prompt")
.arg(&self.ws)
.arg(message)
.args(args)
.env("LERNIE_HOME", &self.harness)
.env("BRAZEN_CONFIG", &self.brazen_config)
.stderr(Stdio::piped())
.output()
.expect("spawn lernie prompt")
}
fn start(&self, message: &str, args: &[&str]) -> String {
let out = self.prompt(message, args);
assert!(
out.status.success(),
"lernie prompt {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
fn bare(&self) -> PathBuf {
self.ws.join("repo.git")
}
}
fn system_slot(fx: &Fixture, agent: &str) -> String {
let path = fx.ws.join("steps").join(agent).join("001/request.json");
let request: serde_json::Value = serde_json::from_slice(&fs::read(path).unwrap()).unwrap();
request["system"][0]["text"].as_str().unwrap().to_string()
}
fn git(dest: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(dest)
.args(args)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.output()
.expect("spawn git");
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
fn git_ok(dest: &Path, args: &[&str]) -> bool {
Command::new("git")
.arg("-C")
.arg(dest)
.args(args)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.output()
.expect("spawn git")
.status
.success()
}
#[test]
fn a_start_forks_from_any_historical_commit_and_inherits_its_tree() {
let fx = fixture();
let first = fx.start("ping", &["--name", "pale-otter"]);
let bare = fx.bare();
let first_ref = format!("agents/{first}");
let on_branch = git(
&bare,
&[
"rev-list",
"--reverse",
&first_ref,
"--not",
"config/default",
],
);
let history = on_branch
.lines()
.nth(1)
.expect("the delivery commit of the agent's opening message")
.to_string();
assert_ne!(
history,
git(&bare, &["rev-parse", &first_ref]),
"the fork point is history, not the tip"
);
let second = fx.start("again", &["--from", &history]);
assert_ne!(second, first, "a fork from history is a new root agent");
let second_ref = format!("agents/{second}");
assert!(
git_ok(
&bare,
&["merge-base", "--is-ancestor", &history, &second_ref]
),
"the fork point must be an ancestor of the new branch"
);
assert_eq!(second.len(), 25, "got {second:?}");
let entries = git(&bare, &["ls-tree", "--name-only", &second_ref, "messages/"]);
assert!(entries.contains("001-user.md"), "got {entries:?}");
assert!(entries.contains("002-user.md"), "got {entries:?}");
assert_eq!(
git(&bare, &["show", &format!("{first_ref}:name")]),
"pale-otter"
);
let minted = git(&bare, &["show", &format!("{second_ref}:name")]);
assert_ne!(minted, "pale-otter", "the inherited name never propagates");
assert!(
!minted.is_empty() && minted.chars().all(|c| c.is_ascii_lowercase()),
"an omitted name is minted as one wordlist word, got {minted:?}"
);
let named_system = system_slot(&fx, &first);
assert!(
named_system.contains("</goal>\n\nYour name is pale-otter.\n\n"),
"got {named_system:?}"
);
let minted_system = system_slot(&fx, &second);
assert!(
minted_system.contains(&format!("</goal>\n\nYour name is {minted}.\n\n")),
"got {minted_system:?}"
);
assert!(minted_system.starts_with("<goal>"), "got {minted_system:?}");
assert!(
!git_ok(&bare, &["cat-file", "-e", &format!("{second_ref}:version")]),
"control files leave the agent's tree"
);
}
#[test]
fn a_start_selects_a_config_lineage_by_name() {
let fx = fixture();
crate::template::authoring::author(
&fx.ws,
&fx.ws.join(".no-pools"),
"strict",
crate::template::authoring::Origin::Fork { source: "default" },
|dir| fs::write(dir.join("souls/worker.md"), "You are the strict worker."),
&crate::template::RealGit::new(),
)
.unwrap();
let agent = fx.start("hi", &["--config", "strict"]);
let bare = fx.bare();
let agent_ref = format!("agents/{agent}");
let strict_head = git(&bare, &["rev-parse", "config/strict"]);
assert!(
git_ok(
&bare,
&["merge-base", "--is-ancestor", &strict_head, &agent_ref]
),
"the named lineage's head must be an ancestor"
);
let soul = git(&bare, &["show", &format!("{agent_ref}:soul.md")]);
assert_eq!(soul, "You are the strict worker.");
}
#[test]
fn an_unstartable_fork_point_is_declined_before_any_branch_exists() {
let fx = fixture();
let bare = fx.bare();
let before = git(&bare, &["for-each-ref", "--format=%(refname)"]);
let out = fx.prompt("hi", &["--config", "strict"]);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr).to_string();
assert!(err.contains("no config lineage \"strict\""), "{err}");
assert!(err.contains("existing lineages: default"), "{err}");
let out = fx.prompt("hi", &["--from", "agents/nope"]);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr).to_string();
assert!(err.contains("no ref or commit \"agents/nope\""), "{err}");
let out = fx.prompt("hi", &["--from", "config/default", "--config", "default"]);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr).to_string();
assert!(err.contains("not both"), "{err}");
assert_eq!(before, git(&bare, &["for-each-ref", "--format=%(refname)"]));
assert!(!fx.ws.join("agents").exists());
}