use super::*;
fn call(value: u8) -> ProviderCall {
ProviderCall {
tool_call_id: ToolCallId::new([value; 12], u64::from(value)),
name: "WebSearch".into(),
arguments: "{}".into(),
}
}
#[test]
fn empty_response_is_appended_at_call_wave_and_done() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
let dispatched = chat
.append_stage(String::new(), vec![ProviderGenerated::ToolCall(call(1))])
.unwrap();
assert_eq!(chat.boxes()[0].box_type(), AGENT_RESPONSE_TYPE);
assert_eq!(chat.boxes()[0].contents(), "");
assert_eq!(dispatched[0].call_box_id, BoxId::new(2));
assert!(chat.flush_active_arrivals().unwrap().is_empty());
let appended = chat.done(String::new()).unwrap();
assert_eq!(appended[0].id(), BoxId::new(3));
assert_eq!(appended[0].box_type(), AGENT_RESPONSE_TYPE);
assert_eq!(appended[0].contents(), "");
}
#[test]
fn promised_response_ids_precede_active_arrivals_at_stage_and_done() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
assert_eq!(chat.accept_user("during stage".into()).unwrap(), None);
chat.append_stage("first".into(), vec![]).unwrap();
assert_eq!(chat.boxes()[0].id(), BoxId::new(1));
let flushed = chat.flush_active_arrivals().unwrap();
assert_eq!(flushed[0].id(), BoxId::new(2));
assert_eq!(chat.accept_user("during done".into()).unwrap(), None);
let done = chat.done("last".into()).unwrap();
assert_eq!(done[0].id(), BoxId::new(3));
assert_eq!(done[0].box_type(), AGENT_RESPONSE_TYPE);
assert_eq!(done[1].id(), BoxId::new(4));
assert_eq!(done[1].contents(), "during done");
}
#[test]
fn mixed_provider_values_keep_order_and_dispatch_only_calls() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
let dispatched = chat
.append_stage(
"response".into(),
vec![
ProviderGenerated::AgentMessage {
contents: "notice one".into(),
},
ProviderGenerated::ToolCall(call(1)),
ProviderGenerated::AgentMessage {
contents: "notice two".into(),
},
ProviderGenerated::ToolCall(call(2)),
],
)
.unwrap();
assert_eq!(
chat.boxes()
.iter()
.map(ChatBox::box_type)
.collect::<Vec<_>>(),
vec![
AGENT_RESPONSE_TYPE,
AGENT_MESSAGE_TYPE,
TOOL_CALL_TYPE,
AGENT_MESSAGE_TYPE,
TOOL_CALL_TYPE,
]
);
assert_eq!(dispatched.len(), 2);
assert_eq!(dispatched[0].call_box_id, BoxId::new(3));
assert_eq!(dispatched[1].call_box_id, BoxId::new(5));
}
#[test]
fn invalid_flush_before_stage_is_transactional() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
chat.accept_user("queued".into()).unwrap();
assert!(matches!(
chat.flush_active_arrivals(),
Err(TransitionError::InvalidPhase)
));
assert!(chat.boxes().is_empty());
let appended = chat.done("response".into()).unwrap();
assert_eq!(appended.len(), 2);
assert_eq!(appended[0].box_type(), AGENT_RESPONSE_TYPE);
assert_eq!(appended[1].contents(), "queued");
}
#[test]
fn marker_only_flush_reenters_generating() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
chat.append_stage("one".into(), vec![]).unwrap();
assert!(matches!(
chat.append_stage("invalid".into(), vec![]),
Err(TransitionError::InvalidPhase)
));
assert!(chat.flush_active_arrivals().unwrap().is_empty());
chat.append_stage("two".into(), vec![]).unwrap();
assert_eq!(chat.boxes()[1].contents(), "two");
}
#[test]
fn abort_preserves_arrivals_without_fabricating_a_response() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
chat.accept_user("first".into()).unwrap();
assert_eq!(chat.abort().unwrap()[0].contents(), "first");
assert!(!chat.round_active());
chat.start_round().unwrap();
chat.append_stage("only response".into(), vec![]).unwrap();
chat.accept_user("second".into()).unwrap();
assert_eq!(chat.abort().unwrap()[0].contents(), "second");
assert_eq!(
chat.boxes()
.iter()
.filter(|value| value.box_type() == AGENT_RESPONSE_TYPE)
.count(),
1
);
}
#[test]
fn duplicate_generated_calls_fail_without_mutation() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
assert!(matches!(
chat.append_stage(
"discarded".into(),
vec![
ProviderGenerated::ToolCall(call(3)),
ProviderGenerated::ToolCall(call(3)),
],
),
Err(TransitionError::DuplicateToolCall)
));
assert!(chat.boxes().is_empty());
assert_eq!(chat.done(String::new()).unwrap()[0].id(), BoxId::new(1));
}
#[test]
fn correlated_message_and_v2_result_recover() {
let id = ToolCallId::new([9; 12], 9);
let mut chat = Chatend::new();
chat.start_round().unwrap();
chat.append_stage(String::new(), vec![ProviderGenerated::ToolCall(call(9))])
.unwrap();
chat.accept_tool_message(id, "working".into()).unwrap();
chat.accept_async_return_v2(
id,
Ok("done".into()),
"k1.web-search-result/v1".into(),
"{}".into(),
)
.unwrap();
assert_eq!(chat.flush_active_arrivals().unwrap().len(), 2);
assert_eq!(
chat.accept_tool_message(id, "late".into()),
Err(TransitionError::ToolMessageAfterResult)
);
chat.abort().unwrap();
let recovered = Chatend::recover(chat.boxes().to_vec()).unwrap();
assert!(!recovered.round_active());
}
#[test]
fn recovery_preserves_historical_agent_message_and_unknown_box() {
let original = vec![
ChatBox::new(
BoxId::new(1),
AGENT_MESSAGE_TYPE.into(),
"historical".into(),
String::new(),
String::new(),
),
ChatBox::new(
BoxId::new(2),
"Future Provider Value".into(),
"opaque".into(),
"future/v1".into(),
"bytes".into(),
),
];
let recovered = Chatend::recover(original.clone()).unwrap();
assert_eq!(recovered.boxes(), original);
assert!(!recovered.round_active());
}