use super::fixtures::{FixedClock, HarnessRoot, StepDir, after_header};
use crate::config::ToolOutputBound;
use crate::prompt::clock::SystemClock;
use crate::prompt::dispatch::assembler::assemble;
use crate::prompt::tool::spawn::PathLookup;
use crate::prompt::tool::{SpawnTool, ToolCall, ToolExecutor};
use brazen::{Content, Role};
use serde_json::json;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
struct NoPath;
impl PathLookup for NoPath {
fn which_on_path(&self, _prefixed_name: &str) -> Option<PathBuf> {
None
}
}
fn bound(head: usize, tail: usize) -> Option<ToolOutputBound> {
Some(ToolOutputBound {
head_bytes: head,
tail_bytes: tail,
})
}
fn address_in(content: &str) -> String {
let clause = content
.split("read the cut middle with read_tool_output, address ")
.nth(1)
.expect("a cut result names its recovery");
clause
.split_whitespace()
.next()
.expect("the clause names an address")
.to_string()
}
#[test]
fn the_address_a_cut_minted_pages_the_bytes_the_cut_removed() {
let root = HarnessRoot::new();
root.install(
"chatty",
r#"for r in 1 2 3; do for c in {a..z}; do printf '%.0s'"$c" {1..99}; printf '\n'; done; done"#,
);
let clock = FixedClock::default();
let step = StepDir::new();
let litany = crate::test_support::litany_binary();
let exec = SpawnTool::new(root.path(), &clock, &litany).with_path_lookup(Box::new(NoPath));
let cut = exec
.execute(
ToolCall {
id: "toolu_big",
name: "chatty",
input: &json!({}),
},
&step.path,
&AtomicBool::new(false),
bound(40, 40),
)
.expect("the tool runs");
let cut_text = String::from_utf8(cut.content).expect("ASCII fixture");
let address = address_in(&cut_text);
assert_eq!(
address,
"steps/convid/001/tools/toolu_big/output.json#stdout@40"
);
let clock = SystemClock;
let litany = crate::test_support::litany_binary();
let exec = SpawnTool::new(root.path(), &clock, &litany).with_path_lookup(Box::new(NoPath));
let page = exec
.execute(
ToolCall {
id: "toolu_page",
name: "read_tool_output",
input: &json!({ "address": address }),
},
&step.path,
&AtomicBool::new(false),
None,
)
.expect("the page reads");
assert!(
!page.is_error,
"{:?}",
String::from_utf8_lossy(&page.content)
);
let body = String::from_utf8(after_header(&page.content).to_vec()).expect("ASCII");
assert!(body.starts_with(&"a".repeat(59)), "{body}");
assert!(
body.contains(
"--- stderr ---\nread_tool_output: bytes 40-4136 of 7800 from \
steps/convid/001/tools/toolu_big/output.json stdout; continue with address \
steps/convid/001/tools/toolu_big/output.json#stdout@4136\n"
),
"{body}"
);
}
#[test]
fn a_foreign_agent_address_is_refused_through_the_binary() {
let root = HarnessRoot::new();
let clock = SystemClock;
let step = StepDir::new();
let litany = crate::test_support::litany_binary();
let exec = SpawnTool::new(root.path(), &clock, &litany).with_path_lookup(Box::new(NoPath));
let out = exec
.execute(
ToolCall {
id: "toolu_theft",
name: "read_tool_output",
input: &json!({
"address": "steps/someone-else/001/tools/toolu_1/output.json#stdout@0"
}),
},
&step.path,
&AtomicBool::new(false),
None,
)
.expect("the tool runs");
assert!(out.is_error);
let text = String::from_utf8(out.content).expect("ASCII");
assert!(
text.contains(
"address names agent \"someone-else\", and you are \"convid\": \
an agent may page only its own tool captures"
),
"{text}"
);
}
#[test]
fn assembly_reads_nothing_under_steps() {
let root = HarnessRoot::new();
root.install(
"chatty",
r#"printf 'HEAD%s TAIL\n' "$(printf 'm%.0s' {1..200})""#,
);
let clock = FixedClock::default();
let step = StepDir::new();
let litany = crate::test_support::litany_binary();
let exec = SpawnTool::new(root.path(), &clock, &litany).with_path_lookup(Box::new(NoPath));
let cut = exec
.execute(
ToolCall {
id: "toolu_big",
name: "chatty",
input: &json!({}),
},
&step.path,
&AtomicBool::new(false),
bound(8, 8),
)
.expect("the tool runs");
let text = String::from_utf8(cut.content).expect("ASCII fixture");
let address = address_in(&text);
let messages = step.worktree.join("messages");
std::fs::create_dir_all(&messages).expect("mkdir messages");
std::fs::write(
messages.join("001-claude-fable-5.json"),
serde_json::to_vec(&[Content::ToolUse {
id: "toolu_big".into(),
name: "read_file".into(),
input: json!({}),
signature: None,
}])
.expect("blocks serialize"),
)
.expect("write call entry");
std::fs::write(
messages.join("002-tool.json"),
serde_json::to_vec(&[Content::ToolResult {
tool_use_id: "toolu_big".into(),
content: vec![Content::Text(text.clone())],
is_error: false,
}])
.expect("blocks serialize"),
)
.expect("write entry");
let steps_root = step
.path
.parent()
.and_then(std::path::Path::parent)
.expect("steps/");
std::fs::remove_dir_all(steps_root).expect("remove steps/");
assert!(!steps_root.exists());
let msgs = assemble(&step.worktree, None)
.expect("assembly needs no step record")
.messages;
assert_eq!(msgs.len(), 2);
assert_eq!(msgs[1].role, Role::Tool);
let Some(Content::ToolResult { content, .. }) = msgs[1].content.first() else {
panic!("the entry composes as a tool result");
};
let Some(Content::Text(composed)) = content.first() else {
panic!("the result carries its text");
};
assert!(composed.contains(&address), "{composed}");
assert!(
composed.contains("read the cut middle with read_tool_output"),
"{composed}"
);
}