use crate::brain::tools::read::ReadTool;
use crate::brain::tools::{Tool, ToolExecutionContext};
use std::io::Write;
use uuid::Uuid;
async fn read(json: serde_json::Value) -> crate::brain::tools::ToolResult {
let tool = ReadTool;
let ctx = ToolExecutionContext::new(Uuid::new_v4());
tool.execute(json, &ctx).await.unwrap()
}
#[tokio::test]
async fn long_line_is_clamped_with_original_length_announced() {
let mut f = tempfile::Builder::new().suffix(".js").tempfile().unwrap();
let long_line = "x".repeat(5_000);
writeln!(f, "short line\n{long_line}\ntail").unwrap();
f.flush().unwrap();
let result = read(serde_json::json!({ "path": f.path().to_str().unwrap() })).await;
assert!(result.success);
assert!(
result
.output
.contains("[line truncated: 5000 chars total, showing first 2000]"),
"clamped line must announce its original length; got: {}",
&result.output[..result.output.len().min(300)]
);
assert!(result.output.contains("tail"));
}
#[tokio::test]
async fn default_read_stops_at_output_budget_with_resume_offset() {
let mut f = tempfile::Builder::new().suffix(".log").tempfile().unwrap();
let total = 4_000usize;
for i in 0..total {
writeln!(f, "log entry {i:0>40}").unwrap();
}
f.flush().unwrap();
let result = read(serde_json::json!({ "path": f.path().to_str().unwrap() })).await;
assert!(result.success);
let warning = result
.metadata
.get("warning")
.expect("budget-truncated read must carry a warning");
assert!(
warning.contains("128 KB output budget"),
"warning: {warning}"
);
assert!(warning.contains("4000 total lines"), "warning: {warning}");
assert!(
result.output.len() <= 128 * 1024,
"budget must bound emitted bytes"
);
let resume = warning
.split("start_line=")
.nth(1)
.and_then(|s| s.split(' ').next())
.and_then(|s| s.parse::<usize>().ok())
.expect("warning must carry a numeric resume offset");
let emitted = result.output.lines().count();
assert_eq!(
resume, emitted,
"resume offset must equal the first unread line"
);
assert!(emitted < total, "budget must stop before the file ends");
}
#[tokio::test]
async fn explicit_range_bypasses_budget_but_never_the_clamp() {
let mut f = tempfile::Builder::new().suffix(".log").tempfile().unwrap();
let total = 4_000usize;
for i in 0..total {
writeln!(f, "log entry {i:0>40}").unwrap();
}
writeln!(f, "{}", "y".repeat(5_000)).unwrap(); f.flush().unwrap();
let result = read(serde_json::json!({
"path": f.path().to_str().unwrap(),
"start_line": 0,
"line_count": 4001
}))
.await;
assert!(result.success);
let warning = result
.metadata
.get("warning")
.map(String::as_str)
.unwrap_or("");
assert!(
!warning.contains("output budget"),
"explicit range must bypass the budget: {warning}"
);
assert!(
warning.contains("exceeded 2000 chars"),
"clamp must still announce: {warning}"
);
assert!(result.output.contains("[line truncated: 5000 chars total"));
}
#[tokio::test]
async fn buffered_budget_path_counts_the_rejected_line() {
let mut f = tempfile::Builder::new().suffix(".log").tempfile().unwrap();
let total = 100_005usize;
for i in 0..total {
writeln!(f, "line {i:0>100}").unwrap();
}
f.flush().unwrap();
let result = read(serde_json::json!({ "path": f.path().to_str().unwrap() })).await;
assert!(result.success);
let warning = result
.metadata
.get("warning")
.expect("buffered budget truncation must carry a warning");
assert!(
warning.contains("128 KB output budget"),
"warning: {warning}"
);
assert!(
warning.contains("100005 total lines"),
"the consumed-but-rejected line must be counted: {warning}"
);
}