use super::*;
use serde_json::{Value, json};
fn provider(tool: &str) -> ProviderCall {
ProviderCall {
tool: tool.into(),
tool_version: "1.0.0".into(),
arguments: json!({"value": tool}),
}
}
fn success(call: &DispatchedToolCall) -> ToolResult {
ToolResult::new(
call.call.call_id(),
call.call_box_id,
call.call.tool().into(),
call.call.tool_version().into(),
ToolResultStatus::Ok,
json!({"accepted": true}),
ResultView::OneLine("success".into()),
)
.unwrap()
}
#[test]
fn allocates_provider_order_and_correlates_one_return() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
let calls = chat
.append_stage(
"thinking".into(),
vec![provider("First"), provider("Second")],
)
.unwrap();
assert_eq!(calls[0].call.call_id().to_string(), "c1");
assert_eq!(calls[1].call.call_id().to_string(), "c2");
assert_eq!(calls[0].call_box_id, BoxId::new(2));
assert_eq!(calls[1].call_box_id, BoxId::new(3));
assert_eq!(chat.accept_async_return(success(&calls[0])).unwrap(), None);
assert_eq!(
chat.accept_async_return(success(&calls[0])),
Err(TransitionError::DuplicateReturn)
);
let flushed = chat.flush_active_arrivals().unwrap();
assert_eq!(flushed[0].id(), BoxId::new(4));
assert_eq!(
flushed[0]
.tool_result_metadata()
.unwrap()
.unwrap()
.call_id(),
calls[0].call.call_id()
);
}
#[test]
fn rejected_stages_are_transactional_and_call_overflow_is_clean() {
let mut chat = Chatend::new();
assert_eq!(
chat.append_stage(String::new(), vec![provider("Tool")]),
Err(TransitionError::InvalidPhase)
);
chat.start_round().unwrap();
let invalid = ProviderCall {
tool: "Tool".into(),
tool_version: "1.0.0".into(),
arguments: json!({"tool": "forbidden"}),
};
assert_eq!(
chat.append_stage("must not append".into(), vec![invalid]),
Err(TransitionError::InvalidToolEnvelope)
);
assert!(chat.boxes().is_empty());
assert_eq!(
chat.append_stage(String::new(), vec![provider("Tool")])
.unwrap()[0]
.call
.call_id()
.to_string(),
"c1"
);
chat.next_call = None;
let before = chat.boxes().to_vec();
assert_eq!(
chat.append_stage(String::new(), vec![provider("Later")]),
Err(TransitionError::CallIdOverflow)
);
assert_eq!(chat.boxes(), before);
}
#[test]
fn active_arrivals_flush_fifo_and_done_closes_the_round() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
assert_eq!(chat.accept_user("first".into()).unwrap(), None);
assert_eq!(
chat.accept_box("Future".into(), "second".into(), "x".into(), "y".into())
.unwrap(),
None
);
let appended = chat.done("final".into()).unwrap();
assert_eq!(
appended
.iter()
.map(|value| (value.box_type(), value.contents()))
.collect::<Vec<_>>(),
vec![
(AGENT_MESSAGE_TYPE, "final"),
(USER_MESSAGE_TYPE, "first"),
("Future", "second"),
]
);
assert!(!chat.round_active());
assert_eq!(chat.done(String::new()), Err(TransitionError::InvalidPhase));
}
#[test]
fn recovery_validates_current_history_and_resumes_counter() {
let mut chat = Chatend::new();
chat.accept_box("Future".into(), "opaque".into(), "x".into(), "y".into())
.unwrap();
chat.start_round().unwrap();
let calls = chat
.append_stage(String::new(), vec![provider("One"), provider("Two")])
.unwrap();
chat.accept_async_return(success(&calls[1])).unwrap();
chat.done(String::new()).unwrap();
let mut recovered = Chatend::recover(chat.boxes().to_vec()).unwrap();
recovered.start_round().unwrap();
let next = recovered
.append_stage(String::new(), vec![provider("Three")])
.unwrap();
assert_eq!(next[0].call.call_id().to_string(), "c3");
assert_eq!(recovered.boxes()[0].box_type(), "Future");
}
#[test]
fn recovery_rejects_noncontiguous_ids_calls_and_malformed_envelopes() {
let gap = vec![ChatBox::new(
BoxId::new(2),
"Future".into(),
"opaque".into(),
String::new(),
String::new(),
)];
assert!(matches!(
Chatend::recover(gap),
Err(RecoveryError::NonContiguousBoxId)
));
let c2 = ToolCall::new(
ToolCallId::new(2).unwrap(),
"Tool".into(),
"1.0.0".into(),
json!({}),
)
.unwrap();
assert!(matches!(
Chatend::recover(vec![ChatBox::tool_call(BoxId::new(1), c2).unwrap()]),
Err(RecoveryError::InvalidCallSequence)
));
let malformed = ChatBox::new(
BoxId::new(1),
TOOL_CALL_TYPE.into(),
"opaque".into(),
"k1.tool-call/not-semver".into(),
"opaque".into(),
);
assert!(matches!(
Chatend::recover(vec![malformed]),
Err(RecoveryError::MalformedToolConvention)
));
}
#[test]
fn result_identity_must_match_without_mutation() {
let mut chat = Chatend::new();
chat.start_round().unwrap();
let call = chat
.append_stage(String::new(), vec![provider("Tool")])
.unwrap()
.remove(0);
let wrong = ToolResult::new(
call.call.call_id(),
call.call_box_id,
"Different".into(),
"1.0.0".into(),
ToolResultStatus::Error,
Value::Null,
ResultView::Error("failed".into()),
)
.unwrap();
let before = chat.boxes().to_vec();
assert_eq!(
chat.accept_async_return(wrong),
Err(TransitionError::MismatchedTool)
);
assert_eq!(chat.boxes(), before);
}