use agent_code_lib::llm::message::*;
use agent_code_lib::services::shell_passthrough::*;
#[test]
fn shell_message_is_valid_user_message() {
let output = CapturedOutput {
text: "hello\n".to_string(),
truncated: false,
exit_code: Some(0),
};
let msg = build_context_message("echo hello", &output).unwrap();
assert!(matches!(msg, Message::User(_)));
if let Message::User(u) = &msg {
assert!(u.is_meta, "Shell output should be marked as meta");
assert!(!u.is_compact_summary);
assert!(!u.uuid.is_nil());
assert!(!u.timestamp.is_empty());
}
}
#[test]
fn shell_message_does_not_break_alternation() {
let output = CapturedOutput {
text: "test output\n".to_string(),
truncated: false,
exit_code: Some(0),
};
let shell_msg = build_context_message("ls", &output).unwrap();
let messages = [
user_message("first question"),
Message::Assistant(AssistantMessage {
uuid: uuid::Uuid::new_v4(),
timestamp: String::new(),
content: vec![ContentBlock::Text {
text: "answer".into(),
}],
model: None,
usage: None,
stop_reason: None,
request_id: None,
}),
shell_msg,
];
assert_eq!(messages.len(), 3);
if let Message::User(u) = &messages[2] {
assert!(u.is_meta);
} else {
panic!("Third message should be User (meta)");
}
}
#[test]
fn shell_message_serialization_roundtrip() {
let output = CapturedOutput {
text: "line 1\nline 2\n".to_string(),
truncated: false,
exit_code: Some(0),
};
let msg = build_context_message("test-cmd", &output).unwrap();
let json = serde_json::to_string(&msg).unwrap();
let deserialized: Message = serde_json::from_str(&json).unwrap();
if let Message::User(u) = &deserialized {
assert!(u.is_meta);
assert_eq!(u.content.len(), 1);
if let ContentBlock::Text { text } = &u.content[0] {
assert!(text.contains("[Shell output from: test-cmd]"));
assert!(text.contains("line 1\nline 2"));
} else {
panic!("Expected Text block after deserialization");
}
} else {
panic!("Expected User message after deserialization");
}
}
#[test]
#[cfg_attr(target_os = "windows", ignore = "requires bash")]
fn subprocess_respects_working_directory() {
let tmp = tempfile::tempdir().unwrap();
let marker_path = tmp.path().join("cwd_test.txt");
std::fs::write(&marker_path, "cwd_verified").unwrap();
let result = run_and_capture("cat cwd_test.txt", tmp.path(), |_| {}, |_| {}).unwrap();
assert_eq!(result.text.trim(), "cwd_verified");
assert_eq!(result.exit_code, Some(0));
}
#[test]
#[cfg_attr(target_os = "windows", ignore = "requires bash")]
fn subprocess_captures_exit_codes() {
let dir = std::env::temp_dir();
let r = run_and_capture("true", &dir, |_| {}, |_| {}).unwrap();
assert_eq!(r.exit_code, Some(0));
let r = run_and_capture("false", &dir, |_| {}, |_| {}).unwrap();
assert_eq!(r.exit_code, Some(1));
let r = run_and_capture("exit 137", &dir, |_| {}, |_| {}).unwrap();
assert_eq!(r.exit_code, Some(137));
}
#[test]
#[cfg_attr(target_os = "windows", ignore = "requires bash")]
fn subprocess_handles_binary_output_gracefully() {
let dir = std::env::temp_dir();
let result = run_and_capture("printf 'hello\\x00world'", &dir, |_| {}, |_| {});
assert!(result.is_ok());
}
#[test]
#[cfg_attr(target_os = "windows", ignore = "requires bash")]
fn subprocess_captures_long_running_output() {
let dir = std::env::temp_dir();
let result = run_and_capture("seq 1 500", &dir, |_| {}, |_| {}).unwrap();
let lines: Vec<&str> = result.text.lines().collect();
assert_eq!(lines.len(), 500);
assert_eq!(lines[0], "1");
assert_eq!(lines[499], "500");
assert!(!result.truncated);
}
#[test]
#[cfg_attr(target_os = "windows", ignore = "requires bash")]
fn subprocess_truncation_preserves_complete_lines() {
let dir = std::env::temp_dir();
let result = run_and_capture(
"for i in $(seq 1 600); do printf '%0100d\\n' $i; done",
&dir,
|_| {},
|_| {},
)
.unwrap();
assert!(result.truncated);
for line in result.text.lines() {
assert_eq!(line.len(), 100, "Line should be exactly 100 chars: {line}");
}
}
#[test]
fn context_message_header_format() {
let output = CapturedOutput {
text: "output\n".to_string(),
truncated: false,
exit_code: Some(0),
};
let msg = build_context_message("cargo test --release", &output).unwrap();
if let Message::User(u) = &msg {
if let ContentBlock::Text { text } = &u.content[0] {
let first_line = text.lines().next().unwrap();
assert_eq!(first_line, "[Shell output from: cargo test --release]");
let rest: String = text.lines().skip(1).collect::<Vec<_>>().join("\n");
assert!(rest.contains("output"));
} else {
panic!("Expected Text block");
}
} else {
panic!("Expected User message");
}
}
#[test]
fn context_message_truncation_suffix_is_last_line() {
let output = CapturedOutput {
text: "some output\n".to_string(),
truncated: true,
exit_code: Some(0),
};
let msg = build_context_message("cmd", &output).unwrap();
if let Message::User(u) = &msg {
if let ContentBlock::Text { text } = &u.content[0] {
let last_line = text.lines().last().unwrap();
assert_eq!(last_line, "[output truncated at 50KB]");
} else {
panic!("Expected Text block");
}
} else {
panic!("Expected User message");
}
}
#[test]
fn context_message_preserves_special_characters() {
let output = CapturedOutput {
text: "café résumé naïve\n\"quoted\" & <html>\n".to_string(),
truncated: false,
exit_code: Some(0),
};
let msg = build_context_message("echo special", &output).unwrap();
if let Message::User(u) = &msg {
if let ContentBlock::Text { text } = &u.content[0] {
assert!(text.contains("café résumé naïve"));
assert!(text.contains("\"quoted\" & <html>"));
} else {
panic!("Expected Text block");
}
} else {
panic!("Expected User message");
}
}