pub const EXEC_OUTPUT_BUDGET: usize = 16 * 1024;
pub const SILENT_BUDGET: usize = 200;
pub const CONCISE_BUDGET: usize = 2 * 1024;
pub const NORMAL_BUDGET: usize = 8 * 1024;
pub const VERBOSE_BUDGET: usize = 16 * 1024;
pub fn output_verbosity_budget(mode: &str) -> Option<usize> {
match mode {
"silent" => Some(SILENT_BUDGET),
"concise" => Some(CONCISE_BUDGET),
"normal" => Some(NORMAL_BUDGET),
"verbose" => Some(VERBOSE_BUDGET),
"full" => None,
_ => Some(CONCISE_BUDGET), }
}
pub fn output_verbosity_schema() -> serde_json::Value {
serde_json::json!({
"type": "string",
"enum": ["silent", "concise", "normal", "verbose", "full"],
"default": "concise",
"description": "Output verbosity: silent (~200B, truncated to exit code + minimal output), concise (~2KiB, default), normal (~8KiB), verbose (~16KiB), full (unlimited, capped by 64KiB hard limit). For tools with output persistence enabled, full output is saved to /outputs/{tool_call_id}.stdout and /outputs/{tool_call_id}.stderr — use read_file to retrieve."
})
}
pub const EXEC_OUTPUT_HINT: &str = "\n\n**Output economy:** Command output is truncated based on the `output` parameter (default: `concise` ~2 KiB). \
Use `verbose` or `full` when debugging failures. Tools with output persistence enabled save full output — stdout to `/outputs/{tool_call_id}.stdout`, stderr to `/outputs/{tool_call_id}.stderr`. \
When output exceeds the budget, the result includes an `output_files` array with paths you can `read_file` with offset/limit.\n\
Available modes: `silent` (~200B), `concise` (~2KiB), `normal` (~8KiB), `verbose` (~16KiB), `full` (unlimited).\n\
For build/install commands, the default `concise` is usually sufficient — check exit code first.\n\
If you need more detail, re-run with `output: \"verbose\"` or read the persisted output files via `read_file`.";
pub const READ_ECONOMY_HINT: &str = "\n\n**File reading economy:** `read_file` returns at most 2000 lines by default.\n\
- Locate the relevant region first with `grep_files`, then read that section with `read_file` using `offset` and `limit`.\n\
- Use `list_directory` to understand file structure before reading.\n\
- When a read is truncated, check `total_lines` to see how much remains and continue from `lines_shown.end` on the next call.";
pub fn strip_ansi(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
match chars.peek() {
Some('[') => {
chars.next(); for c in chars.by_ref() {
if ('@'..='~').contains(&c) {
break;
}
}
}
Some(']') => {
chars.next(); for c in chars.by_ref() {
if c == '\x07' {
break;
}
if c == '\x1b' {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
}
}
Some('(') | Some(')') => {
chars.next(); chars.next(); }
_ => {
chars.next();
}
}
} else {
result.push(ch);
}
}
result
}
pub fn collapse_cr_lines(text: &str) -> String {
let mut result = String::with_capacity(text.len());
for line in text.split('\n') {
if !result.is_empty() {
result.push('\n');
}
if let Some(pos) = line.rfind('\r') {
if pos + 1 == line.len() {
result.push_str(&line[..pos]);
} else {
result.push_str(&line[pos + 1..]);
}
} else {
result.push_str(line);
}
}
result
}
pub fn middle_truncate(text: &str, max_bytes: usize) -> String {
if text.len() <= max_bytes {
return text.to_string();
}
let marker_budget = 80; let content_budget = max_bytes.saturating_sub(marker_budget);
if content_budget == 0 {
let mut marker = format!("[... {} bytes omitted ...]", text.len());
if marker.len() > max_bytes {
let cutoff = utf8_floor(&marker, max_bytes);
marker.truncate(cutoff);
}
return marker;
}
let head_budget = content_budget / 5;
let tail_budget = content_budget - head_budget;
let head_end = utf8_floor(text, head_budget);
let tail_start = utf8_ceil(text, text.len().saturating_sub(tail_budget));
let omitted = text.len() - head_end - (text.len() - tail_start);
let marker = format!("\n\n[... {} bytes omitted ...]\n\n", omitted);
let mut result = String::with_capacity(head_end + marker.len() + (text.len() - tail_start));
result.push_str(&text[..head_end]);
result.push_str(&marker);
result.push_str(&text[tail_start..]);
result
}
pub fn clean_exec_output(text: &str) -> String {
let cleaned = strip_ansi(text);
collapse_cr_lines(&cleaned)
}
pub const READ_FILE_DEFAULT_LIMIT: usize = 2000;
pub const READ_FILE_HARD_BYTE_CAP: usize = 50 * 1024;
pub fn apply_read_file_hard_cap(result: &mut String) -> bool {
if result.len() <= READ_FILE_HARD_BYTE_CAP {
return false;
}
let cut = utf8_floor(result, READ_FILE_HARD_BYTE_CAP);
result.truncate(cut);
true
}
#[derive(Debug)]
struct FormattedReadFileWindow {
content: String,
total_lines: usize,
start_line: usize,
end_line: usize,
line_capped: bool,
size_capped: bool,
}
fn format_lines_with_metadata(
content: &str,
offset: usize,
limit: usize,
) -> FormattedReadFileWindow {
let window_end = offset.saturating_add(limit);
let mut total_lines = 0;
let mut result = String::new();
let mut start_line = 0;
let mut end_line = 0;
let mut size_capped = false;
for (idx, line) in content.lines().enumerate() {
total_lines = idx + 1;
if idx < offset || idx >= window_end {
continue;
}
if size_capped {
continue;
}
let line_num = idx + 1;
if start_line == 0 {
start_line = line_num;
}
let separator = if result.is_empty() { "" } else { "\n" };
let formatted_line = format!("{separator}{line_num}|{line}");
let available = READ_FILE_HARD_BYTE_CAP.saturating_sub(result.len());
if formatted_line.len() <= available {
result.push_str(&formatted_line);
end_line = line_num;
continue;
}
let cut = utf8_floor(&formatted_line, available);
if cut > 0 {
result.push_str(&formatted_line[..cut]);
end_line = line_num;
}
size_capped = true;
}
let end = offset.saturating_add(limit).min(total_lines);
let line_capped = end < total_lines;
FormattedReadFileWindow {
content: result,
total_lines,
start_line,
end_line,
line_capped,
size_capped,
}
}
pub fn format_lines(content: &str, offset: usize, limit: usize) -> (String, usize, bool) {
let formatted = format_lines_with_metadata(content, offset, limit);
(
formatted.content,
formatted.total_lines,
formatted.line_capped || formatted.size_capped,
)
}
pub fn parse_read_file_window_args(
arguments: &serde_json::Value,
) -> Result<(usize, usize), String> {
let offset = arguments
.get("offset")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let limit = arguments
.get("limit")
.and_then(|v| v.as_u64())
.unwrap_or(READ_FILE_DEFAULT_LIMIT as u64) as usize;
if limit == 0 {
return Err("limit must be a positive integer".to_string());
}
Ok((offset, limit))
}
pub fn build_text_read_file_result(
tool_name: &str,
path: &str,
content: &str,
encoding: &str,
offset: usize,
limit: usize,
) -> serde_json::Value {
let formatted = format_lines_with_metadata(content, offset, limit);
let truncated = formatted.line_capped || formatted.size_capped;
let mut result = serde_json::json!({
"path": path,
"content": formatted.content,
"encoding": encoding,
"total_lines": formatted.total_lines,
"lines_shown": {
"start": formatted.start_line,
"end": formatted.end_line,
},
"truncated": truncated,
"size_bytes": content.len(),
});
let truncation = if truncated {
if formatted.size_capped {
crate::truncation_info::TruncationInfo::without_resume(
formatted.content.len(),
Some(content.len()),
crate::truncation_info::TruncationReason::SizeCap,
)
} else if formatted.line_capped {
crate::truncation_info::TruncationInfo::with_resume(
formatted.content.len(),
Some(content.len()),
formatted.end_line as u64,
format!(
"call {tool_name} with offset={} to resume from line {}",
formatted.end_line,
formatted.end_line + 1,
),
crate::truncation_info::TruncationReason::LineCap,
)
} else {
crate::truncation_info::TruncationInfo::without_resume(
formatted.content.len(),
Some(content.len()),
crate::truncation_info::TruncationReason::SizeCap,
)
}
} else {
crate::truncation_info::TruncationInfo::not_truncated(formatted.content.len())
};
truncation.attach(&mut result);
result
}
pub fn build_binary_read_file_result(
path: &str,
size_bytes: usize,
encoding: &str,
) -> serde_json::Value {
let mut result = serde_json::json!({
"path": path,
"content_type": "binary",
"encoding": encoding,
"size_bytes": size_bytes,
"note": "Binary file — use a different tool or download to inspect."
});
crate::truncation_info::TruncationInfo {
truncated: false,
bytes_returned: 0,
bytes_total: Some(size_bytes),
next_offset: None,
resume_hint: None,
reason: crate::truncation_info::TruncationReason::SizeCap,
}
.attach(&mut result);
result
}
pub fn build_bytes_read_file_result(
tool_name: &str,
path: &str,
bytes: &[u8],
offset: usize,
limit: usize,
) -> serde_json::Value {
match std::str::from_utf8(bytes) {
Ok(content) => build_text_read_file_result(tool_name, path, content, "text", offset, limit),
Err(_) => build_binary_read_file_result(path, bytes.len(), "binary"),
}
}
pub fn sanitize_exec_output(text: &str, max_bytes: usize) -> String {
let cleaned = clean_exec_output(text);
priority_aware_truncate(&cleaned, max_bytes)
}
const ERROR_CONTEXT_LINES: usize = 5;
const ERROR_PATTERNS: &[&str] = &[
"error:",
"Error:",
"ERROR",
"FAILED",
"FAIL",
"failed",
"panic",
"panicked at",
"assert",
"assertion failed",
"Traceback (most recent call last)",
"at Object.<anonymous>",
"at Module._compile",
"--- stderr ---",
];
const LINE_START_PATTERNS: &[&str] = &["E "];
#[derive(Debug, Clone)]
struct ErrorRegion {
start: usize,
end: usize,
}
fn find_error_regions(lines: &[&str]) -> Vec<ErrorRegion> {
let mut hit_lines: Vec<usize> = Vec::new();
for (idx, line) in lines.iter().enumerate() {
let is_error = ERROR_PATTERNS.iter().any(|p| line.contains(p))
|| LINE_START_PATTERNS.iter().any(|p| line.starts_with(p));
if is_error {
hit_lines.push(idx);
}
}
if hit_lines.is_empty() {
return Vec::new();
}
let total = lines.len();
let mut regions: Vec<ErrorRegion> = Vec::new();
for &hit in &hit_lines {
let start = hit.saturating_sub(ERROR_CONTEXT_LINES);
let end = (hit + ERROR_CONTEXT_LINES + 1).min(total);
if let Some(last) = regions.last_mut()
&& start <= last.end
{
last.end = end;
continue;
}
regions.push(ErrorRegion { start, end });
}
regions
}
pub fn priority_aware_truncate(text: &str, max_bytes: usize) -> String {
if text.len() <= max_bytes {
return text.to_string();
}
let lines: Vec<&str> = text.lines().collect();
let regions = find_error_regions(&lines);
if regions.is_empty() {
return middle_truncate(text, max_bytes);
}
let mut sections: Vec<String> = Vec::new();
let mut error_bytes: usize = 0;
for region in ®ions {
let region_text: String = lines[region.start..region.end].join("\n");
error_bytes += region_text.len() + 40; sections.push(region_text);
}
let marker_overhead = 80;
if max_bytes < marker_overhead {
return middle_truncate(text, max_bytes);
}
let available_for_context = max_bytes - marker_overhead;
if error_bytes >= available_for_context {
let mut result = String::new();
let mut remaining = available_for_context;
for (i, section) in sections.iter().enumerate() {
let marker = if i == 0 && regions[i].start > 0 {
format!("[... {} lines above ...]\n", regions[i].start)
} else if i > 0 {
let gap = regions[i].start - regions[i - 1].end;
format!("\n[... {} lines omitted ...]\n", gap)
} else {
String::new()
};
if marker.len() >= remaining {
break;
}
remaining -= marker.len();
result.push_str(&marker);
let take = section.len().min(remaining);
let safe_take = utf8_floor(section, take);
result.push_str(§ion[..safe_take]);
remaining = remaining.saturating_sub(safe_take);
if remaining == 0 {
break;
}
}
let lines_after = lines
.len()
.saturating_sub(regions.last().map_or(0, |r| r.end));
if lines_after > 0 {
let trailer = format!("\n[... {} lines below ...]", lines_after);
if trailer.len() <= remaining {
result.push_str(&trailer);
}
}
return result;
}
let context_budget = available_for_context - error_bytes;
let head_budget = context_budget / 5; let tail_budget = context_budget - head_budget;
let mut result = String::new();
let first_region_start = regions[0].start;
if first_region_start > 0 {
let mut head_used = 0usize;
let mut head_lines_kept = 0usize;
for line in &lines[..first_region_start] {
let needed = if head_lines_kept > 0 {
1 + line.len()
} else {
line.len()
};
if head_used + needed > head_budget {
break;
}
if head_lines_kept > 0 {
result.push('\n');
}
result.push_str(line);
head_used += needed;
head_lines_kept += 1;
}
let omitted = first_region_start - head_lines_kept;
if omitted > 0 {
result.push_str(&format!("\n[... {} lines omitted ...]\n", omitted));
} else {
result.push('\n');
}
}
for (i, (region, section)) in regions.iter().zip(sections.iter()).enumerate() {
if i > 0 {
let gap = region.start - regions[i - 1].end;
if gap > 0 {
result.push_str(&format!("\n[... {} lines omitted ...]\n", gap));
}
}
result.push_str(section);
}
let last_region_end = regions.last().map_or(0, |r| r.end);
let tail_lines = &lines[last_region_end..];
if !tail_lines.is_empty() {
let tail_total: usize =
tail_lines.iter().map(|l| l.len()).sum::<usize>() + tail_lines.len().saturating_sub(1);
if tail_total <= tail_budget {
result.push('\n');
for (i, line) in tail_lines.iter().enumerate() {
if i > 0 {
result.push('\n');
}
result.push_str(line);
}
} else {
let mut tail_used = 0usize;
let mut tail_start_idx = tail_lines.len();
for i in (0..tail_lines.len()).rev() {
let needed = tail_lines[i].len() + if i < tail_lines.len() - 1 { 1 } else { 0 };
if tail_used + needed > tail_budget {
break;
}
tail_used += needed;
tail_start_idx = i;
}
let omitted = tail_start_idx;
result.push_str(&format!("\n[... {} lines omitted ...]\n", omitted));
for (i, line) in tail_lines[tail_start_idx..].iter().enumerate() {
if i > 0 {
result.push('\n');
}
result.push_str(line);
}
}
}
if result.len() > max_bytes {
let safe = utf8_floor(&result, max_bytes);
result.truncate(safe);
}
result
}
fn utf8_floor(text: &str, pos: usize) -> usize {
let pos = pos.min(text.len());
let mut i = pos;
while i > 0 && !text.is_char_boundary(i) {
i -= 1;
}
i
}
fn utf8_ceil(text: &str, pos: usize) -> usize {
let pos = pos.min(text.len());
let mut i = pos;
while i < text.len() && !text.is_char_boundary(i) {
i += 1;
}
i
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_ansi_no_escapes() {
assert_eq!(strip_ansi("hello world"), "hello world");
}
#[test]
fn test_strip_ansi_sgr_color_codes() {
assert_eq!(
strip_ansi("\x1b[1;31merror\x1b[0m: something failed"),
"error: something failed"
);
}
#[test]
fn test_strip_ansi_cursor_movement() {
assert_eq!(strip_ansi("\x1b[2J\x1b[Hhello"), "hello");
}
#[test]
fn test_strip_ansi_osc_title() {
assert_eq!(strip_ansi("\x1b]0;my title\x07some output"), "some output");
}
#[test]
fn test_strip_ansi_osc_terminated_by_st() {
assert_eq!(strip_ansi("\x1b]0;title\x1b\\output"), "output");
}
#[test]
fn test_strip_ansi_preserves_normal_brackets() {
assert_eq!(strip_ansi("array[0] = 1"), "array[0] = 1");
}
#[test]
fn test_strip_ansi_mixed_content() {
let input =
"\x1b[32mCompiling\x1b[0m foo v0.1.0\n\x1b[31merror\x1b[0m[E0308]: mismatched types";
assert_eq!(
strip_ansi(input),
"Compiling foo v0.1.0\nerror[E0308]: mismatched types"
);
}
#[test]
fn test_strip_ansi_empty() {
assert_eq!(strip_ansi(""), "");
}
#[test]
fn test_collapse_cr_no_cr() {
assert_eq!(collapse_cr_lines("hello\nworld"), "hello\nworld");
}
#[test]
fn test_collapse_cr_progress_bar() {
let input = "Downloading 10%\rDownloading 50%\rDownloading 100%";
assert_eq!(collapse_cr_lines(input), "Downloading 100%");
}
#[test]
fn test_collapse_cr_mixed_lines() {
let input = "Building...\rBuilding... done\nTests passed\nProgress 50%\rProgress 100%";
assert_eq!(
collapse_cr_lines(input),
"Building... done\nTests passed\nProgress 100%"
);
}
#[test]
fn test_collapse_cr_trailing_cr() {
assert_eq!(collapse_cr_lines("hello\r"), "hello");
}
#[test]
fn test_collapse_cr_crlf_preserved() {
assert_eq!(collapse_cr_lines("line1\r\nline2\r\n"), "line1\nline2\n");
}
#[test]
fn test_collapse_cr_empty() {
assert_eq!(collapse_cr_lines(""), "");
}
#[test]
fn test_middle_truncate_under_budget() {
let text = "short text";
assert_eq!(middle_truncate(text, 1024), text);
}
#[test]
fn test_middle_truncate_exact_budget() {
let text = "a".repeat(100);
assert_eq!(middle_truncate(&text, 100), text);
}
#[test]
fn test_middle_truncate_over_budget() {
let text = "a".repeat(1000);
let result = middle_truncate(&text, 200);
assert!(result.len() <= 200);
assert!(result.contains("[..."));
assert!(result.contains("bytes omitted"));
let marker_pos = result.find("[...").unwrap();
let after_marker = result.find("...]").unwrap() + 4;
let head_len = marker_pos;
let tail_len = result.len() - after_marker;
assert!(
tail_len > head_len,
"tail ({}) should be > head ({})",
tail_len,
head_len
);
}
#[test]
fn test_middle_truncate_utf8_safety() {
let text = "€".repeat(200); let result = middle_truncate(&text, 100);
assert!(result.len() <= 100 + 80); assert!(result.contains("[..."));
}
#[test]
fn test_middle_truncate_very_small_budget() {
let text = "a".repeat(1000);
let result = middle_truncate(&text, 50);
assert!(result.contains("bytes omitted"));
}
#[test]
fn test_middle_truncate_preserves_head_and_tail() {
let text = format!(
"{}{}{}",
"HEAD_CONTENT_",
"x".repeat(10000),
"_TAIL_CONTENT"
);
let result = middle_truncate(&text, 500);
assert!(result.starts_with("HEAD_CONTENT_"));
assert!(result.ends_with("_TAIL_CONTENT"));
}
#[test]
fn test_sanitize_pipeline() {
let input = format!(
"\x1b[32mCompiling\x1b[0m foo\nProgress 50%\rProgress 100%\n{}",
"x".repeat(20000)
);
let result = sanitize_exec_output(&input, 500);
assert!(!result.contains("\x1b"));
assert!(!result.contains("Progress 50%"));
assert!(result.contains("Progress 100%"));
assert!(result.len() <= 500 + 80); }
#[test]
fn test_sanitize_small_output_unchanged() {
let input = "hello world";
assert_eq!(sanitize_exec_output(input, EXEC_OUTPUT_BUDGET), input);
}
#[test]
fn test_utf8_floor_ascii() {
assert_eq!(utf8_floor("hello", 3), 3);
}
#[test]
fn test_utf8_floor_multibyte() {
let text = "a€b"; assert_eq!(utf8_floor(text, 2), 1); assert_eq!(utf8_floor(text, 4), 4); }
#[test]
fn test_utf8_ceil_multibyte() {
let text = "a€b"; assert_eq!(utf8_ceil(text, 2), 4); }
#[test]
fn test_utf8_floor_beyond_len() {
assert_eq!(utf8_floor("abc", 100), 3);
}
#[test]
fn test_utf8_ceil_beyond_len() {
assert_eq!(utf8_ceil("abc", 100), 3);
}
#[test]
fn test_format_lines_basic() {
let (content, total, truncated) = format_lines("alpha\nbeta\ngamma", 0, 2000);
assert_eq!(content, "1|alpha\n2|beta\n3|gamma");
assert_eq!(total, 3);
assert!(!truncated);
}
#[test]
fn test_format_lines_with_offset() {
let (content, total, truncated) = format_lines("a\nb\nc\nd\ne", 2, 2);
assert_eq!(content, "3|c\n4|d");
assert_eq!(total, 5);
assert!(truncated);
}
#[test]
fn test_format_lines_offset_beyond_end() {
let (content, total, truncated) = format_lines("a\nb", 10, 5);
assert_eq!(content, "");
assert_eq!(total, 2);
assert!(!truncated);
}
#[test]
fn test_format_lines_limit_clips() {
let (content, total, truncated) = format_lines("a\nb\nc\nd\ne", 0, 3);
assert_eq!(content, "1|a\n2|b\n3|c");
assert_eq!(total, 5);
assert!(truncated);
}
#[test]
fn test_format_lines_empty_content() {
let (content, total, truncated) = format_lines("", 0, 2000);
assert_eq!(content, "");
assert_eq!(total, 0);
assert!(!truncated);
}
#[test]
fn test_format_lines_hard_byte_cap() {
let big_line = "x".repeat(1000);
let content = (0..100)
.map(|_| big_line.as_str())
.collect::<Vec<_>>()
.join("\n");
let (formatted, total, truncated) = format_lines(&content, 0, 100);
assert_eq!(total, 100);
assert!(truncated);
assert!(formatted.len() <= READ_FILE_HARD_BYTE_CAP);
assert!(formatted.is_char_boundary(formatted.len()));
}
#[test]
fn test_apply_read_file_hard_cap() {
let mut formatted = "x".repeat(READ_FILE_HARD_BYTE_CAP + 128);
let truncated = apply_read_file_hard_cap(&mut formatted);
assert!(truncated);
assert!(formatted.len() <= READ_FILE_HARD_BYTE_CAP);
assert!(formatted.is_char_boundary(formatted.len()));
}
#[test]
fn test_format_lines_single_line() {
let (content, total, truncated) = format_lines("hello", 0, 2000);
assert_eq!(content, "1|hello");
assert_eq!(total, 1);
assert!(!truncated);
}
#[test]
fn test_build_text_read_file_result_window() {
let result =
build_text_read_file_result("read_file", "/workspace/a.txt", "a\nb\nc", "text", 1, 1);
assert_eq!(result["content"], "2|b");
assert_eq!(result["total_lines"], 3);
assert_eq!(result["lines_shown"]["start"], 2);
assert_eq!(result["lines_shown"]["end"], 2);
assert_eq!(result["truncated"], true);
assert_eq!(result["truncation"]["next_offset"], 2);
}
#[test]
fn test_build_text_read_file_result_hard_cap_has_no_resume() {
let big_line = "x".repeat(READ_FILE_HARD_BYTE_CAP + 128);
let result =
build_text_read_file_result("read_file", "/workspace/big.txt", &big_line, "text", 0, 1);
assert_eq!(result["total_lines"], 1);
assert_eq!(result["lines_shown"]["start"], 1);
assert_eq!(result["lines_shown"]["end"], 1);
assert_eq!(result["truncated"], true);
assert_eq!(result["truncation"]["reason"], "size_cap");
assert!(result["truncation"].get("next_offset").is_none());
assert!(
result["content"].as_str().unwrap().len() <= READ_FILE_HARD_BYTE_CAP,
"content exceeded hard byte cap"
);
}
#[test]
fn test_build_bytes_read_file_result_omits_binary_content() {
let result = build_bytes_read_file_result(
"sandbox_read_file",
"/workspace/archive.zip",
&[0xff, 0x00, 0xfe],
0,
READ_FILE_DEFAULT_LIMIT,
);
assert_eq!(result["content_type"], "binary");
assert_eq!(result["encoding"], "binary");
assert_eq!(result["size_bytes"], 3);
assert_eq!(result["truncation"]["truncated"], false);
assert_eq!(result["truncation"]["bytes_returned"], 0);
assert_eq!(result["truncation"]["bytes_total"], 3);
assert!(result.get("content").is_none());
}
#[test]
fn test_parse_read_file_window_rejects_zero_limit() {
let err = parse_read_file_window_args(&serde_json::json!({"limit": 0})).unwrap_err();
assert_eq!(err, "limit must be a positive integer");
}
#[test]
fn test_priority_truncate_no_errors_falls_back_to_middle() {
let text = "a\n".repeat(5000);
let result = priority_aware_truncate(&text, 500);
let expected = middle_truncate(&text, 500);
assert_eq!(result, expected);
}
#[test]
fn test_priority_truncate_under_budget_unchanged() {
let text = "short output with error: something failed";
assert_eq!(priority_aware_truncate(text, 1024), text);
}
#[test]
fn test_priority_truncate_preserves_error_in_middle() {
let mut lines: Vec<String> = Vec::new();
for i in 0..100 {
lines.push(format!("Compiling dep-{}", i));
}
lines.push("error: mismatched types".to_string());
lines.push(" --> src/main.rs:42:5".to_string());
for i in 0..100 {
lines.push(format!("post-error output line {}", i));
}
let text = lines.join("\n");
let result = priority_aware_truncate(&text, 1000);
assert!(
result.contains("error: mismatched types"),
"error line must be preserved, got: {}",
result
);
assert!(
result.contains("src/main.rs:42:5"),
"error context must be preserved"
);
}
#[test]
fn test_priority_truncate_preserves_python_traceback() {
let mut lines: Vec<String> = Vec::new();
for i in 0..50 {
lines.push(format!("installing dep {}", i));
}
lines.push("Traceback (most recent call last):".to_string());
lines.push(" File \"test.py\", line 10, in <module>".to_string());
lines.push(" raise ValueError(\"bad\")".to_string());
lines.push("ValueError: bad".to_string());
for i in 0..50 {
lines.push(format!("cleanup line {}", i));
}
let text = lines.join("\n");
let result = priority_aware_truncate(&text, 800);
assert!(
result.contains("Traceback (most recent call last)"),
"Python traceback must be preserved"
);
}
#[test]
fn test_priority_truncate_preserves_panic() {
let mut lines: Vec<String> = Vec::new();
for _ in 0..80 {
lines.push("noise line".to_string());
}
lines.push("thread 'main' panicked at 'index out of bounds'".to_string());
for _ in 0..80 {
lines.push("more noise".to_string());
}
let text = lines.join("\n");
let result = priority_aware_truncate(&text, 600);
assert!(
result.contains("panicked at"),
"panic message must be preserved"
);
}
#[test]
fn test_priority_truncate_pytest_e_lines() {
let mut lines: Vec<String> = Vec::new();
for _ in 0..50 {
lines.push("collecting tests...".to_string());
}
lines.push("E AssertionError: expected 1, got 2".to_string());
for _ in 0..50 {
lines.push("test summary".to_string());
}
let text = lines.join("\n");
let result = priority_aware_truncate(&text, 600);
assert!(
result.contains("E AssertionError"),
"pytest E line must be preserved"
);
}
#[test]
fn test_priority_truncate_multiple_error_regions() {
let mut lines: Vec<String> = Vec::new();
for _ in 0..30 {
lines.push("compiling...".to_string());
}
lines.push("error: first error".to_string());
for _ in 0..30 {
lines.push("more compiling...".to_string());
}
lines.push("error: second error".to_string());
for _ in 0..30 {
lines.push("finishing...".to_string());
}
let text = lines.join("\n");
let result = priority_aware_truncate(&text, 1000);
assert!(result.contains("error: first error"));
assert!(result.contains("error: second error"));
}
#[test]
fn test_priority_truncate_omission_markers() {
let mut lines: Vec<String> = Vec::new();
for _ in 0..100 {
lines.push("x".repeat(20));
}
lines.push("FAILED test case".to_string());
for _ in 0..100 {
lines.push("y".repeat(20));
}
let text = lines.join("\n");
let result = priority_aware_truncate(&text, 800);
assert!(
result.contains("lines omitted")
|| result.contains("lines above")
|| result.contains("lines below"),
"must include omission markers"
);
}
#[test]
fn test_priority_truncate_respects_budget() {
let mut lines: Vec<String> = Vec::new();
for i in 0..500 {
lines.push(format!("line {} {}", i, "x".repeat(50)));
}
lines.push("error: something broke".to_string());
for i in 0..500 {
lines.push(format!("line {} {}", i + 500, "y".repeat(50)));
}
let text = lines.join("\n");
let budget = 2000;
let result = priority_aware_truncate(&text, budget);
assert!(
result.len() <= budget,
"result ({} bytes) must not exceed budget ({})",
result.len(),
budget
);
}
#[test]
fn test_find_error_regions_empty() {
let lines: Vec<&str> = vec!["hello", "world", "ok"];
assert!(find_error_regions(&lines).is_empty());
}
#[test]
fn test_find_error_regions_merges_nearby() {
let mut lines: Vec<&str> = vec!["ok"; 5];
lines.push("error: first");
lines.extend(std::iter::repeat_n("ok", 3));
lines.push("error: second"); lines.extend(std::iter::repeat_n("ok", 20));
let regions = find_error_regions(&lines);
assert_eq!(regions.len(), 1, "nearby errors should merge");
}
}