use crate::harness_root::Roots;
use crate::prompt::step::STEPS_DIR;
use crate::prompt::tool::STEP_TOOLS_SUBDIR;
use crate::template::{GitRunner, RealGit};
use crate::test_support::litany_binary;
use crate::workspace::{agent_worktree, fixture};
use std::io::Write;
use std::process::{Command, Stdio};
const AGENT: &str = "20260101-a1";
#[test]
fn a_bash_tool_call_raises_an_inner_invocation_that_the_transcript_never_sees() {
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 inner = r#"{"id":"tu_outer-1","name":"bash","input":{"command":"echo inner"}}"#;
let command = format!("printf '%s' '{inner}' | {} invoke", litany.display());
let outer = serde_json::json!({ "command": command }).to_string();
let mut child = Command::new(&litany)
.arg("tool")
.arg("bash")
.current_dir(&worktree)
.env("LITANY_HOME", &home)
.env(crate::prompt::tool::ENV_CONV_REPO, &ws)
.env(crate::prompt::tool::ENV_CONV_BRANCH, AGENT)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn litany tool bash");
child
.stdin
.take()
.expect("stdin is piped")
.write_all(outer.as_bytes())
.expect("write the outer tool input");
let out = child.wait_with_output().expect("reap litany tool bash");
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!(stdout.contains("Exit code: 0\ninner\n"), "{stdout}{stderr}");
assert!(
step.join(STEP_TOOLS_SUBDIR).join("tu_outer-1").is_dir(),
"the inner record lands under the in-flight step"
);
let after = git.run_capture(&worktree, &["rev-parse", "HEAD"]).unwrap();
assert_eq!(before, after, "the door commits nothing");
assert!(
!worktree
.join(crate::prompt::dispatch::MESSAGES_DIR)
.exists(),
"no transcript entry is written for an inner invocation"
);
assert_eq!(agent_worktree(&ws, AGENT), worktree);
}