use agent_code_lib::llm::message::*;
use agent_code_lib::llm::normalize::*;
use uuid::Uuid;
fn assistant_text(text: &str) -> Message {
Message::Assistant(AssistantMessage {
uuid: Uuid::new_v4(),
timestamp: String::new(),
content: vec![ContentBlock::Text { text: text.into() }],
model: None,
usage: None,
stop_reason: None,
request_id: None,
})
}
fn assistant_with_tool_use(id: &str, name: &str) -> Message {
Message::Assistant(AssistantMessage {
uuid: Uuid::new_v4(),
timestamp: String::new(),
content: vec![ContentBlock::ToolUse {
id: id.into(),
name: name.into(),
input: serde_json::json!({}),
}],
model: None,
usage: None,
stop_reason: None,
request_id: None,
})
}
fn system_info(text: &str) -> Message {
Message::System(SystemMessage {
uuid: Uuid::new_v4(),
timestamp: String::new(),
subtype: SystemMessageType::Informational,
content: text.into(),
level: MessageLevel::Info,
})
}
fn user_with_empty_and_text() -> Message {
Message::User(UserMessage {
uuid: Uuid::new_v4(),
timestamp: String::new(),
content: vec![
ContentBlock::Text { text: "".into() },
ContentBlock::Text {
text: "real content".into(),
},
ContentBlock::Text { text: "".into() },
],
is_meta: false,
is_compact_summary: false,
})
}
fn user_empty() -> Message {
Message::User(UserMessage {
uuid: Uuid::new_v4(),
timestamp: String::new(),
content: vec![],
is_meta: false,
is_compact_summary: false,
})
}
fn user_with_document(data: &str, title: Option<&str>) -> Message {
Message::User(UserMessage {
uuid: Uuid::new_v4(),
timestamp: String::new(),
content: vec![ContentBlock::Document {
media_type: "application/pdf".into(),
data: data.into(),
title: title.map(String::from),
}],
is_meta: false,
is_compact_summary: false,
})
}
#[test]
fn user_assistant_user_alternation_is_valid() {
let messages = vec![
user_message("hello"),
assistant_text("hi there"),
user_message("how are you?"),
];
assert!(validate_alternation(&messages).is_ok());
}
#[test]
fn system_messages_are_ignored_in_alternation() {
let messages = vec![
system_info("session started"),
user_message("hello"),
system_info("context loaded"),
assistant_text("hi"),
];
assert!(validate_alternation(&messages).is_ok());
}
#[test]
fn consecutive_user_messages_fail_alternation() {
let messages = vec![user_message("one"), user_message("two")];
assert!(validate_alternation(&messages).is_err());
}
#[test]
fn consecutive_assistant_messages_fail_alternation() {
let messages = vec![
user_message("hello"),
assistant_text("hi"),
assistant_text("also hi"),
];
assert!(validate_alternation(&messages).is_err());
}
#[test]
fn orphaned_tool_use_gets_synthetic_result() {
let mut messages = vec![assistant_with_tool_use("call_99", "Bash")];
ensure_tool_result_pairing(&mut messages);
assert_eq!(messages.len(), 2);
if let Message::User(u) = &messages[1] {
assert_eq!(u.content.len(), 1);
if let ContentBlock::ToolResult {
tool_use_id,
is_error,
..
} = &u.content[0]
{
assert_eq!(tool_use_id, "call_99");
assert!(*is_error);
} else {
panic!("Expected ToolResult block");
}
} else {
panic!("Expected User message");
}
}
#[test]
fn paired_tool_use_does_not_add_synthetic_result() {
let mut messages = vec![
assistant_with_tool_use("call_1", "FileRead"),
tool_result_message("call_1", "file contents here", false),
];
let original_len = messages.len();
ensure_tool_result_pairing(&mut messages);
assert_eq!(messages.len(), original_len);
}
#[test]
fn multiple_orphaned_tool_uses_all_get_results() {
let mut messages = vec![Message::Assistant(AssistantMessage {
uuid: Uuid::new_v4(),
timestamp: String::new(),
content: vec![
ContentBlock::ToolUse {
id: "a".into(),
name: "Bash".into(),
input: serde_json::json!({}),
},
ContentBlock::ToolUse {
id: "b".into(),
name: "FileRead".into(),
input: serde_json::json!({}),
},
],
model: None,
usage: None,
stop_reason: None,
request_id: None,
})];
ensure_tool_result_pairing(&mut messages);
assert_eq!(messages.len(), 3);
}
#[test]
fn merge_consecutive_user_messages_combines_content() {
let mut messages = vec![
user_message("first"),
user_message("second"),
assistant_text("response"),
];
merge_consecutive_user_messages(&mut messages);
assert_eq!(messages.len(), 2);
if let Message::User(u) = &messages[0] {
assert_eq!(u.content.len(), 2);
assert_eq!(u.content[0].as_text(), Some("first"));
assert_eq!(u.content[1].as_text(), Some("second"));
} else {
panic!("Expected merged user message");
}
}
#[test]
fn merge_does_not_affect_properly_alternating_messages() {
let mut messages = vec![
user_message("hello"),
assistant_text("hi"),
user_message("bye"),
];
merge_consecutive_user_messages(&mut messages);
assert_eq!(messages.len(), 3);
}
#[test]
fn strip_empty_blocks_removes_empty_text_keeps_non_empty() {
let mut messages = vec![user_with_empty_and_text()];
strip_empty_blocks(&mut messages);
if let Message::User(u) = &messages[0] {
assert_eq!(u.content.len(), 1);
assert_eq!(u.content[0].as_text(), Some("real content"));
} else {
panic!("Expected User message");
}
}
#[test]
fn strip_empty_blocks_preserves_tool_use_blocks() {
let mut messages = vec![Message::Assistant(AssistantMessage {
uuid: Uuid::new_v4(),
timestamp: String::new(),
content: vec![
ContentBlock::Text { text: "".into() },
ContentBlock::ToolUse {
id: "t1".into(),
name: "Bash".into(),
input: serde_json::json!({}),
},
],
model: None,
usage: None,
stop_reason: None,
request_id: None,
})];
strip_empty_blocks(&mut messages);
if let Message::Assistant(a) = &messages[0] {
assert_eq!(a.content.len(), 1);
assert!(a.content[0].as_tool_use().is_some());
}
}
#[test]
fn remove_empty_messages_drops_contentless_messages() {
let mut messages = vec![
user_message("keep"),
user_empty(),
user_message("also keep"),
];
remove_empty_messages(&mut messages);
assert_eq!(messages.len(), 2);
}
#[test]
fn remove_empty_messages_keeps_system_messages() {
let mut messages = vec![system_info("info"), user_empty()];
remove_empty_messages(&mut messages);
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], Message::System(_)));
}
#[test]
fn cap_document_replaces_oversized_with_text() {
let mut messages = vec![user_with_document(&"x".repeat(2000), Some("huge.pdf"))];
cap_document_blocks(&mut messages, 500);
if let Message::User(u) = &messages[0] {
if let ContentBlock::Text { text } = &u.content[0] {
assert!(text.contains("huge.pdf"));
assert!(text.contains("too large"));
} else {
panic!("Expected Text block replacing document");
}
}
}
#[test]
fn cap_document_keeps_small_documents() {
let mut messages = vec![user_with_document("short", Some("small.pdf"))];
cap_document_blocks(&mut messages, 500);
if let Message::User(u) = &messages[0] {
assert!(matches!(u.content[0], ContentBlock::Document { .. }));
}
}
#[test]
fn messages_to_api_params_produces_correct_json_structure() {
let messages = vec![user_message("hi"), assistant_text("hello")];
let params = messages_to_api_params(&messages);
assert_eq!(params.len(), 2);
assert_eq!(params[0]["role"], "user");
assert_eq!(params[0]["content"], "hi");
assert_eq!(params[1]["role"], "assistant");
assert_eq!(params[1]["content"], "hello");
}
#[test]
fn messages_to_api_params_filters_out_system_messages() {
let messages = vec![
system_info("context loaded"),
user_message("hello"),
assistant_text("world"),
];
let params = messages_to_api_params(&messages);
assert_eq!(params.len(), 2);
assert_eq!(params[0]["role"], "user");
assert_eq!(params[1]["role"], "assistant");
}
#[test]
fn messages_to_api_params_includes_tool_use_blocks() {
let messages = vec![assistant_with_tool_use("c1", "Bash")];
let params = messages_to_api_params(&messages);
assert_eq!(params.len(), 1);
assert_eq!(params[0]["role"], "assistant");
let content = ¶ms[0]["content"];
assert!(content.is_array());
assert_eq!(content[0]["type"], "tool_use");
assert_eq!(content[0]["id"], "c1");
assert_eq!(content[0]["name"], "Bash");
}
#[test]
fn cached_params_marks_second_to_last_non_meta_user_message() {
let messages = vec![
user_message("first"),
assistant_text("r1"),
user_message("second"),
assistant_text("r2"),
user_message("third"),
];
let params = messages_to_api_params_cached(&messages);
assert_eq!(params.len(), 5);
assert_eq!(params[2]["role"], "user");
}
#[test]
fn cached_params_with_single_user_message_no_cache_mark() {
let messages = vec![user_message("only one")];
let params = messages_to_api_params_cached(&messages);
assert_eq!(params.len(), 1);
assert_eq!(params[0]["role"], "user");
}
#[test]
fn usage_merge_accumulates_output_replaces_input() {
let mut u1 = Usage {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
};
let u2 = Usage {
input_tokens: 200,
output_tokens: 30,
cache_creation_input_tokens: 10,
cache_read_input_tokens: 20,
};
u1.merge(&u2);
assert_eq!(u1.input_tokens, 200);
assert_eq!(u1.output_tokens, 80);
assert_eq!(u1.cache_creation_input_tokens, 10);
assert_eq!(u1.cache_read_input_tokens, 20);
}
#[test]
fn usage_total_sums_all_fields() {
let usage = Usage {
input_tokens: 10,
output_tokens: 20,
cache_creation_input_tokens: 5,
cache_read_input_tokens: 15,
};
assert_eq!(usage.total(), 50);
}
#[test]
fn usage_default_is_zero() {
let usage = Usage::default();
assert_eq!(usage.total(), 0);
assert_eq!(usage.input_tokens, 0);
assert_eq!(usage.output_tokens, 0);
}
#[test]
fn usage_merge_across_multiple_turns() {
let mut total = Usage::default();
let turn1 = Usage {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 20,
cache_read_input_tokens: 0,
};
total.merge(&turn1);
assert_eq!(total.output_tokens, 50);
let turn2 = Usage {
input_tokens: 150,
output_tokens: 40,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 20,
};
total.merge(&turn2);
assert_eq!(total.input_tokens, 150);
assert_eq!(total.output_tokens, 90); assert_eq!(total.cache_read_input_tokens, 20);
}