use super::*;
#[test]
fn paginate_read_caps_a_large_file_to_the_default_window() {
let body: String = (1..=15_057)
.map(|n| format!("line {n}"))
.collect::<Vec<_>>()
.join("\n");
let out = paginate_read(&body, None, None, DEFAULT_MAX_OUTPUT_TOKENS);
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines[0], "line 1");
assert_eq!(lines[1999], "line 2000");
assert!(
!out.contains("line 2001"),
"window stops at 2000: {:?}",
&out[..40]
);
assert!(out.contains("of 15057"), "footer names the total");
assert!(
out.contains("offset=2001"),
"footer points at the next window"
);
}
#[test]
fn paginate_read_offset_and_limit_return_just_that_window() {
let body: String = (1..=100)
.map(|n| format!("L{n}"))
.collect::<Vec<_>>()
.join("\n");
let out = paginate_read(&body, Some(10), Some(5), DEFAULT_MAX_OUTPUT_TOKENS);
assert!(out.starts_with("L10\nL11\nL12\nL13\nL14"), "{out:?}");
assert!(out.contains("offset=15"), "continues at line 15: {out:?}");
}
#[test]
fn paginate_read_small_file_is_returned_verbatim_without_a_footer() {
assert_eq!(
paginate_read("a\nb\nc\n", None, None, DEFAULT_MAX_OUTPUT_TOKENS),
"a\nb\nc\n"
);
}
#[test]
fn paginate_read_char_backstop_tracks_the_token_budget() {
let budget = 1_000;
let max_chars = crate::tokens::TokenEstimation::default().chars_for_tokens(budget);
let body = format!("{}\nshort second line", "x".repeat(50_000));
let out = paginate_read(&body, None, None, budget);
assert!(
out.len() < max_chars + 300,
"the oversized first line stays under the cap, not emitted whole: {} bytes",
out.len()
);
assert!(out.contains("truncated"), "marks the truncation");
assert!(
out.contains("~1000 tokens"),
"footer names the token budget: {out:?}"
);
assert!(
out.contains("offset=1 char_offset="),
"footer resumes MID-LINE via char_offset, not at the next line: {out:?}"
);
let many_lines = "y".repeat(30) + "\n";
let wide_body = many_lines.repeat(500);
let max_chars = crate::tokens::TokenEstimation::default().chars_for_tokens(budget);
let wide_out = paginate_read(&wide_body, None, None, budget);
assert!(
wide_out.len() < max_chars + 300,
"char-capped to the token budget (~{max_chars} chars): {} bytes",
wide_out.len()
);
}
#[test]
fn paginate_read_zero_budget_disables_the_char_backstop() {
let body = "y".repeat(500_000);
let out = paginate_read(&body, None, None, 0);
assert_eq!(out, body, "zero budget = no char backstop");
}
#[test]
fn paginate_read_offset_past_end_is_a_clear_message() {
let out = paginate_read("a\nb", Some(99), None, DEFAULT_MAX_OUTPUT_TOKENS);
assert!(out.contains("past end"), "{out:?}");
}
#[test]
fn cap_model_output_passes_small_output_through_unchanged() {
let small = "hello\nworld\n";
assert_eq!(cap_model_output(small, DEFAULT_MAX_OUTPUT_TOKENS), small);
}
#[test]
fn cap_model_output_preserves_existing_handle_for_short_view() {
let store = content_spill::SessionSpillStore::new([20u8; 16]);
let (handle, _) = content_spill::store_redacted_full(
"first diagnostic\nlast diagnostic\n",
Some("run_command".into()),
&store,
);
let handle = handle.unwrap();
let hint = content_spill::tool_output_retrieval_hint(&handle);
for budget in [0, DEFAULT_MAX_OUTPUT_TOKENS] {
let out = cap_model_output_with_handle("last diagnostic", budget, 100, Some(&handle));
assert!(out.starts_with("last diagnostic"), "{out}");
assert!(!out.contains("first diagnostic"), "{out}");
assert!(
out.contains(&hint),
"short view lost its retained source: {out}"
);
}
}
#[test]
fn cap_model_output_truncates_over_budget_as_head_tail() {
let big = format!("HEAD_MARKER\n{}\nTAIL_MARKER", "middle\n".repeat(20_000));
let out = cap_model_output_with_handle(&big, 1_000, 100, None);
assert!(out.len() < big.len(), "must shrink: {} bytes", out.len());
assert!(out.contains("HEAD_MARKER"), "head dropped: {out:?}");
assert!(out.contains("TAIL_MARKER"), "tail dropped: {out:?}");
assert!(out.contains("head+tail shown"), "marker present: {out:?}");
assert!(
!out.contains(&"middle\n".repeat(1_000)),
"middle should be elided"
);
}
#[test]
fn cap_model_output_truncates_at_a_char_boundary() {
let budget = 10; let body = "é".repeat(1_000); let out = cap_model_output(&body, budget);
assert!(out.is_char_boundary(out.len()), "valid boundary");
assert!(
out.chars()
.all(|c| c == 'é' || !c.is_control() || c == '\n'),
"no split char: {out:?}"
);
}
#[test]
fn cap_model_output_zero_budget_is_no_cap() {
let body = "z".repeat(500_000);
assert_eq!(cap_model_output(&body, 0), body);
}
#[test]
fn token_to_char_math_uses_the_default_four_chars_per_token() {
let est = crate::tokens::TokenEstimation::default();
assert_eq!(est.chars_for_tokens(DEFAULT_MAX_OUTPUT_TOKENS), 40_000);
}
#[test]
fn output_cap_sizes_at_the_conservative_ratio_not_the_estimate() {
let cap = crate::tokens::TokenEstimation::new(DEFAULT_OUTPUT_CAP_CHARS_PER_TOKEN);
assert_eq!(cap.chars_for_tokens(DEFAULT_MAX_OUTPUT_TOKENS), 30_000);
assert!(
cap.chars_for_tokens(DEFAULT_MAX_OUTPUT_TOKENS)
< crate::tokens::TokenEstimation::default().chars_for_tokens(DEFAULT_MAX_OUTPUT_TOKENS),
"cap must be tighter than the estimate"
);
}
#[test]
fn cap_model_output_caps_dense_body_the_estimate_would_pass() {
let body = "x".repeat(35_000);
let out = cap_model_output(&body, DEFAULT_MAX_OUTPUT_TOKENS);
assert!(
out.len() < body.len(),
"conservative cap must truncate a 35k-char body at a 10k-token budget \
(old 4-c/t backstop of 40k would have passed it); got {} bytes",
out.len()
);
assert!(
out.contains("head+tail shown"),
"cap marker present: {out:?}"
);
}
#[test]
fn pr_creation_url_extracts_github_and_gitlab() {
let github = "remote: Create a pull request for 'fix/foo' on GitHub by visiting:\n\
remote: https://github.com/OWNER/REPO/pull/new/fix/foo\n";
assert_eq!(
pr_creation_url(github),
Some("https://github.com/OWNER/REPO/pull/new/fix/foo")
);
let gitlab = "remote: To create a merge request for topic, visit:\n\
remote: https://gitlab.com/g/p/-/merge_requests/new?x=topic\n";
assert_eq!(
pr_creation_url(gitlab),
Some("https://gitlab.com/g/p/-/merge_requests/new?x=topic")
);
assert_eq!(pr_creation_url("Already up to date.\n"), None);
assert_eq!(
pr_creation_url("see https://github.com/OWNER/REPO/issues/1"),
None
);
}
#[test]
fn shell_envelope_output_appends_pr_hint_on_push() {
let push = serde_json::json!({
"exit_code": 0,
"stdout": "",
"stderr": "remote: Create a pull request for 'fix/foo' on GitHub by visiting:\n\
remote: https://github.com/OWNER/REPO/pull/new/fix/foo\n",
});
let out = shell_envelope_output(&push, 50, false, false, None, None);
assert!(out.contains("gh pr create --fill"), "hint missing: {out}");
assert!(
out.contains("https://github.com/OWNER/REPO/pull/new/fix/foo"),
"url dropped: {out}"
);
let plain = serde_json::json!({ "exit_code": 0, "stdout": "hello\n", "stderr": "" });
let out = shell_envelope_output(&plain, 50, false, false, None, None);
assert!(!out.contains("gh pr create"), "spurious hint: {out}");
assert_eq!(out, "hello\n");
}
#[test]
fn shell_envelope_output_spills_full_output_before_head_tail_cap() {
let full = format!(
"HEAD_ONLY_MARKER\n{}\nMIDDLE_ONLY_MARKER\n{}\nTAIL_ONLY_MARKER\n",
"alpha\n".repeat(10_000),
"omega\n".repeat(10_000)
);
let envelope = serde_json::json!({
"exit_code": 0,
"stdout": full,
"stderr": "",
});
let store = content_spill::SessionSpillStore::new([7u8; 16]);
let mut display = ToolDisplay::new(Vec::new(), false, 80, 3, false);
display.call("run_command", "large-output-command");
let out = shell_envelope_output(&envelope, 50, false, true, Some(&store), Some(&mut display));
display.result(&out);
assert!(out.contains("HEAD_ONLY_MARKER"), "head dropped: {out}");
assert!(out.contains("TAIL_ONLY_MARKER"), "tail dropped: {out}");
let handle = out
.split("spill:")
.nth(1)
.and_then(|s| s.split('"').next())
.expect("teaser names a spill handle");
let cid = content_spill::SpillCid::parse(handle).expect("handle is a canonical CID");
let arguments = serde_json::json!({"address": format!("spill:{handle}")});
assert!(
out.contains(&format!(
"memory_fetch tool with JSON arguments {arguments}"
)),
"retrieval tool and address parameter missing: {out}"
);
assert!(
out.contains("\"grep\":\"<pattern>\""),
"search affordance missing: {out}"
);
let stored = store.fetch(&cid).expect("full output stored").redacted_text;
assert!(
stored.contains("MIDDLE_ONLY_MARKER"),
"spilled payload was capped before storage"
);
assert!(stored.ends_with("TAIL_ONLY_MARKER\n"));
let rendered = String::from_utf8(display.into_inner()).unwrap();
assert!(
rendered.contains("▓ TAIL_ONLY_MARKER\n…\n"),
"operator spill lost the raw shell tail: {rendered}"
);
assert!(
!rendered.contains("memory_fetch tool"),
"operator saw the model teaser instead of raw shell output: {rendered}"
);
}
#[test]
fn shell_envelope_selected_view_retains_exact_full_stream_text() {
let stdout = "stdout α\r\nfirst diagnostic\n";
let stderr = "stderr β\r\nlast diagnostic\n";
let envelope = serde_json::json!({"exit_code": 7, "stdout": stdout, "stderr": stderr});
let store = content_spill::SessionSpillStore::new([21u8; 16]);
let mut handles = Vec::new();
for trim in [OutputTrim::Head(1), OutputTrim::Tail(1)] {
let view = trim.apply(stdout, stderr);
let out = shell::shell_envelope_output_with_view(
&envelope,
Some(&view),
20,
false,
true,
Some(&store),
None,
);
assert!(
out.starts_with(&format!("error: command exited 7\n{view}")),
"{out}"
);
let handle = out
.split("spill:")
.nth(1)
.and_then(|rest| rest.split('"').next())
.expect("selected view retains its full source");
let cid = content_spill::SpillCid::parse(handle).unwrap();
assert_eq!(
store.fetch(&cid).unwrap().redacted_text,
format!("{stdout}{stderr}")
);
handles.push(handle.to_owned());
}
assert_eq!(
handles[0], handles[1],
"a different view is not a different source"
);
}
#[test]
fn shell_envelope_capture_truncation_survives_output_caps_and_retrieval() {
for (stdout_truncated, stderr_truncated, streams) in [
(true, false, "stdout"),
(false, true, "stderr"),
(true, true, "stdout and stderr"),
] {
let store = content_spill::SessionSpillStore::new([22u8; 16]);
let envelope = serde_json::json!({
"exit_code": 0,
"stdout": "captured line\n".repeat(10_000),
"stderr": "last captured diagnostic\n",
"stdout_truncated": stdout_truncated,
"stderr_truncated": stderr_truncated,
});
let out = shell_envelope_output(&envelope, 20, false, true, Some(&store), None);
let notice = format!("{streams} capture truncated; omitted bytes are unavailable");
assert!(
out.contains(¬ice),
"capture loss must be explicit: {out}"
);
let handle = out
.split("spill:")
.nth(1)
.and_then(|rest| rest.split('"').next())
.expect("large capture has a retained-output handle");
let cid = content_spill::SpillCid::parse(handle).unwrap();
let retained = store.fetch(&cid).unwrap().redacted_text;
assert!(
retained.contains(¬ice),
"re-reading must preserve capture loss"
);
assert!(retained.contains("last captured diagnostic"));
}
}
#[test]
fn shell_envelope_capture_notice_survives_a_tiny_tail_budget() {
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"agentic::tools::tests::output_bounds::capture_notice_tiny_tail_child",
"--ignored",
"--nocapture",
])
.output()
.unwrap();
assert!(
output.status.success(),
"isolated output-cap test failed: {}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
String::from_utf8_lossy(&output.stdout).contains("1 passed"),
"the exact child test must run"
);
}
#[test]
#[ignore = "runs in an isolated child because output settings are process-wide"]
fn capture_notice_tiny_tail_child() {
output_budget::set_max_output_tokens(2);
output_budget::set_output_head_tokens(0);
let store = content_spill::SessionSpillStore::new([23u8; 16]);
let envelope = serde_json::json!({
"exit_code": 0,
"stdout": format!("{}tail42", "captured line\n".repeat(100)),
"stderr": "",
"stdout_truncated": true,
});
let out = shell_envelope_output(&envelope, 20, false, true, Some(&store), None);
assert!(out.contains("tail42"), "tail selection survives: {out}");
assert!(
out.contains("spill:"),
"retained source is discoverable: {out}"
);
assert!(
out.ends_with("[stdout capture truncated; omitted bytes are unavailable]"),
"capture loss survives even when the cap drops its original prefix: {out}"
);
}
#[test]
fn shell_envelope_without_streams_commits_the_exit_result() {
let envelope = serde_json::json!({
"exit_code": 3,
"stdout": "",
"stderr": "",
});
let mut display = ToolDisplay::new(Vec::new(), false, 80, 3, false);
display.call("run_command", "exit 3");
let out = shell_envelope_output(&envelope, 50, false, false, None, Some(&mut display));
display.result(&out);
assert_eq!(out, "error: command exited 3");
assert_eq!(
String::from_utf8(display.into_inner()).unwrap(),
"⚙ run_command: exit 3\n▒ error: command exited 3\n…\n"
);
}
#[test]
fn a_char_capped_page_ends_on_a_whole_line_and_names_the_exact_resume_offset() {
let body: String = (1..=3_000)
.map(|n| format!("line {n:05} {}", "y".repeat(30)))
.collect::<Vec<_>>()
.join("\n");
let out = paginate_read(&body, None, None, 1_000);
let (page, footer) = out
.rsplit_once("\n\n[")
.expect("a truncated page has a footer");
let last = page.lines().last().unwrap();
assert!(last.ends_with(&"y".repeat(30)), "no half line: {last:?}");
let shown: usize = last[5..10].parse().unwrap();
assert!(
footer.contains(&format!("offset={}", shown + 1)),
"resume exactly after line {shown}: {footer}"
);
}
#[test]
fn with_offload_on_a_read_file_page_stays_under_the_spill_cap() {
let body = (1..=5_000)
.map(|n| format!("line {n} {}", "z".repeat(40)))
.collect::<Vec<_>>()
.join("\n");
let cap = crate::agentic::content_spill::TOOL_RESULT_SPILL_CAP;
let on = read_file_page("", &body, None, None, None, true);
assert!(
on.chars().count() <= cap,
"{} chars would spill",
on.chars().count()
);
assert!(on.contains("offset="), "says how to continue");
let off = read_file_page("", &body, None, None, None, false);
assert!(
off.chars().count() > cap,
"offload off keeps the full budget"
);
}
#[test]
fn observed_report_envelope_tap_precedes_output_trimming() {
use crate::agentic::observed_report::{Capture, Presentation, State};
let dir = tempfile::tempdir().unwrap();
let capture = std::sync::Mutex::new(Capture::default());
let mut display = ToolDisplay::new(Vec::new(), false, 80, 20, false);
let mut view = Presentation {
inner: &mut display,
capture: Some(&capture),
};
view.execution_command("cargo test", dir.path().to_str().unwrap());
let stdout = "test result: ok. 1 passed; 0 failed\n".repeat(12);
let envelope =
serde_json::json!({"exit_code":101,"stdout":stdout,"stderr":"error: one target failed"});
shell::shell_envelope_output_with_view(
&envelope,
Some("short display"),
1,
false,
false,
None,
Some(&mut view),
);
let mut state = State::default();
state.bind(dir.path(), &crate::Scope::All);
state.observe(
dir.path(),
&crate::Scope::All,
&capture.lock().unwrap(),
Some(crate::ExecOutcome::Failed),
None,
);
let text = state.render(dir.path(), &crate::Scope::All, "done");
assert!(text.contains("Failed; exit 101"), "{text}");
assert!(text.contains("Result excerpt incomplete"), "{text}");
assert!(text.contains("error: one target failed"), "{text}");
}