use super::*;
fn tid(n: u64) -> ToolCallId {
ToolCallId::new([n as u8; 12], n)
}
fn call(n: u64) -> ProviderCall {
ProviderCall {
tool_call_id: tid(n),
name: format!("t{n}"),
arguments: format!("{{\"n\":{n}}}"),
}
}
#[test]
fn open_boxes_and_real_attachments_round_trip() {
let mut chat = Chatend::new();
chat.accept_box(
"Future Kind".into(),
"opaque\ncontents".into(),
"future/v9".into(),
"hidden bytes".into(),
)
.unwrap();
chat.accept_attachment(
"Object ID: abc\nName: report.pdf".into(),
"k1.attachment/v1".into(),
"{\"mime\":\"application/pdf\"}".into(),
)
.unwrap();
let boxes = chat.boxes().to_vec();
assert_eq!(
(
boxes[0].id().get(),
boxes[0].box_type(),
boxes[0].contents()
),
(1, "Future Kind", "opaque\ncontents")
);
assert_eq!(
(boxes[0].hidden_type(), boxes[0].hidden_contents()),
("future/v9", "hidden bytes")
);
assert!(boxes[1].contents().contains("report.pdf"));
assert_eq!(Chatend::recover(boxes.clone()).unwrap().boxes(), boxes);
}
#[test]
fn stages_queue_flush_and_results_remain_transactional() {
let mut chat = Chatend::new();
chat.accept_system("prior".into()).unwrap();
assert_eq!(chat.start_round().unwrap().unwrap().get(), 1);
let dispatched = chat
.append_stage("thinking".into(), vec![call(1), call(2)])
.unwrap();
assert_eq!(
dispatched
.iter()
.map(|value| value.call_box_id.get())
.collect::<Vec<_>>(),
[3, 4]
);
let before = chat.boxes().to_vec();
assert_eq!(
chat.append_stage("bad".into(), vec![call(1)]),
Err(TransitionError::DuplicateToolCall)
);
assert_eq!(chat.boxes(), before);
chat.accept_user("queued".into()).unwrap();
chat.accept_async_return(tid(2), Ok("two".into())).unwrap();
let flushed = chat.flush_active_arrivals().unwrap();
assert_eq!(
flushed
.iter()
.map(|value| value.id().get())
.collect::<Vec<_>>(),
[5, 6]
);
assert_eq!(flushed[0].box_type(), USER_MESSAGE_TYPE);
assert_eq!(
flushed[1]
.tool_result_metadata()
.unwrap()
.unwrap()
.originating_call
.get(),
4
);
assert_eq!(
chat.accept_async_return(tid(2), Ok("again".into())),
Err(TransitionError::DuplicateReturn)
);
chat.done("final".into()).unwrap();
assert_eq!(chat.boxes().last().unwrap().box_type(), AGENT_MESSAGE_TYPE);
assert_eq!(
chat.accept_async_return(tid(9), Ok("x".into())),
Err(TransitionError::UnknownToolCall)
);
}
#[test]
fn recognized_tools_validate_but_unknown_conventions_are_inert() {
let call = call(7);
let call_box = tool_call_box(&call);
assert_eq!(call_box.tool_call_metadata().unwrap(), Some(call.clone()));
let result_box = tool_result_box(tid(7), BoxId::new(1), Err("failed\nfully".into()));
assert_eq!(
result_box.tool_result_metadata().unwrap().unwrap().result,
Err("failed\nfully".into())
);
let unknown = ChatBox::new(
BoxId::new(1),
"Tool Call".into(),
"visible".into(),
"future/tool".into(),
"anything".into(),
);
assert_eq!(
Chatend::recover(vec![unknown.clone()]).unwrap().boxes(),
&[unknown]
);
let malformed = ChatBox::new(
BoxId::new(1),
TOOL_CALL_TYPE.into(),
"visible".into(),
TOOL_CALL_HIDDEN_TYPE.into(),
"bad".into(),
);
assert!(matches!(
Chatend::recover(vec![malformed]),
Err(RecoveryError::MalformedToolConvention)
));
}
#[test]
fn recovery_enforces_order_and_exact_tool_correlation() {
let call_box = {
let mut value = tool_call_box(&call(1));
value.id = BoxId::new(1);
value
};
let result_box = {
let mut value = tool_result_box(tid(1), BoxId::new(1), Ok("yes".into()));
value.id = BoxId::new(2);
value
};
let mut recovered = Chatend::recover(vec![call_box.clone(), result_box.clone()]).unwrap();
assert_eq!(
recovered.accept_user("next".into()).unwrap().unwrap().get(),
3
);
let duplicate = {
let mut value = call_box.clone();
value.id = BoxId::new(2);
value
};
assert!(matches!(
Chatend::recover(vec![call_box.clone(), duplicate]),
Err(RecoveryError::DuplicateToolCall)
));
let wrong = {
let mut value = tool_result_box(tid(1), BoxId::new(2), Ok("x".into()));
value.id = BoxId::new(2);
value
};
assert!(matches!(
Chatend::recover(vec![call_box.clone(), wrong]),
Err(RecoveryError::WrongOriginatingCall)
));
let gap = ChatBox::new(
BoxId::new(2),
"Future".into(),
String::new(),
String::new(),
String::new(),
);
assert!(matches!(
Chatend::recover(vec![gap]),
Err(RecoveryError::NonContiguousBoxId)
));
}