use crate::harness_root::Roots;
use crate::prompt::step::STEPS_DIR;
use crate::prompt::tool::{ENV_TOOL_ID, STEP_TOOLS_SUBDIR};
use crate::template::{GitRunner, RealGit};
use crate::test_support::litany_binary;
use crate::workspace::fixture;
use std::io::Write;
use std::process::{Command, Stdio};
const AGENT: &str = "20260101-a1";
const TOOL_ID: &str = "tu_program";
const PROGRAM: &str = r#"
import litany_tools
first = litany_tools.bash(command="printf alpha")
second = litany_tools.bash(command="printf beta; exit 3")
print("ran 2, ok=%d, codes=%s, joined=%s" % (
sum(1 for r in (first, second) if r.ok),
[first.exit_code, second.exit_code],
first.stdout.splitlines()[-1] + second.stdout.splitlines()[-1],
))
"#;
#[test]
fn a_program_composes_two_inner_invocations_and_only_its_stdout_comes_back() {
let holder = tempfile::TempDir::new().unwrap();
let home = holder.path().join("home");
let roots = Roots {
config: home.clone(),
data: home.clone(),
};
let ws = fixture::workspace_under(&roots);
std::fs::write(
home.join("models.yaml"),
format!("adapter: {}\n", home.join("no-adapter").display()),
)
.unwrap();
let worktree = fixture::spawn_root(&ws, AGENT);
let step = ws.join(STEPS_DIR).join(AGENT).join("001");
std::fs::create_dir_all(&step).unwrap();
let git = RealGit::new();
let before = git.run_capture(&worktree, &["rev-parse", "HEAD"]).unwrap();
let litany = litany_binary();
let input = serde_json::json!({ "program": PROGRAM }).to_string();
let mut child = Command::new(&litany)
.arg("tool")
.arg("python")
.current_dir(&worktree)
.env("LITANY_HOME", &home)
.env(crate::prompt::tool::ENV_CONV_REPO, &ws)
.env(crate::prompt::tool::ENV_CONV_BRANCH, AGENT)
.env(ENV_TOOL_ID, TOOL_ID)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn litany tool python");
child
.stdin
.take()
.expect("stdin is piped")
.write_all(input.as_bytes())
.expect("write the program");
let out = child.wait_with_output().expect("reap litany tool python");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(out.status.success(), "{stdout}{stderr}");
assert_eq!(
stdout, "ran 2, ok=1, codes=[0, 3], joined=alphabeta\n",
"{stderr}"
);
assert!(
!stdout.contains("Exit code:"),
"an inner result envelope reached the model: {stdout}"
);
let tools = step.join(STEP_TOOLS_SUBDIR);
assert!(tools.join(format!("{TOOL_ID}-1")).is_dir(), "{stderr}");
assert!(tools.join(format!("{TOOL_ID}-2")).is_dir(), "{stderr}");
assert!(tools.join(TOOL_ID).join("litany_tools.py").is_file());
let after = git.run_capture(&worktree, &["rev-parse", "HEAD"]).unwrap();
assert_eq!(before, after, "an inner invocation commits nothing");
assert!(
!worktree
.join(crate::prompt::dispatch::MESSAGES_DIR)
.exists(),
"no transcript entry is written for an inner invocation"
);
}