use super::fixtures::StepDir;
use crate::prompt::clock::SystemClock;
use crate::prompt::tool::spawn::PathLookup;
use crate::prompt::tool::{
INPUT_FILE, OUTPUT_FILE, STEP_TOOLS_SUBDIR, SpawnTool, ToolCall, ToolExecutor, ToolInputRecord,
ToolOutputRecord,
};
use serde_json::json;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use tempfile::TempDir;
fn lernie_bin() -> PathBuf {
crate::test_support::lernie_binary()
}
struct NoPath;
impl PathLookup for NoPath {
fn which_on_path(&self, _prefixed_name: &str) -> Option<PathBuf> {
None
}
}
struct Fixture {
_harness_root: TempDir,
step: StepDir,
}
impl Fixture {
fn new() -> Self {
let harness = TempDir::new().expect("harness root tempdir");
std::fs::create_dir_all(harness.path().join("tools")).unwrap();
Self {
_harness_root: harness,
step: StepDir::new(),
}
}
fn harness_path(&self) -> &std::path::Path {
self._harness_root.path()
}
}
fn executor<'a>(harness: &'a Path, clock: &'a SystemClock, lernie: &'a Path) -> SpawnTool<'a> {
SpawnTool::new(harness, clock, lernie).with_path_lookup(Box::new(NoPath))
}
#[test]
fn read_file_through_executor_returns_file_bytes_and_lands_disk_record() {
let fixture = Fixture::new();
let target = fixture.step.worktree.join("greeting.txt");
let body = b"hello from read_file\n";
std::fs::write(&target, body).unwrap();
let clock = SystemClock;
let lernie = lernie_bin();
let exec = executor(fixture.harness_path(), &clock, &lernie);
let outcome = exec
.execute(
ToolCall {
id: "toolu_rf_ok",
name: "read_file",
input: &json!({ "path": target }),
},
&fixture.step.path,
&AtomicBool::new(false),
)
.expect("execute succeeds");
assert!(!outcome.is_error, "happy-path is_error should be false");
assert_eq!(outcome.content, body);
let dir = fixture
.step
.path
.join(STEP_TOOLS_SUBDIR)
.join("toolu_rf_ok");
let input: ToolInputRecord =
serde_json::from_slice(&std::fs::read(dir.join(INPUT_FILE)).unwrap()).unwrap();
assert_eq!(input.id, "toolu_rf_ok");
assert_eq!(input.name, "read_file");
assert_eq!(input.input["path"], json!(target));
let output: ToolOutputRecord =
serde_json::from_slice(&std::fs::read(dir.join(OUTPUT_FILE)).unwrap()).unwrap();
assert_eq!(output.exit_code, 0);
assert_eq!(output.stdout.as_bytes(), body);
assert_eq!(output.stderr, "");
assert!(!output.started_at.is_empty(), "started_at present");
assert!(!output.ended_at.is_empty(), "ended_at present");
}
#[test]
fn read_file_resolves_a_relative_path_against_the_agents_worktree() {
let fixture = Fixture::new();
let body = b"relative read\n";
std::fs::write(fixture.step.worktree.join("in-worktree.txt"), body).unwrap();
let clock = SystemClock;
let lernie = lernie_bin();
let exec = executor(fixture.harness_path(), &clock, &lernie);
let outcome = exec
.execute(
ToolCall {
id: "toolu_rf_rel",
name: "read_file",
input: &json!({ "path": "in-worktree.txt" }),
},
&fixture.step.path,
&AtomicBool::new(false),
)
.expect("execute succeeds");
assert!(!outcome.is_error, "relative read resolves: {outcome:?}");
assert_eq!(outcome.content, body);
}
#[test]
fn read_file_failure_concats_stderr_and_marks_is_error() {
let fixture = Fixture::new();
let missing = fixture.step.worktree.join("does-not-exist.txt");
let clock = SystemClock;
let lernie = lernie_bin();
let exec = executor(fixture.harness_path(), &clock, &lernie);
let outcome = exec
.execute(
ToolCall {
id: "toolu_rf_err",
name: "read_file",
input: &json!({ "path": missing }),
},
&fixture.step.path,
&AtomicBool::new(false),
)
.expect("execute returns Ok even when the tool exits non-zero");
assert!(outcome.is_error, "failure path: is_error must be true");
let content = String::from_utf8_lossy(&outcome.content);
assert!(
content.contains("does-not-exist.txt"),
"stderr message should name the offending path; got: {content:?}",
);
let dir = fixture
.step
.path
.join(STEP_TOOLS_SUBDIR)
.join("toolu_rf_err");
let output: ToolOutputRecord =
serde_json::from_slice(&std::fs::read(dir.join(OUTPUT_FILE)).unwrap()).unwrap();
assert_ne!(
output.exit_code, 0,
"output.json.exit_code records the non-zero exit"
);
assert!(
output.stderr.contains("does-not-exist.txt"),
"output.json.stderr captures the failure message: {:?}",
output.stderr
);
}