use serde::{Deserialize, Serialize};
use super::{Message, Text, ToolResultContent};
mod vec_content_serde {
use super::super::{AssistantContent, Message, UserContent};
#[test]
fn message_content_still_serializes_as_a_plain_sequence() {
let message = Message::User {
content: vec![UserContent::text("hi")],
};
let json = serde_json::to_value(&message).expect("serialize");
assert_eq!(
json,
serde_json::json!({
"role": "user",
"content": [{"type": "text", "text": "hi"}],
})
);
}
#[test]
fn message_content_round_trips_byte_identically() {
let message = Message::Assistant(crate::message::AssistantMessage::new(vec![
AssistantContent::text("hello"),
]));
let encoded = serde_json::to_string(&message).expect("serialize");
let decoded: Message = serde_json::from_str(&encoded).expect("deserialize");
assert_eq!(
serde_json::to_string(&decoded).expect("re-serialize"),
encoded
);
}
#[test]
fn an_empty_content_array_deserializes() {
for message in [
serde_json::json!({"role": "user", "content": []}),
serde_json::json!({"role": "assistant", "content": []}),
] {
let decoded = serde_json::from_value::<Message>(message.clone()).expect("parses");
assert_eq!(serde_json::to_value(&decoded).expect("serializes"), message);
}
}
}
#[test]
fn system_message_constructor_and_serde_roundtrip() {
let message = Message::system("You are concise.");
match &message {
Message::System { content } => assert_eq!(content, "You are concise."),
_ => panic!("Expected system message"),
}
let json = serde_json::to_string(&message).expect("serialize");
let roundtrip: Message = serde_json::from_str(&json).expect("deserialize");
assert_eq!(roundtrip, message);
}
#[test]
fn a_rig_issued_call_id_round_trips_as_local() {
let call = super::ToolCall::from_wire(
"",
super::ToolFunction::new(
super::ToolName::new("add").expect("tool name"),
serde_json::json!({}),
),
);
assert!(call.id.is_local());
let json = serde_json::to_value(&call).expect("serialize");
let roundtrip: super::ToolCall = serde_json::from_value(json).expect("deserialize");
assert!(roundtrip.id.provider().is_none());
assert_eq!(roundtrip, call);
}
#[test]
fn legacy_call_id_key_cannot_recover_an_untagged_identity() {
let legacy = serde_json::json!({
"id": "fc_123",
"call_id": "call_abc",
"function": {"name": "add", "arguments": {"x": 1}},
});
assert!(serde_json::from_value::<super::ToolCall>(legacy).is_err());
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct ExecutorLikeResponse {
output: serde_json::Value,
logs: Vec<String>,
execution_time_ms: u64,
}
#[test]
fn tool_result_content_decodes_structured_and_legacy_json() {
let response = ExecutorLikeResponse {
output: serde_json::json!({"answer": 42}),
logs: vec!["computed".to_string()],
execution_time_ms: 7,
};
let value = serde_json::to_value(&response).expect("serialize response");
let structured = ToolResultContent::json(value.clone());
assert_eq!(structured.as_json(), Some(&value));
assert_eq!(structured.as_text(), None);
assert_eq!(
structured
.deserialize_json::<ExecutorLikeResponse>()
.expect("decode structured response"),
response
);
let legacy_json = value.to_string();
let legacy_text = ToolResultContent::Text(Text::new(legacy_json.clone()));
assert_eq!(legacy_text.as_text(), Some(legacy_json.as_str()));
assert_eq!(legacy_text.as_json(), None);
assert_eq!(
legacy_text
.deserialize_json::<ExecutorLikeResponse>()
.expect("decode legacy response"),
response
);
let image = ToolResultContent::image_url("https://example.com/result.png", None, None);
let image_error = image.deserialize_json::<ExecutorLikeResponse>();
assert!(image_error.is_err());
if let Err(error) = image_error {
assert_eq!(
error.to_string(),
"cannot decode image tool-result content as JSON"
);
}
}
#[test]
fn the_legacy_tool_call_id_shape_does_not_parse() {
let legacy = serde_json::json!({
"id": {"origin": "explicit", "id": "call_1"},
"function": {"name": "add", "arguments": {}},
});
assert!(serde_json::from_value::<super::ToolCall>(legacy).is_err());
assert!(serde_json::from_str::<super::CallId>(r#""call_1""#).is_err());
}
#[test]
fn a_stored_call_with_any_arguments_shape_loads() {
let stored = serde_json::json!({"name": "search", "arguments": null});
let function: super::ToolFunction =
serde_json::from_value(stored).expect("a stored call loads");
assert_eq!(function.arguments_value(), serde_json::json!({}));
let stored = serde_json::json!({"name": "search", "arguments": "{\"q\":1}"});
let function: super::ToolFunction =
serde_json::from_value(stored).expect("a stored call loads");
assert_eq!(function.arguments_value(), serde_json::json!({"q": 1}));
}
#[test]
fn an_unanswered_turn_fails_its_run_only_when_cut_or_failed() {
use super::{AssistantContent, StopReason, turn_failure};
use crate::completion::FinishReason;
let empty: Vec<AssistantContent> = vec![AssistantContent::reasoning("thinking")];
let answered = vec![AssistantContent::text("hi")];
let failed = StopReason::Error("refused".into());
assert_eq!(
turn_failure(
&empty,
Some(&StopReason::Length),
Some(&FinishReason::Length)
),
Some(FinishReason::Length.no_answer_message())
);
assert!(turn_failure(&empty, Some(&failed), None).is_some_and(|m| m.contains("refused")));
assert_eq!(
turn_failure(&empty, Some(&StopReason::Stop), Some(&FinishReason::Stop)),
None
);
assert_eq!(
turn_failure(
&answered,
Some(&StopReason::Length),
Some(&FinishReason::Length)
),
None
);
}
#[test]
fn a_failed_turn_with_an_answer_fails_its_run() {
use super::{AssistantContent, StopReason, turn_failure};
use crate::completion::FinishReason;
let answered = vec![AssistantContent::text("hi")];
let failed = StopReason::Error("Provider finish_reason: weird".into());
let message = turn_failure(
&answered,
Some(&failed),
Some(&FinishReason::Other("weird".into())),
)
.expect("the run fails");
assert_eq!(
message,
"the provider failed the turn: Provider finish_reason: weird"
);
let filtered = StopReason::Error("Provider finish_reason: content_filter".into());
assert!(
turn_failure(
&answered,
Some(&filtered),
Some(&FinishReason::ContentFilter)
)
.is_some_and(|message| message.contains("content_filter"))
);
let aborted = StopReason::Aborted("the caller stopped".into());
assert_eq!(turn_failure(&answered, Some(&aborted), None), None);
}
#[test]
fn a_failed_turn_with_calls_fails_its_run() {
use super::{AssistantContent, StopReason, ToolName, turn_failure};
use crate::completion::FinishReason;
let call = AssistantContent::tool_call(
"call_1",
ToolName::new("lookup").expect("a tool name"),
serde_json::json!({}),
);
let turn = vec![AssistantContent::text("checking"), call];
for stop in [
StopReason::Error("Provider finish_reason: pause".into()),
StopReason::Aborted("the stream ended".into()),
] {
let message = turn_failure(&turn, Some(&stop), None).expect("the run fails");
assert!(message.contains("none of its tool calls ran"), "{message}");
}
for (stop, finish) in [
(StopReason::Length, FinishReason::Length),
(StopReason::ToolUse, FinishReason::ToolCalls),
] {
assert_eq!(turn_failure(&turn, Some(&stop), Some(&finish)), None);
}
}
#[test]
fn redacted_reasoning_without_its_item_is_blank() {
use super::{AssistantContent, Reasoning};
let redacted = AssistantContent::Reasoning(Reasoning {
text: "kept".to_owned(),
redacted: true,
native: None,
});
assert!(redacted.is_blank());
let with_item =
redacted.with_native(serde_json::json!({"type": "redacted_thinking", "data": "x"}));
assert!(!with_item.is_blank());
}