use crate::{ActorState, ChatBox, ProviderCall, StateError, ToolCallId, USER_ATTACHMENT_TYPE};
fn id(n: u64) -> ToolCallId {
ToolCallId::new([n as u8; 12], n)
}
fn call(n: u64) -> ProviderCall {
ProviderCall {
tool_call_id: id(n),
name: format!("t{n}"),
arguments: "{}".into(),
}
}
#[test]
fn begin_adds_no_box() {
let mut state = ActorState::new(true);
let start = state.begin_inference().unwrap().unwrap();
assert_eq!(start.frontier, None);
assert!(state.boxes().is_empty());
state.complete_inference(start.job, String::new()).unwrap();
}
#[test]
fn stages_are_canonical_and_launchable_before_done() {
let mut state = ActorState::new(true);
let start = state.begin_inference().unwrap().unwrap();
let first = state
.append_stage(start.job, "a".into(), vec![call(1), call(2)])
.unwrap();
let second = state
.append_stage(start.job, String::new(), vec![call(3)])
.unwrap();
assert_eq!(first.len(), 2);
assert_eq!(second[0].tool_call_id, id(3));
assert_eq!(state.boxes()[0].box_type(), "Agent Message");
assert_eq!(state.boxes()[0].contents(), "a");
assert_eq!(state.boxes()[1].box_type(), "Tool Call");
state.complete_inference(start.job, "z".into()).unwrap();
assert_eq!(state.boxes().last().unwrap().contents(), "z");
}
#[test]
fn generic_unknown_boxes_survive_flush_completion_and_recovery() {
let mut state = ActorState::new(true);
let start = state.begin_inference().unwrap().unwrap();
state
.accept_box(
"Future Kind".into(),
"visible payload".into(),
"future/v9".into(),
"hidden payload".into(),
)
.unwrap();
let flushed = state.flush_active_arrivals(start.job).unwrap();
assert_eq!(flushed.len(), 1);
assert_eq!(flushed[0].box_type(), "Future Kind");
assert_eq!(flushed[0].hidden_contents(), "hidden payload");
state
.accept_box(
"Later Future".into(),
"later visible".into(),
"future/v10".into(),
"later hidden".into(),
)
.unwrap();
state.complete_inference(start.job, String::new()).unwrap();
let boxes = state.boxes().to_vec();
assert_eq!(boxes[1].box_type(), "Later Future");
assert_eq!(
ActorState::recover(boxes.clone(), false).unwrap().boxes(),
boxes
);
}
#[test]
fn attachments_require_real_open_fields() {
let mut state = ActorState::new(false);
state
.accept_attachment(
"Object ID: abc\nName: report.pdf".into(),
"k1.attachment/v1".into(),
"{\"mime\":\"application/pdf\"}".into(),
)
.unwrap();
let attachment = &state.boxes()[0];
assert_eq!(attachment.box_type(), USER_ATTACHMENT_TYPE);
assert!(attachment.contents().contains("report.pdf"));
assert_eq!(attachment.hidden_type(), "k1.attachment/v1");
}
#[test]
fn queued_arrivals_are_fifo_and_schedule_one_followup() {
let mut state = ActorState::new(true);
let first = state.begin_inference().unwrap().unwrap();
state.accept_system("s".into()).unwrap();
state.accept_user("u".into()).unwrap();
state
.accept_attachment("a".into(), "attachment/v1".into(), "metadata".into())
.unwrap();
state.complete_inference(first.job, String::new()).unwrap();
assert_eq!(state.boxes()[0].box_type(), "System Message");
assert_eq!(state.boxes()[1].box_type(), "User Message");
assert_eq!(state.boxes()[2].contents(), "a");
let followup = state.begin_inference().unwrap().unwrap();
assert!(state.begin_inference().unwrap().is_none());
state
.complete_inference(followup.job, String::new())
.unwrap();
assert!(state.quiet());
}
#[test]
fn flush_is_fifo_keeps_round_active_and_prevents_duplicate_followup() {
let mut state = ActorState::new(true);
let start = state.begin_inference().unwrap().unwrap();
state
.append_stage(start.job, String::new(), vec![call(1)])
.unwrap();
state.accept_user("u".into()).unwrap();
state.accept_async_return(id(1), Ok("r".into())).unwrap();
let arrivals = state.flush_active_arrivals(start.job).unwrap();
assert_eq!(arrivals.len(), 2);
assert_eq!(arrivals[0].box_type(), "User Message");
assert_eq!(arrivals[1].box_type(), "Tool Result");
assert_eq!(arrivals[1].id().get(), arrivals[0].id().get() + 1);
assert!(state.flush_active_arrivals(start.job).unwrap().is_empty());
state
.append_stage(start.job, "continued".into(), vec![])
.unwrap();
state.complete_inference(start.job, String::new()).unwrap();
assert!(state.quiet());
}
#[test]
fn flush_with_wrong_job_is_transactional() {
let mut state = ActorState::new(true);
let start = state.begin_inference().unwrap().unwrap();
state.accept_user("queued".into()).unwrap();
assert!(matches!(
state.flush_active_arrivals(start.job + 1),
Err(StateError::WrongInference { .. })
));
assert!(state.boxes().is_empty());
assert_eq!(state.flush_active_arrivals(start.job).unwrap().len(), 1);
}
#[test]
fn arrival_after_flush_restores_exactly_one_followup() {
let mut state = ActorState::new(true);
let start = state.begin_inference().unwrap().unwrap();
state.accept_user("first".into()).unwrap();
assert_eq!(state.flush_active_arrivals(start.job).unwrap().len(), 1);
state.accept_system("later".into()).unwrap();
state.complete_inference(start.job, String::new()).unwrap();
assert_eq!(state.boxes().last().unwrap().contents(), "later");
let followup = state.begin_inference().unwrap().unwrap();
assert!(state.begin_inference().unwrap().is_none());
state
.complete_inference(followup.job, String::new())
.unwrap();
assert!(state.quiet());
}
#[test]
fn call_only_stage_does_not_schedule_and_returns_are_open_format() {
let mut state = ActorState::new(true);
let start = state.begin_inference().unwrap().unwrap();
assert_eq!(
state
.append_stage(start.job, String::new(), vec![call(1)])
.unwrap()
.len(),
1
);
state.complete_inference(start.job, String::new()).unwrap();
assert!(state.quiet());
state
.accept_async_return(id(1), Ok("result".into()))
.unwrap();
assert_eq!(state.boxes().last().unwrap().box_type(), "Tool Result");
let followup = state.begin_inference().unwrap().unwrap();
state
.complete_inference(followup.job, String::new())
.unwrap();
assert!(state.quiet());
}
#[test]
fn recovery_is_idle_and_keeps_box_ids() {
let mut original = ActorState::new(false);
original.accept_user("prior".into()).unwrap();
let boxes = original.boxes().to_vec();
let mut state = ActorState::recover(boxes, false).unwrap();
assert!(state.quiet());
state.accept_system("next".into()).unwrap();
assert_eq!(state.boxes().last().unwrap().id().get(), 2);
}
#[test]
fn retry_uses_a_fresh_job_and_the_same_frontier() {
let mut state = ActorState::new(false);
state.accept_user("ask".into()).unwrap();
let first = state.begin_inference().unwrap().unwrap();
state.stall_inference(first.job, "retry".into()).unwrap();
assert_eq!(state.take_halt(), Some("retry".into()));
state.restart().unwrap();
let second = state.begin_inference().unwrap().unwrap();
assert!(second.job > first.job);
assert_eq!(second.frontier, first.frontier);
assert_eq!(state.boxes().len(), 1);
state.complete_inference(second.job, String::new()).unwrap();
}
#[test]
fn wrong_job_and_halt_are_transactional() {
let mut state = ActorState::new(true);
let start = state.begin_inference().unwrap().unwrap();
assert!(matches!(
state.append_stage(start.job + 1, String::new(), vec![]),
Err(StateError::WrongInference { .. })
));
assert!(state.halt("stop".into()));
assert!(matches!(state.restart(), Err(StateError::Busy)));
state.complete_inference(start.job, String::new()).unwrap();
assert_eq!(state.take_halt(), Some("stop".into()));
assert!(state.quiet());
}
#[test]
fn recovered_unknown_box_is_constructible_as_a_chatbox() {
let unknown = ChatBox::new(
crate::BoxId::new(1),
"Unrecognized".into(),
"contents".into(),
"unknown/v1".into(),
"hidden".into(),
);
let state = ActorState::recover(vec![unknown], false).unwrap();
assert_eq!(state.boxes()[0].hidden_type(), "unknown/v1");
}