use crate::config::ToolOutputBound;
use std::path::Path;
const STDERR_MARKER: &str = "--- stderr ---\n";
const FRAME_CLOSE: &str = "</file>\n";
const CONTEXT_FILE_LABEL: &str = "context file";
pub(in crate::prompt) fn frame_open(path: &Path) -> String {
format!("<file path=\"{}\">", path.display())
}
pub(in crate::prompt) fn context_file(
path: &Path,
bytes: &[u8],
bound: Option<ToolOutputBound>,
) -> Vec<u8> {
let body = super::bound::apply(bytes, CONTEXT_FILE_LABEL, bound, path);
let mut out = frame_open(path).into_bytes();
out.push(b'\n');
out.extend_from_slice(&body);
if !out.ends_with(b"\n") {
out.push(b'\n');
}
out.extend_from_slice(FRAME_CLOSE.as_bytes());
out
}
pub(super) fn render(exit_code: i32, stdout: &[u8], stderr: &[u8]) -> Vec<u8> {
let mut out = format!("Exit code: {exit_code}\n").into_bytes();
out.extend_from_slice(stdout);
if !stderr.is_empty() {
if !out.ends_with(b"\n") {
out.push(b'\n');
}
out.extend_from_slice(STDERR_MARKER.as_bytes());
out.extend_from_slice(stderr);
}
out
}
pub(crate) fn stated_exit_code(content: &[u8]) -> Option<i32> {
let line = content
.split(|byte| *byte == b'\n')
.next()
.unwrap_or_default();
std::str::from_utf8(line)
.ok()?
.strip_prefix("Exit code: ")?
.parse()
.ok()
}
#[cfg(test)]
mod tests {
use super::{context_file, frame_open, render, stated_exit_code};
use crate::config::ToolOutputBound;
use std::path::Path;
fn framed(path: &str, body: &str, bound: Option<ToolOutputBound>) -> String {
String::from_utf8(context_file(Path::new(path), body.as_bytes(), bound))
.expect("ASCII fixtures render as UTF-8")
}
#[test]
fn a_context_file_rides_in_the_path_frame() {
assert_eq!(
framed("/w/proj/AGENTS.md", "house rules\n", None),
"<file path=\"/w/proj/AGENTS.md\">\nhouse rules\n</file>\n"
);
assert_eq!(
frame_open(Path::new("/w/AGENTS.md")),
"<file path=\"/w/AGENTS.md\">"
);
}
#[test]
fn an_unterminated_file_gains_a_separator_before_the_close() {
assert_eq!(
framed("/w/AGENTS.md", "no newline", None),
"<file path=\"/w/AGENTS.md\">\nno newline\n</file>\n"
);
}
#[test]
fn an_oversized_context_file_is_bounded_as_its_own_stream() {
let bound = Some(ToolOutputBound {
head_bytes: 5,
tail_bytes: 5,
});
let out = framed("/w/AGENTS.md", "AAAA\nmiddle-middle\nZZZZ\n", bound);
assert!(
out.starts_with("<file path=\"/w/AGENTS.md\">\nAAAA\n[... context file truncated: ")
);
assert!(out.contains("full record: /w/AGENTS.md ...]\nZZZZ\n</file>\n"));
}
fn rendered(exit_code: i32, stdout: &str, stderr: &str) -> String {
String::from_utf8(render(exit_code, stdout.as_bytes(), stderr.as_bytes()))
.expect("ASCII fixtures render as UTF-8")
}
#[test]
fn success_states_the_code_and_carries_stdout() {
assert_eq!(rendered(0, "hello\n", ""), "Exit code: 0\nhello\n");
}
#[test]
fn stderr_survives_a_zero_exit() {
assert_eq!(
rendered(0, "built\n", "warning: deprecated\n"),
"Exit code: 0\nbuilt\n--- stderr ---\nwarning: deprecated\n"
);
}
#[test]
fn the_exit_code_distinguishes_failures() {
assert_eq!(
rendered(127, "", "sh: nope: not found\n"),
"Exit code: 127\n--- stderr ---\nsh: nope: not found\n"
);
assert_eq!(
rendered(1, "", "assertion failed\n"),
"Exit code: 1\n--- stderr ---\nassertion failed\n"
);
}
#[test]
fn unterminated_stdout_gains_a_separator_before_the_marker() {
assert_eq!(
rendered(0, "no trailing newline", "note\n"),
"Exit code: 0\nno trailing newline\n--- stderr ---\nnote\n"
);
}
#[test]
fn an_empty_stderr_emits_no_marker() {
let out = rendered(3, "partial", "");
assert_eq!(out, "Exit code: 3\npartial");
assert!(!out.contains("stderr"));
}
#[test]
fn a_silent_command_is_not_empty_content() {
assert_eq!(rendered(0, "", ""), "Exit code: 0\n");
}
#[test]
fn non_utf8_bytes_pass_through_untouched() {
let out = render(0, &[0xff, 0xfe], &[0x80]);
assert_eq!(out, b"Exit code: 0\n\xff\xfe\n--- stderr ---\n\x80");
}
#[test]
fn the_stated_code_reads_back_off_a_rendered_envelope() {
for code in [0, 3, 127, 143] {
let rendered = render(code, b"out", b"err");
assert_eq!(stated_exit_code(&rendered), Some(code));
}
}
#[test]
fn content_that_is_not_an_envelope_states_no_code() {
for content in ["", "hello\nworld", "Exit code: seven\n"] {
assert_eq!(stated_exit_code(content.as_bytes()), None, "{content:?}");
}
}
}